# electron-PC端多端融合方案 > 每天都要写第二天的 todoList。有一天在写的时候突然想到,为了让自己清楚知道自己需要做啥、做了多少、还剩多少没做,想写一个电脑端程序,在技术选型的时候就选了 electron。 ## 一、 方案选型 3天时间写了个 PC 端应用程序。先看看结果吧 ![Todo1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-TodoApp1.png) ![Todo1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-TodoApp2.png) ![Todo1](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-TodoApp3.png) 为什么要选 electron 作为 pc 端开发方案? 史前时代,以 MFC 为代表的技术栈,开发效率较低,维护成本高。 后来使用 QT 技术,特点是使用 DirectUI + 面向对象 + XML 定义 UI,适用于小型软件、性能要求、包大小、UI 复杂度叫高的需求。 再到后来,以 QT Quick 为代表的技术,特点是框架本身提供子控件,基于子控件组合来创建新的控件。类似于 ActionScript 的脚本化界面逻辑代码。 新时代主要是以 [electron](https://electronjs.org) 和 [Cef](https://bitbucket.org/chromiumembedded/cef) 为 代表。特点是界面开发以 Web 技术为主,部分逻辑需要 Native 代码实现。大家都熟悉的 VS Code 就是使用 electron 开发的。适用于 UI 变化较多、体积限制不大、开发效率高的场景。 拿 C 系列写应用程序的体验不好,累到奔溃。再加上有 Hybrid、React Native、iOS、Vue、React 等开发经验,electron 是不二选择。 ## 二、 Quick start 执行下面命令快速体验 Hello world,也是官方给的一个 Demo。 ```shell git clone https://github.com/electron/electron-quick-start cd electron-quick-start npm install && npm start ``` 简单介绍下 Demo 工程,工程目录如下所示 ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electron-packagejson.png) 在终端执行 `npm start` 执行的是 package.json 中的 `scripts` 节点下的 start 命令,也就是 `electron .`,`.` 代表执行 main.js 中的逻辑。 ```Javascript // Modules to control application life and create native browser window const {app, BrowserWindow} = require('electron') const path = require('path') function createWindow () { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) // and load the index.html of the app. mainWindow.loadFile('index.html') // Open the DevTools. mainWindow.webContents.openDevTools() } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(createWindow) // Quit when all windows are closed. app.on('window-all-closed', function () { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') app.quit() }) app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow() }) // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. ``` 写过 Vue、React、Native 的人看代码很容易,因为应用程序的生命周期钩子函数是很重要的,开发者根据需求在钩子函数里面做相应的视图创建、初始化、销毁对象等等。比如 electron 中的 activate、window-all-closed 等。 app 对象在 `whenReady` 的时候执行 createWindow 方法。内部创建了一个 `BrowserWindow` 对象,指定了大小和功能设置(webPreferences Object (可选) - 网页功能的设置。其中 preload String (可选) - 在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。 当 node integration 关闭时, 预加载的脚本将从全局范围重新引入 node 的全局引用标志)。 `mainWindow.loadFile('index.html')` 加载了同级目录下的 index.html 文件。也可以加载服务器资源(部署好的网页),比如 `win.loadURL('https://github.com/FantasticLBP')` ```javascript // All of the Node.js APIs are available in the preload process. // It has the same sandbox as a Chrome extension. window.addEventListener('DOMContentLoaded', () => { const replaceText = (selector, text) => { const element = document.getElementById(selector) if (element) element.innerText = text } console.table(process) console.info(process.versions) for (const type of ['chrome', 'node', 'electron']) { replaceText(`${type}-version`, process.versions[type]) } }) ``` 接下去看看 preload.js。在页面运行其他脚本之前预先加载指定的脚本,无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。Demo 中的逻辑很简单,就是读取 process.versions 对象中的 node、chrome、electron 的版本信息并展示出来。 index.html 中的内容就是主页面显示的内容。 ## 三、 实现原理 electron 分为渲染进程和主进程。 😂 和 Native 中的概念不一样的是 electron 中主进程只有一个,渲染进程(也就是 UI 进程) 有多个。主进程在后台运行,每次打开一个界面,会新开一个新的渲染进程。 - 渲染进程: 用户看到的 web 界面就是由渲染进程绘制出来的,包括 html、css、js。 - 主进程:electron 运行 package.json 中的 main.js 脚本的进程被称为主进程。在主进程中运行的脚本通过创建 web 页面来展示用户界面。一个 electron 应用程序总是只有一个主进程。 ### 1. Chromium 架构 ![Chromium 架构](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-03-ChromiumArch.png) 这张图是 chromium 多进程架构图。早在2007年之前,市面上的浏览器都是单进程架构。单进程浏览器指的是浏览器的所有功能模块都是运行在同一个进程里的,这些模块包括网络、插件、Javascript 运行环境、渲染引擎和页面等。如此复杂的功能都在一个进程内运行,所以导致浏览器出现不稳定、不安全、不流畅等问题。 多进程架构的浏览器解决了上述问题,至于如何解决的以后的文章会专门讲解,不是本文的主要内容。 简单描述下。 - 主进程中的 `RenderProcessHost` 和 render 进程中的 `RenderProcess` 是用来处理进程间通信的(IPC)。 - Render 进程中的 RenderView 内容基于 WebKit 排版展示出来的 - Render 进程中的 `ResourceDispatcher` 是用来处理资源请求的。Render 进程中如果有请求则创建一个请求 ID,转发到 IPC,由 Browser 进程中处理后返回 - Chromium 是多进程架构,包括一个主进程,多个渲染进程 对于 chromium 多进程架构感兴趣的可以点击这个链接查看更多资料-[Multi-process Architecture](http://www.chromium.org/developers/design-documents/multi-process-architecture)。 ### 2. Electron 架构 ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-03-ElectronArch.png) Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲染进程。但是也有区别 - 在各个进行中暴露了 Native API ,提供了 Native 能力。 - 引入了 Node.js,所以可以使用 Node 的能力 **技术难点**:由于 Electron 内部整合了 Chromium 和 Node.js,主线程在某个时刻只可以执行一个事件循环,但是2者的事件循环机制不一样,Node.js 的事件循环基于 [libuv](https://github.com/libuv/libuv),但是 Chromium 基于 [message bump](https://chromium.googlesource.com/chromium/chromium/+/master/base/message_pump.h)。 所以 Electron 原理的重点就是「如何整合事件循环」。2种思路 - Chromium 集成到 Node.js 中:用 libuv 实现 messagebump(Node-Webkit 就是这么干的,缺点挺多) - Node.js 集成到 Chromium 中(Electron 所采用的方式) 后来随着 libuv 引入 backend_fd 的概念,相当于是 libuv 轮询事件的文件描述符。通过轮训 backend_fd 可以知道 libuv 的新事件。所以 Electron 采取的做法就是将 Node.js 集成到 Chromium 中。 ![Node.js与Chromium通信](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-03-ChromiumCommunicateWithNode.png) 上图描述了 Node.js 如何融入到 Chromium 中。描述下原理 - Electron 新起一个安全线程去轮训 backend_fd - 当检测到一个新的 backend_fd,也就是一个新的 Node.js 事件之后,通过 PostTask 转发到 Chromium 的事件循环中 上述2个步骤完成了 Electron 的事件循环。 ## 四、 如何调试 调试分为主进程调试和渲染进程调试。 ### 1. 渲染进程调试 看到 Demo 工程中执行 `npm start` 之后可以看到主界面,Mac 端快捷键 `comand + option + i`,唤出调试界面,类似于 chrome 下的 devtools。其实就是无头浏览器的那些东西。或者在代码里打开调试模式 `mainWindow.webContents.openDevTools()` 。 工程采用 Electron + Vue 技术,下面截图 Vue-devtools 很方便查看 Vue 组件层级等 Vue 相关的调试 ![渲染进程调试](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronVue.png) ### 2. 主进程调试方式 主进程调试有2种方法 方法一:利用 chrome inspect 功能进行调试 - 需要在启动的时候给 `package.json` 中的 scripts 节点下的 start 命令加上调试开关 ```shell --inspect=[port] // electrom --inspect=8001 yourApp ``` - 然后打开浏览器,在地址栏输入 `chrome://inspect` - 点击 `configure`,在弹出的面板中填写需要调试的端口信息 ![chrome inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electronChromeInspect.png) - 重新开启服务 `npm start`,在 chrome inspect 面板的 `Target` 节点中选择需要调试的页面 - 在面板中可以看到主进程执行的 `main.js`。可以加断点进行调试 ![chrome inspect](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electron-MainProcessInspect.png) 方法二:利用 VS Code 调试 electron 主进程。 - 在 VS Code 的左侧菜单栏,第四个功能模块就是调试,点击调试,弹出对话框让你添加调试配置文件 `launch.json` - 编辑 launch.json 的文件内容。如下 ```json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug main process", "cwd": "${workspaceRoot}", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" }, "args": ["."], "outputCapture": "std" } ] } ``` - 在调试模块电机绿色小三角,会运行程序,可以添加断点信息。整体界面如下所示。可以单步调试、可以暂停、鼠标移上去可以看到对象的各种信息。 ![VS Code 调试功能](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-04-21-electromDebugInVSCode.png) ### 3. 主进程调试之 hot reload Electron 的渲染进程中的代码改变了,使用 Command + R 可以刷新,但是修改主进程中的代码则必须重新启动 `yarn run dev` 。效率低下,所以为了提升开发效率,有必要解决这个问题 Webpack 有一个 api: `watch-run`,可以针对代码文件检测,有变化则 Restart ![main Process reload](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronMainProcessHotReload.png) ## 五、 开发 tips 1. 或许会为网页添加事件代码,但是页面看到的内容是渲染进程,所以事件相关的逻辑代码应该写在 html 引入的 `render.js` 中。 2. 在 `render.js` 中写 Node 代码的时候需要在 `main.js` 初始化 BrowserWindow 的时候,在 webPreferences 节点下添加 `nodeIntegration: true` 。不然会报错:renderer.js:9 Uncaught ReferenceError: process is not defined。 3. 从 Chrome Extenstion V2 开始,不允许执行任何 inline javascript 代码在 html 中。不支持以内联方式写事件绑定代码。比如 `` ```shell Refused to execute inline event handler because it violates the following Content Security Policy directive: ``` 4. 利用 electron 进行开发的时候,可以看成是 NodeJS + chromium + Web 前端开发技术。NodeJS 拥有文件访问等后端能力,chromium 提供展示功能,以及网络能力(electron 网络能力不是 NodeJS 提供的,而是 chromium 的 net 模块提供的)。web 前端开发技术方案都可以应用在 electron 中,比如 Vue、React、Bootstrap、sass 等。 5. 在工程化角度看,使用 yarn 比 npm 好一些,因为 yarn 会缓存已经安装过的依赖,其他项目只要发现存在缓存,则读取本地的包依赖,会更加快速。 6. 在使用 Vue、React 开发 electron 应用时,可以使用 npm 或 yarn install 包,也可以使用 electron-vue 脚手架工具。 ```shell vue init simulatedgreg/electron-vue my-project cd my-project npm install npm run dev ``` 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: ", 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 多窗口与单窗口应用区别 ![用途](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-03-ElectronUsage.png) 10. 知道 Electron 开发原理,所以大部分时间是在写前端代码。所以根据团队技术沉淀、选择对应的前端框架,比如 Vue、React、Angular。 11. 也许开发并不难,难在视觉和 UX。很多写过网页的开发者或者以前端的视觉去写 Electron App 难免会写出网页版的桌面应用程序,说难听点,就是四不像 😂。所以需要转变想法,这是在开发桌面应用程序。 12. Electron 和 Web 开发相比,各自有侧重点 ![electronAndWeb](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronAndWeb.png) ## 六、 技术体系搭建 其实一个技术本身的难易程度并不是能否在自己企业、公司、团队内顺利使用的唯一标尺,其配套的 CI/CD、APM、埋点系统、发布更新、灰度测试等能否与现有的系统以较小成本融合才是很大的决定要素。因为某个技术并不是非常难,要是大多数开发者觉得很难,那它设计上就是失败的。 ### 1. 构建 ![构建](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronBuild.png) ### 2. 工程解耦 ![工程解耦](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronCode.png) ### 3. 问题定位 Electron 提供的 crash 信息进行包装。 ![crash 分析](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2020-05-04-ElectronCrash.png) 引申资料 - [electron-vue架构解析-开发环境启动流程分析](https://dushaofeng.github.io/2018/06/08/electron-vue架构解析-开发环境启动流程分析/)