docs: Electron 原理

This commit is contained in:
杭城小刘
2020-05-12 13:44:07 +08:00
parent 966910d38d
commit f546284346
12 changed files with 252 additions and 44 deletions

View File

@@ -1,6 +1,8 @@
# electron-PC端多端融合方案
# ElectronPC 端多端融合方案
> 每天都要写第二天的 todoList。有一天在写的时候突然想到为了让自己清楚知道自己需要做啥、做了多少、还剩多少没做想写一个电脑端程序在技术选型的时候就选了 electron。
>
> 本篇文章的目的不是讲解 API 如何使用,想知道这些可以直接看[官方文档](https://www.electronjs.org/docs)。本文目的旨在讲明如何技术如何选择、如何快速上手、如何调试、Electron 底层原理、工程体系方面的总结。
@@ -35,7 +37,7 @@ npm install && npm start
```
简单介绍下 Demo 工程,工程目录如下所示
![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electron-packagejson.png)
![工程目录](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electron-packagejson.png)
在终端执行 `npm start` 执行的是 package.json 中的 `scripts` 节点下的 start 命令,也就是 `electron .``.` 代表执行 main.js 中的逻辑。
@@ -84,10 +86,17 @@ app.on('activate', function () {
```
写过 Vue、React、Native 的人看代码很容易,因为应用程序的生命周期钩子函数是很重要的,开发者根据需求在钩子函数里面做相应的视图创建、初始化、销毁对象等等。比如 electron 中的 activate、window-all-closed 等。
app 对象在 `whenReady` 的时候执行 createWindow 方法。内部创建了一个 `BrowserWindow` 对象,指定了大小和功能设置webPreferences Object (可选) - 网页功能的设置。其中 preload String (可选) - 在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入 node 的全局引用标志)
app 对象在 `whenReady` 的时候执行 createWindow 方法。内部创建了一个 `BrowserWindow` 对象,指定了大小和功能设置。
1. webPreferences Object (可选) - 网页功能的设置。
2. preload String (可选) - 在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入 node 的全局引用标志。
`mainWindow.loadFile('index.html')` 加载了同级目录下的 index.html 文件。也可以加载服务器资源(部署好的网页),比如 `win.loadURL('https://github.com/FantasticLBP')`
接下去看看 preload.js
```javascript
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
@@ -104,17 +113,16 @@ window.addEventListener('DOMContentLoaded', () => {
})
```
接下去看看 preload.js。在页面运行其他脚本之前预先加载指定的脚本,无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。Demo 中的逻辑很简单,就是读取 process.versions 对象中的 node、chrome、electron 的版本信息并展示出来。
在页面运行其他脚本之前预先加载指定的脚本,无论页面是否集成 Node 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。Demo 中的逻辑很简单,就是读取 `process.versions` 对象中的 node、chrome、electron 的版本信息并展示出来。
index.html 中的内容就是主页面显示的内容。
`index.html` 中的内容就是主页面显示的内容。一般不会直接写 html、css、js都会根据技术背景选择前端框架比如 Vue、React 等,或者模版引擎 ejs 等。
## 三、 实现原理
electron 分为渲染进程和主进程。 😂 和 Native 中的概念不一样的是 electron 中主进程只有一个,渲染进程(也就是 UI 进程) 有多个。主进程在后台运行,每次打开一个界面,会新开一个新的渲染进程。
electron 分为**渲染进程和主进程**。和 Native 中的概念不一样的是 electron 中主进程只有一个,渲染进程(也就是 UI 进程) 有多个。主进程在后台运行,每次打开一个界面,会新开一个新的渲染进程。
- 渲染进程: 用户看到的 web 界面就是由渲染进程绘制出来的,包括 html、css、js。
- 主进程electron 运行 package.json 中的 main.js 脚本的进程被称为主进程。在主进程中运行的脚本通过创建 web 页面来展示用户界面。一个 electron 应用程序总是只有一个主进程。
@@ -123,11 +131,170 @@ electron 分为渲染进程和主进程。 😂 和 Native 中的概念不一样
### 1. Chromium 架构
浏览器分为单进程和多进程架构。下面先讲讲 Chrome 为代表的浏览器过去和未来。
#### 1.1 单进程浏览器
单进程浏览器指的是浏览器的所有功能模块都是运行在同一个进程里的这些模块包括网络、插件、Javascript 运行环境、渲染引擎和页面等。如此复杂的功能都在一个进程内运行,所以导致浏览器出现不稳定、不安全、不流畅等问题。
![单进程浏览器架构示意图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-07-BrowserSingleThread.png)
早在2007年之前市面上的浏览器都是单进程架构。
- 问题1: 不稳定
早期浏览器需要借助插件来实现类似 Web 视频、Web 游戏等各种“强大”的功能。但插件往往是最容易出现问题的模块。此外因为运行在浏览器进程中,所以一个插件的意外奔溃到导致整个浏览器到的奔溃。
除了插件之外,**渲染引擎模块也是不稳定的**。通常一些复杂的 Javascript 代码就有可能导致渲染引擎模块的奔溃。和插件一样,渲染引擎的奔溃会导致整个浏览器奔溃。
- 问题2: 不流畅
从单进程浏览器架构图看出所有页面的渲染模块、Javascript 执行环境、插件都是在一个线程中执行的。这意味着同一时刻只有一个模块可以执行。
```javascript
function execUtilCrash() {
while (true) {
console.log("Stay hungry, stay foolish.");
}
}
execUtilCrash();
```
在单进程浏览器架构下,该代码在执行的时候会独占线程,导致其他运行在该线程中的模块没机会执行,浏览器中的所有页面都运行在该线程中,所以页面都没机会去执行任务,表现为整个浏览器失去响应,也就是卡顿。
**脚本、插件** 会让单进程浏览器变卡顿外,页面的内存泄露也会导致浏览器卡顿。通常浏览器内核是非常复杂的,运行一个复杂的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是随着使用时间的变长,内存泄漏问题越严重,内存占用越高,可用内存越来越少,浏览器会变得越来越慢。
- 问题3: 不安全
一般浏览器插件都是用 C/C++ 编写的,通过插件就可以获取到较多的操作系统资源,当你在页面上运行一个插件的时候,也就意味着这个插件能“完全”控制你的电脑。如果是恶意插件,那它就可以做一些窃取账号密码等,引发安全问题
#### 1.2 早期多进程架构浏览器
![早期 Chrome 进程架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-07-ChromEarlyArch.png)
上图2008年 Chrome 发布时的进程架构图。可以看出 Chrome 的页面是运行在单独的渲染进程中,同时页面的插件也是运行在单独的插件进行中的,进程之间通过 IPC 进行通信。
**解决了不稳定问题。**由于进程之间是彼此隔离的,所以当一个页面或者插件奔溃时,受影响的仅仅是当前的页面或者插件进程,并不会影响到浏览器和其他的页面。也就是说解决了早期浏览器某个页面或者插件奔溃导致整个浏览器的奔溃,从而解决了不稳定问题。
**解决了不流畅问题。** 同样Javascript 进行也是运行在渲染进程中的,所以即使当前 Javascript 阻塞了渲染进程,影响到的也只是当前的渲染页面,并不会影响到浏览器和其他页面或者插件进程(其他的页面的脚本是运行在自己的渲染进程中的)。
对于**内存泄漏的解决办法更加简单**。当关闭某个页面的时候,整个渲染进程就会被关闭,所以该进程所占用的内存都会被系统回收,于是轻松解决了浏览器页面的内存泄漏问题。
**解决了安全问题。**采用多进程架构可以使用**安全沙箱技术**。沙箱可以看成是操作系统给浏览器一个小黑盒黑盒内部可以执行程序但是不能访问操作系统资源、不能访问硬盘数据也不能在敏感位置读取任何数据例如你的文档和桌面。Chrome 把插件进程和渲染进程使用沙箱隔离起来,这样即使在渲染进程或者浏览器进程中执行了恶意代码,恶意代码也无法突破沙箱限制去获取系统权限。
沙箱隔离起来的进程必须使用 IPC 通道才可以与浏览器内核进程通信,通信进程就会进行安全的检查。
沙箱设计的目的是为了让不可信的代码运行在一定的环境中,从而限制这些代码访问隔离区之外的资源。如果因为某种原因,确实需要访问隔离区外的资源,那么就必须通过的指定的通道,这些通道会进行严格的安全检查,来判断请求的合法性。通道会采取默认拒绝的策略,一般采用封装 API 的方式来实现。
#### 1.3 目前多进程架构浏览器
Chrome 团队不断发展,目前架构有了较新变化,最新 Chrome 架构图如下所示
![最新 Chrome 进程架构图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-07-ChromeMordernArch.png)
最新 Chrome 浏览器包括1个网络进程、1个浏览器进程、1个 GPU 进程、多个渲染进程、多个插件进程。
- 浏览器进程:主要负责界面显示、用户交互、子进程管理,同时提供存储功能。
- 渲染进程:核心任务是将 HTML、CSS、Javascript 转换为用户可以与之的网页,排版引擎 Blink 和 Javascript 引擎 V8 都是运行在该进程中。默认情况下Chrome 为每个 tab 标签页创建一个新的渲染进程。出于安全考虑,渲染进程都是运行在沙箱机制下的。
- GPU 进程:最早 Chrome 刚发布的时候是没有 GPU 进程的,而 GPU 的使用初衷是实现 css 3D 效果。随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍需求。最后 Chrome 多进程架构中也引入了 GPU 进程。
- 网络进程:主要负责页面的网络资源请求加载。早期是作为一个模块运行在浏览器进程里面的,最近才独立出来作为一个单独的进程。
- 插件进程:主要负责插件的运行。因插件代码由普通开发者书写,所以在 QA 方面可能不是那么完善,代码质量参差不齐,插件容易奔溃,所以需要通过插件进程来隔离,以保证插件进程的奔溃不会对浏览器和页面造成影响。
所以你会发现打开一个页面查看进程发现有4个进程。凡事具有两面性上面说了多进程架构带来浏览器稳定性、安全性、流畅性但是也带来一些问题
- 更高资源占用:每个进程都会包含公共基础结构的副本(如 Javascript 运行环境),这意味着浏览器将会消耗更多的资源
- 更复杂的体系结构:浏览器各模块之间耦合度高、拓展性差,会导致现在的架构很难适应新需求。
Chrome 团队一直在寻求新的弹性方案,既可以解决资源占用较高问题吗,也可以解决复杂的体系架构问题。
#### 1.4 未来面向服务的架构
2016年 Chrome 官方团队使用 “**面向服务的架构**”Services Oriented Architecture简称 SOA的思想设计了最新的 Chrome 架构。Chrome 整体架构会向现代操作系统所采用的“面向服务的架构”方向发展。
之前的各种模块会被重构成为单独的服务Services每个服务都可以运行在独立的进程中访问服务必须使用定义好的接口通过 IPC 进行通信。从而构建一个更内聚、低耦合、易于维护和拓展的系统。
Chrome 最终把 UI、数据库、文件、设备、网络等模块重构为基础服务。下图是 “Chrome 面向服务的架构”的进程模型图
![Chrome ”面向服务架构“的进程模型图](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-08-ChromeMltipleProcessArch.png)
目前 Chrome 正处于老架构向新架构的过度阶段,且比较漫长。
Chrome 提供灵活的弹性架构在强大性能设备上会以多进程的方式运行基础服务但是在设备资源受限的情况下Chrome 会将很多服务整合到一个进程中,从而节省内存占用。
![Chrome弹性架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-08-FlexiableChromeMltipleProcessArch.png)
#### 1.5 小实验
测试环境: MacBook PromacOS 10.15.3、Chrome Version 81.0.4044.138 (Official Build) (64-bit)
操作步骤:
1. 打开 Chrome 浏览器
2. 在地址栏输入 `https://github.com/FantasticLBP`
3. 点击 Chrome 浏览器右上角 `...`,在下拉菜单中选择 `More Tools`,在对应的展开菜单中点击 `Task Manager`
实验现象:
![Chrome 进程仪表](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-08-ChromeProcessInspector.png)
实验结论:
仅仅打开了 1 个页面,为什么有 4 个进程?因为打开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个;如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
从而印证了上述观点。
#### 1.6 特殊情况
现象:我们在使用 Chrome 的时候还是会出现由于单个页面卡死最终崩溃导致所有页面崩溃的情况why
通常情况下是一个页面使用一个进程,但是,有一种情况,叫"同一站点(same-site)"具体地讲我们将“同一站点”定义为根域名例如github.com加上协议例如https:// 或者http://),还包含了该根域名下的所有子域名和不同的端口,比如下面这三个:
https://developer.github.com
https://www.github.com
https://www.github.com:8080
都是属于同一站点,因为它们的协议都是 https而根域名也都是 github.com。区别于浏览器同源策略。
Chrome 的默认策略是,每个标签对应一个渲染进程。但是如果从一个页面打开了新页面,而新页面和当前页面属于同一站点时,那么新页面会复用父页面的渲染进程。官方把这个默认策略叫 `process-per-site-instance`。
直白的讲,就是如果几个页面符合同一站点,那么他们将被分配到一个渲染进程里面去。
这种情况下,一个页面崩溃了,会导致同一站点的页面同时崩溃,因为他们使用了同一个渲染进程。
为什么要让他们跑在一个进程里面呢?
因为在一个渲染进程里面他们就会共享JS的执行环境也就是说A页面可以直接在B页面中执行脚本。因为是同一家的站点所以是有这个需求的
#### 1.7 Chromium 架构
![Chromium 架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-03-ChromiumArch.png)
这张图是 chromium 多进程架构图。早在2007年之前市面上的浏览器都是单进程架构。单进程浏览器指的是浏览器的所有功能模块都是运行在同一个进程里的这些模块包括网络、插件、Javascript 运行环境、渲染引擎和页面等。如此复杂的功能都在一个进程内运行,所以导致浏览器出现不稳定、不安全、不流畅等问题。
这张图是 chromium 多进程架构图。
多进程架构的浏览器解决了上述问题,至于如何解决的以后的文章会专门讲解,不是本文的主要内容。
@@ -235,7 +402,7 @@ Electron 架构和 Chromium 架构类似也是具有1个主进程和多个渲
}
```
- 在调试模块电机绿色小三角,会运行程序,可以添加断点信息。整体界面如下所示。可以单步调试、可以暂停、鼠标移上去可以看到对象的各种信息。
- 在调试模点击绿色小三角,会运行程序,可以添加断点信息。整体界面如下所示。可以单步调试、可以暂停、鼠标移上去可以看到对象的各种信息。
![VS Code 调试功能](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electromDebugInVSCode.png)
@@ -251,7 +418,9 @@ Webpack 有一个 api: `watch-run`,可以针对代码文件检测,有变化
## 五、 开发 tips
## 五、 开发 tips及其优化手段
### 1. 开发 tips
1. 或许会为网页添加事件代码,但是页面看到的内容是渲染进程,所以事件相关的逻辑代码应该写在 html 引入的 `render.js` 中。
@@ -279,34 +448,37 @@ Webpack 有一个 api: `watch-run`,可以针对代码文件检测,有变化
7. 开发完毕后需要设置应用程序的图标信息、版本号等,打包需要指定不同的平台。
8. 新开项目创建后会报错.
- `ERROR in Template execution failed: ReferenceError: process is not defined`。解决办法是使用 nvm 将 node 版本将为 10。
- 报错如下
```shell
┏ Electron -------------------
[11000:0615/095124.922:ERROR:CONSOLE(7574)] "Extension server error: Object not found: <top>", source: chrome-devtools://devtools/bundled/inspector.js (7574)
┗ ----------------------------
```
解决办法是在 main/index.dev.js 修改代码
```javascript
- require('electron-debug')({ showDevTools: true });
+ // NB: Don't open dev tools with this, it is causing the error
+ require('electron-debug')();
```
在 In main/index.js in the createWindow() function:
```javascript
mainWindow.loadURL(winURL);
+ // Open dev tools initially when in development mode
+ if (process.env.NODE_ENV === "development") {
+ mainWindow.webContents.on("did-frame-finish-load", () => {
+ mainWindow.webContents.once("devtools-opened", () => {
+ mainWindow.focus();
+ });
+ mainWindow.webContents.openDevTools();
+ });
+ }
```
初始化工程后会报错 `ERROR in Template execution failed: ReferenceError: process is not defined`。解决办法是使用 nvm 将 node 版本将为 10。
继续运行还是报错,如下
```shell
┏ Electron -------------------
[11000:0615/095124.922:ERROR:CONSOLE(7574)] "Extension server error: Object not found: <top>", source: chrome-devtools://devtools/bundled/inspector.js (7574)
┗ ----------------------------
```
解决办法是在 main/index.dev.js 修改代码
```javascript
- require('electron-debug')({ showDevTools: true });
+ // NB: Don't open dev tools with this, it is causing the error
+ require('electron-debug')();
```
在 In main/index.js in the createWindow() function:
```javascript
mainWindow.loadURL(winURL);
+ // Open dev tools initially when in development mode
+ if (process.env.NODE_ENV === "development") {
+ mainWindow.webContents.on("did-frame-finish-load", () => {
+ mainWindow.webContents.once("devtools-opened", () => {
+ mainWindow.focus();
+ });
+ mainWindow.webContents.openDevTools();
+ });
+ }
```
9. Electron 多窗口与单窗口应用区别
@@ -321,7 +493,42 @@ Webpack 有一个 api: `watch-run`,可以针对代码文件检测,有变化
![electronAndWeb](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronAndWeb.png)
### 2. 优化手段
#### 2.1 性能分析
因为开发过程中Electron 体验就是在开发一个前端项目,所以我们使用 Chrome 的 Performance 进行分析。
分析某段代码的执行过程,也可以通过下面命令生成分析文件,然后导入到 Chrome Performance 中分析:
```shell
# 输出 cpu 和 堆分析文件
node --cpu-prof --heap-prof -e "require('request)”“
```
#### 2.2 白屏优化
- 不管是 Native App 还是 Web 网页,骨架屏都是比较常见的一些技术手段。比如弱网下简书 App 的效果
- 懒加载。优先加载第一屏(主界面)所需的依赖,其他的依赖延迟加载
- 代码分割。Webpack 工具支持代码分割,这在前端中是很常见的一种优化手段
- Node 模块延迟加载或合并。Node 属于 CMD 规范,模块查找、文件读取都比较耗时,某些 Node 模块依赖模块较多、子模块较深这样首屏会很慢。所以延迟加载或者选择使用打包工具优化和合并 Node 模块。
- 打包优化。现代打包工具有非常多优化手段。 Webpack 支持代码压缩、预执行... 裁剪多余的代码, 减少运行时负担。模块合并后还可以减小 IO 。
- 和 Native App 中的 Hybrid 优化手段机制一样。可以对静态资源进行缓存(公司基础样式文件、基础组件等,设计资源更新策略)。使用 Service-Worker 进行静态资源拦截,或者在 Native 端做请求拦截,比如 Mac 端 NSURLProtocol
- 预加载机制。在登陆界面或者启动动画阶段,初始化主界面,然后将主界面设置到不可见状态。等启动或者登陆后再将主界面设置为可见状态
- 使用 Node API 尽量避免同步操作。一般 Node API 都有同步和异步2种 API比如 `fs` 文件读取模块
- 对内存影响较大的功能使用 Native 的能力,比如 Database、Image、Network、Animation 等。虽然 Web 都有对应的解决方案,但是原生对于这些功能的操作权限更大、内存控制更灵活、安全性更高、性能更佳
- 减小主进程压力。Electron 有1个主进程和多个渲染进程主进程是所有窗口的父进程它负责调度各种资源。如果主进程被阻塞将影响整个应用响应性能。
## 六、 技术体系搭建
@@ -350,4 +557,5 @@ Electron 提供的 crash 信息进行包装。
引申资料
- [electron-vue架构解析-开发环境启动流程分析](https://dushaofeng.github.io/2018/06/08/electron-vue架构解析-开发环境启动流程分析/)
- [chromium 进程模型](https://www.chromium.org/developers/design-documents/process-models)