每天都要写第二天的 todoList。有一天在写的时候突然想到,为了让自己清楚知道自己需要做啥、做了多少、还剩多少没做,想写一个电脑端程序,在技术选型的时候就选了 Electron。
本篇文章的目的不是讲解 API 如何使用,想知道这些可以直接看官方文档。本文目的旨在讲明如何技术如何选择、如何快速上手、如何调试、Electron 底层原理、工程体系方面的总结。
一、浅谈 GUI 系统
浏览器是如何将布局数据计算为像素数据的,你能实现出原理类似的渲染器吗?
浏览器在各个平台上的文字排版渲染结果是否一致,你能解释原因吗?
你所负责的前端应用,其渲染性能还有多大的提升空间,你能量化地证明吗?
你能设计实现出类似 RN 和小程序那样的 Hybrid 方案吗?
你能自己控制 GPU 渲染管线,实现渲染的硬件加速吗?
GUI 起源:从 1979 年乔布斯造访施乐 PARC 算起
GUI 架构:过程化绘制(drawLine、drawRect)-> 面向对象抽象时代 -> 界面与样式分离时代 -> MVC、MVVM 时代 -> 声明式、组件式时代(Vue、React、RN、Weex、Flutter)
我们可以看到不变的是:随着计算机科学技术的发展,为了实现某个效果,一流程序员或者组织不断研发各种技术框架,来提高开发效率和效果。Electron 就是这条历史长河中诞生的 PC 端技术框架之一。
二、 技术选型
3天时间写了个 PC 端应用程序。先看看结果吧
为什么要选 Electron 作为 pc 端开发方案?
史前时代,以 MFC 为代表的技术栈,开发效率较低,维护成本高。 后来使用 QT 技术,特点是使用 DirectUI + 面向对象 + XML 定义 UI,适用于小型软件、性能要求、包大小、UI 复杂度叫高的需求。 再到后来,以 QT Quick 为代表的技术,特点是框架本身提供子控件,基于子控件组合来创建新的控件。类似于 ActionScript 的脚本化界面逻辑代码。 新时代主要是以 Electron 和 Cef 为 代表。特点是界面开发以 Web 技术为主,部分逻辑需要 Native 代码实现。大家都熟悉的 VS Code 就是使用 Electron 开发的。适用于 UI 变化较多、体积限制不大、开发效率高的场景。
拿 C 系列写应用程序的体验不好,累到奔溃。再加上有 Hybrid、React Native、iOS、Vue、React 等开发经验,Electron 是不二选择。
三、 Quick start
执行下面命令快速体验 Hello world,也是官方给的一个 Demo。
git clone https://github.com/Electron/Electron-quick-start
cd Electron-quick-start
npm install && npm start
简单介绍下 Demo 工程,工程目录如下所示
在终端执行 npm start
执行的是 package.json 中的 scripts
节点下的 start 命令,也就是 Electron .
,.
代表执行 main.js 中的逻辑。
// 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 的全局引用标志。
- preload: String (可选) - 在页面运行其他脚本之前预先加载指定的脚本。无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。 当 node
mainWindow.loadFile('index.html')
加载了同级目录下的 index.html 文件。也可以加载服务器资源(部署好的网页),比如 win.loadURL('https://github.com/FantasticLBP')
接下去看看 preload.js
// 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])
}
})
在页面运行其他脚本之前预先加载指定的脚本,无论页面是否集成 Node, 此脚本都可以访问所有 Node API 脚本路径为文件的绝对路径。Demo 中的逻辑很简单,就是读取 process.versions
对象中的 node、chrome、Electron 的版本信息并展示出来。
index.html
中的内容就是主页面显示的内容。一般不会直接写 html、css、js,都会根据技术背景选择前端框架,比如 Vue、React 等,或者模版引擎 ejs 等。
四、 实现原理
Electron 分为渲染进程和主进程。和 Native 中的概念不一样的是 Electron 中主进程只有一个,渲染进程(也就是 UI 进程) 有多个。主进程在后台运行,每次打开一个界面,会新开一个新的渲染进程。
- 渲染进程: 用户看到的 web 界面就是由渲染进程绘制出来的,包括 html、css、js。
- 主进程:Electron 运行 package.json 中的 main.js 脚本的进程被称为主进程。在主进程中运行的脚本通过创建 web 页面来展示用户界面。一个 Electron 应用程序总是只有一个主进程。
1. Chromium 架构
浏览器分为单进程和多进程架构。下面先讲讲 Chrome 为代表的浏览器过去和未来。
1.1 单进程浏览器
单进程浏览器指的是浏览器的所有功能模块都是运行在同一个进程里的,这些模块包括网络、插件、Javascript 运行环境、渲染引擎和页面等。如此复杂的功能都在一个进程内运行,所以导致浏览器出现不稳定、不安全、不流畅等问题。
早在2007年之前,市面上的浏览器都是单进程架构。
-
问题1: 不稳定
早期浏览器需要借助插件来实现类似 Web 视频、Web 游戏等各种“强大”的功能。但插件往往是最容易出现问题的模块。因为运行在浏览器进程中,所以一个插件的意外奔溃到导致整个浏览器到的奔溃。
除了插件之外,渲染引擎模块也是不稳定的。通常一些复杂的 Javascript 代码就有可能导致渲染引擎模块的奔溃。和插件一样,渲染引擎的奔溃会导致整个浏览器奔溃。
-
问题2: 不流畅
从单进程浏览器架构图看出,所有页面的渲染模块、Javascript 执行环境、插件都是在一个线程中执行的。这意味着同一时刻只有一个模块可以执行。
function execUtilCrash() { while (true) { console.log("Stay hungry, stay foolish."); } } execUtilCrash();
在单进程浏览器架构下,该代码在执行的时候会独占线程,导致其他运行在该线程中的模块没机会执行,浏览器中的所有页面都运行在该线程中,所以页面都没机会去执行任务,表现为整个浏览器失去响应,也就是卡顿。
脚本、插件 会让单进程浏览器变卡顿外,页面的内存泄露也会导致浏览器卡顿。通常浏览器内核是非常复杂的,运行一个复杂的页面再关闭页面,会存在内存不能完全回收的情况,这样导致的问题是随着使用时间的变长,内存泄漏问题越严重,内存占用越高,可用内存越来越少,浏览器会变得越来越慢。
-
问题3: 不安全
一般浏览器插件都是用 C/C++ 编写的,通过插件就可以获取到较多的操作系统资源,当你在页面上运行一个插件的时候,也就意味着这个插件能“完全”控制你的电脑。如果是恶意插件,那它就可以做一些窃取账号密码等,引发安全问题
1.2 早期多进程架构浏览器
上图2008年 Chrome 发布时的进程架构图。可以看出 Chrome 的页面是运行在单独的渲染进程中,同时页面的插件也是运行在单独的插件进行中的,进程之间通过 IPC 进行通信。
解决了不稳定问题。由于进程之间是彼此隔离的,所以当一个页面或者插件奔溃时,受影响的仅仅是当前的页面或者插件进程,并不会影响到浏览器和其他的页面。也就是说解决了早期浏览器某个页面或者插件奔溃导致整个浏览器的奔溃,从而解决了不稳定问题。
解决了不流畅问题。 同样,Javascript 进行也是运行在渲染进程中的,所以即使当前 Javascript 阻塞了渲染进程,影响到的也只是当前的渲染页面,并不会影响到浏览器和其他页面或者插件进程(其他的页面的脚本是运行在自己的渲染进程中的)。
对于内存泄漏的解决办法更加简单。当关闭某个页面的时候,整个渲染进程就会被关闭,所以该进程所占用的内存都会被系统回收,于是轻松解决了浏览器页面的内存泄漏问题。
解决了安全问题。采用多进程架构可以使用安全沙箱技术。沙箱可以看成是操作系统给浏览器一个小黑盒,黑盒内部可以执行程序,但是不能访问操作系统资源、不能访问硬盘数据,也不能在敏感位置读取任何数据,例如你的文档和桌面。Chrome 把插件进程和渲染进程使用沙箱隔离起来,这样即使在渲染进程或者浏览器进程中执行了恶意代码,恶意代码也无法突破沙箱限制去获取系统权限。
沙箱隔离起来的进程必须使用 IPC 通道才可以与浏览器内核进程通信,通信进程就会进行安全的检查。
沙箱设计的目的是为了让不可信的代码运行在一定的环境中,从而限制这些代码访问隔离区之外的资源。如果因为某种原因,确实需要访问隔离区外的资源,那么就必须通过的指定的通道,这些通道会进行严格的安全检查,来判断请求的合法性。通道会采取默认拒绝的策略,一般采用封装 API 的方式来实现。
1.3 目前多进程架构浏览器
Chrome 团队不断发展,目前架构有了较新变化,最新 Chrome 架构图如下所示
最新 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 正处于老架构向新架构的过度阶段,且比较漫长。
Chrome 提供灵活的弹性架构,在强大性能设备上会以多进程的方式运行基础服务,但是在设备资源受限的情况下,Chrome 会将很多服务整合到一个进程中,从而节省内存占用。
1.5 小实验
测试环境: MacBook Pro(macOS 10.15.3)、Chrome Version 81.0.4044.138 (Official Build) (64-bit)
操作步骤:
- 打开 Chrome 浏览器
- 在地址栏输入
https://github.com/FantasticLBP
- 点击 Chrome 浏览器右上角
...
,在下拉菜单中选择More Tools
,在对应的展开菜单中点击Task Manager
实验现象:
实验结论:
仅仅打开了 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 多进程架构图。
多进程架构的浏览器解决了上述问题,至于如何解决的以后的文章会专门讲解,不是本文的主要内容。
简单描述下。
- 主进程中的
RenderProcessHost
和 render 进程中的RenderProcess
是用来处理进程间通信的(IPC)。 - Render 进程中的 RenderView 内容基于 WebKit 排版展示出来的
- Render 进程中的
ResourceDispatcher
是用来处理资源请求的。Render 进程中如果有请求则创建一个请求 ID,转发到 IPC,由 Browser 进程中处理后返回 - Chromium 是多进程架构,包括一个主进程,多个渲染进程
对于 chromium 多进程架构感兴趣的可以点击这个链接查看更多资料-Multi-process Architecture。
2. Electron 架构
Electron 架构和 Chromium 架构类似,也是具有1个主进程和多个渲染进程。但是也有区别
- 在各个进行中暴露了 Native API ,提供了 Native 能力。
- 引入了 Node.js,所以可以使用 Node 的能力
技术难点:由于 Electron 内部整合了 Chromium 和 Node.js,主线程在某个时刻只可以执行一个事件循环,但是2者的事件循环机制不一样,Node.js 的事件循环基于 libuv,但是 Chromium 基于 message bump。
所以 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 中。描述下原理
- 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 相关的调试
2. 主进程调试方式
主进程调试有2种方法
方法一:利用 chrome inspect 功能进行调试
- 需要在启动的时候给
package.json
中的 scripts 节点下的 start 命令加上调试开关
--inspect=[port]
// electrom --inspect=8001 yourApp
- 然后打开浏览器,在地址栏输入
chrome://inspect
- 点击
configure
,在弹出的面板中填写需要调试的端口信息 - 重新开启服务
npm start
,在 chrome inspect 面板的Target
节点中选择需要调试的页面 - 在面板中可以看到主进程执行的
main.js
。可以加断点进行调试
方法二:利用 VS Code 调试 Electron 主进程。
-
在 VS Code 的左侧菜单栏,第四个功能模块就是调试,点击调试,弹出对话框让你添加调试配置文件
launch.json
-
编辑 launch.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" } ] }
-
在调试模点击绿色小三角,会运行程序,可以添加断点信息。整体界面如下所示。可以单步调试、可以暂停、鼠标移上去可以看到对象的各种信息。
3. 主进程调试之 hot reload
Electron 的渲染进程中的代码改变了,使用 Command + R 可以刷新,但是修改主进程中的代码则必须重新启动 yarn run dev
。效率低下,所以为了提升开发效率,有必要解决这个问题
Webpack 有一个 api: watch-run
,可以针对代码文件检测,有变化则 Restart
六、 软件更新方式
更新方式 | 手动更新 | 文件覆盖 | 自动更新 | 操作系统级别的应用商店 |
---|---|---|---|---|
优点 | 简单、稳定 | 下载过程快 | 稳定、快、打扰少 | 统一、稳定 |
缺点 | 过程繁琐、慢、影响使用、更新率低 | 慢、实现比较复杂、稳定性差、写文件失败 | 实现复杂 | 受应用商店局限 |
适用场景 | 低频更新、用户粘性高、作为 各种升级技术的降级方案 | 打补丁 | 高频更新软件、体验要求高 | 操作系统应用商店上架的软件 |
Electron 官方给出了解决方案 Squirrel,基于 Squirrel 框架完成的自动更新,这其实就是图上“自定更新”的类别。
六、 开发 tips及其优化手段
1. 开发 tips
-
或许会为网页添加事件代码,但是页面看到的内容是渲染进程,所以事件相关的逻辑代码应该写在 html 引入的
render.js
中。 -
在
render.js
中写 Node 代码的时候需要在main.js
初始化 BrowserWindow 的时候,在 webPreferences 节点下添加nodeIntegration: true
。不然会报错:renderer.js:9 Uncaught ReferenceError: process is not defined。 -
从 Chrome Extenstion V2 开始,不允许执行任何 inline javascript 代码在 html 中。不支持以内联方式写事件绑定代码。比如
<button onclick="handleCPU">查看</button>
Refused to execute inline event handler because it violates the following Content Security Policy directive:
-
利用 Electron 进行开发的时候,可以看成是 NodeJS + chromium + Web 前端开发技术。NodeJS 拥有文件访问等后端能力,chromium 提供展示功能,以及网络能力(Electron 网络能力不是 NodeJS 提供的,而是 chromium 的 net 模块提供的)。web 前端开发技术方案都可以应用在 Electron 中,比如 Vue、React、Bootstrap、sass 等。
-
在工程化角度看,使用 yarn 比 npm 好一些,因为 yarn 会缓存已经安装过的依赖,其他项目只要发现存在缓存,则读取本地的包依赖,会更加快速。
-
在使用 Vue、React 开发 Electron 应用时,可以使用 npm 或 yarn install 包,也可以使用 Electron-vue 脚手架工具。
vue init simulatedgreg/Electron-vue my-project cd my-project npm install npm run dev
-
开发完毕后需要设置应用程序的图标信息、版本号等,打包需要指定不同的平台。
-
新开项目创建后会报错.
初始化工程后会报错 ERROR in Template execution failed: ReferenceError: process is not defined
。解决办法是使用 nvm 将 node 版本将为 10。
继续运行还是报错,如下
┏ 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 修改代码
- 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:
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();
+ });
+ }
-
Electron 多窗口与单窗口应用区别
-
知道 Electron 开发原理,所以大部分时间是在写前端代码。所以根据团队技术沉淀、选择对应的前端框架,比如 Vue、React、Angular。
-
也许开发并不难,难在视觉和 UX。很多写过网页的开发者或者以前端的视觉去写 Electron App 难免会写出网页版的桌面应用程序,说难听点,就是四不像 😂。所以需要转变想法,这是在开发桌面应用程序。
-
Electron 和 Web 开发相比,各自有侧重点
2. 优化手段
2.1 性能分析
因为开发过程中,Electron 体验就是在开发一个前端项目,所以我们使用 Chrome 的 Performance 进行分析。
分析某段代码的执行过程,也可以通过下面命令生成分析文件,然后导入到 Chrome Performance 中分析:
# 输出 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个主进程和多个渲染进程,主进程是所有窗口的父进程,它负责调度各种资源。如果主进程被阻塞,将影响整个应用响应性能。
七、 技术体系搭建:全景
其实一个技术本身的难易程度并不是能否在自己企业、公司、团队内顺利使用的唯一标尺,其配套的 CI/CD、APM、埋点系统、发布更新、灰度测试等能否与现有的系统以较小成本融合才是很大的决定要素。因为某个技术并不是非常难,要是大多数开发者觉得很难,那它设计上就是失败的。
1. 构建
2. 工程解耦
3. 问题定位
Electron 提供的 crash 信息进行包装。
import { BrowserWindow, app, dialog} from 'Electron';
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.on('crashed',
const options = {
type: 'error',
title: '进程崩溃了',
message: '这个进程已经崩溃.',
buttons: ['重载', '退出'],
};
recordCrash().then(() => {
dialog.showMessageBox(options, (index) => {
if (index === 0) reloadWindow(mainWindow);
else app.quit();
});
}).catch((e) => {
console.log('err', e);
});
})
function recordCrash() {
return new Promise(resolve => {
// 崩溃日志请求成功....
resolve();
})
}
function reloadWindow(mainWin) {
if (mainWin.isDestroyed()) {
app.relaunch();
app.exit(0);
} else {
BrowserWindow.getAllWindows().forEach((w) => {
if (w.id !== mainWin.id) w.destroy();
});
mainWin.reload();
}
}
// index.js
app.on('will-finish-launching', () => {
if(!isDev) {
require('./updater.js')
}
require('./crash-reporter').init()
})
// crash-reporter.js
const {crashReporter} = require('Electron')
function init() {
crashReporter.start({
productName: 'ZanPandora',
companyName: 'youzan',
submitURL: 'http://127.0.0.1:33855/crash',
})
}
module.exports = {init}
// server
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const serve = require('koa-static-server')
const router = new Router()
const compareVersions = require('compare-versions')
const multer = require('koa-multer')
const uploadCrash = multer({dest: 'crash/'})
router.post('/crash', uploadCrash.single('upload_file_minidump'), (ctx, next) => {
console.log(ctx.req.body)
// 存DB
})
4. 软件更新
// package.json
"dependencies": {
"Electron-is-dev": "^1.1.0",
"Electron-squirrel-startup": "^1.0.0",
// ...
},
// index.js
app.on('will-finish-launching', () => {
if(!isDev) {
require('./updater.js')
}
require('./crash-reporter').init()
})
// updater.js
const {autoUpdater, app, dialog} = require('Electron')
if(process.platform == 'darwin') {
autoUpdater.setFeedURL('http://127.0.0.1:33855/darwin?version=' + app.getVersion())
} else {
autoUpdater.setFeedURL('http://127.0.0.1:33855/win32?version=' + app.getVersion())
}
autoUpdater.checkForUpdates() // 定时轮训、服务端推送
autoUpdater.on('update-available', () => {
console.log('update-available')
})
autoUpdater.on('update-downloaded', (e, notes, version) => {
// 提醒用户更新
app.whenReady().then(() => {
let clickId = dialog.showMessageBoxSync({
type: 'info',
title: '升级提示',
message: '已为你升级到最新版,是否立即体验',
buttons: ['马上升级', '手动重启'],
cancelId: 1,
})
if(clickId === 0) {
autoUpdater.quitAndInstall()
app.quit()
}
})
})
autoUpdater.on('error', (err) => {
console.log('error', err)
})
// server
// index.js
const Koa = require('koa')
const app = new Koa()
const Router = require('koa-router')
const serve = require('koa-static-server')
const router = new Router()
const compareVersions = require('compare-versions')
const multer = require('koa-multer')
const uploadCrash = multer({dest: 'crash/'})
router.post('/crash', uploadCrash.single('upload_file_minidump'), (ctx, next) => {
console.log(ctx.req.body)
// 存DB
})
function getNewVersion(version) {
if(!version) return null
let maxVersion = {
name: '1.0.1',
pub_date: '2020-02-01T12:26:53+1:00',
notes: '新增功能AAA',
url: `http://127.0.0.1:33855/public/ZanPandora-1.0.1-mac.zip`
}
if(compareVersions.compare(maxVersion.name , version, '>')) {
return maxVersion
}
return null
}
router.get('/win32/RELEASES', (ctx, next) => {
let newVersion = getNewVersion(ctx.query.version)
if(newVersion) {
ctx.body='BBC6F98A5CD32C675AAB6737A5F67176248B900C ZanPandora-1.0.1-full.nupkg 62177782'
} else {
ctx.status = 204
}
})
router.get('/win32/*.nupkg', (ctx, next) => {
// redirect s3 静态文件服务
ctx.redirect(`/public/${ctx.params[0]}.nupkg`)
})
router.get('/darwin', (ctx, next) => {
// 处理Mac更新, ?version=1.0.0&uid=123
let {version} = ctx.query
let newVersion = getNewVersion(version)
if(newVersion) {
ctx.body = newVersion
} else {
ctx.status = 204
}
})
app.use(serve({rootDir: 'public', rootPath: '/public'}))
app.use(router.routes())
.use(router.allowedMethods())
app.listen(33855)
八、 Electron 应用场景
业界拿 Electron 做了很多东西,比如大家都在用的 VSCode、Atom、一些翻墙软件的实现、很多大厂的面试软件、一些不是非常注重渲染效率的产品、一些集团内部的开发工具等。Electron 大有可为,有很多想象空间去做一些事情。举几个例子
1. 字节跳动
-
Electron做了一个工具,能直接查看线上包的函数耗时,无任何侵入
-
调试工具的合集
-
研发需求管理
-
代码合并工具(MR)。区别于 gitlab 的特性,没有多仓合代码的能力。
比如同时在主工程系修改了7个 pod 、1个主工程的代码,需要分批次提交,不具备同时将8个仓库的代码原子性合入
pod 的 changeLog、版本号等需要设计自动发版的流程
-
把性能调试和效率工具都整合到一起了。
2. 阿里
- 沙盒的查看与操作,比如在 PC 端查看移动沙盒内文件内容、数据库文件的查看与 SQL 执行等
- lint 功能、检测无用方法、在线日志解密查看、数据库文件的解密查看、jspatch 的 mock、
- 针对网络请求和响应的操作,比如自定义请求、延迟、mock response、
- Mock、查看
- 性能测试:cpu、load、fps、启动耗时(机房有高速摄像机解帧,模拟点击 icon、启动页启动、App 首页出现、首页图片出现、App 可以滚动交互了。不需要 hook 去监控耗时)
- pre-main、main、首页打点
- 数据板:埋点体系的数据,看坑位的点击效果
- 应用数据:沙盒、Cookie、数据
- 开发工具:CPU、内存、OOM
- 视觉:移动端元素的参考线
- 网络、开关数据、卡顿、内存泄漏
一些原则,一言以蔽之:该工具只做数据的查看、Mock 等工作,不做线上数据的干扰和生产。
主要实现方式:通过 deviceID、ip 地址等与设备产生连接,将一切可以标准化的流程都抽象、自动化、比如性能调试和效率工具都整合到一起。