Electron 学习笔记

518 阅读17分钟

Electron介绍

Electron 是由Github开发,现在由OpenJS基金会维护的一个开源框架,它允许开发者使用web技术构建跨平台桌面应用。

Electron 的核心组成是 Chromium、Node.js 以及内置的 Native API。

  1. Chromium 为 Electron 提供强大的 UI 能力,可以在不考虑兼容性的情况下,利用强大的 Web 生态来开发界面
  2. Node.js 让 Electron 有了底层的操作能力,比如文件的读写、集成 C++ 等等,还可以使用大量的 NPM 包来帮助大家完成项目需求
  3. 内置的 Native API 解决了跨平台的问题,首先它提供了统一的原生界面,比如窗口、托盘,其次是系统能力,比如 Notification,最后是应用的基础能力,比如软件更新,崩溃监控等等

什么时候用Electron

  1. 快速试错
  2. 开发特定领域的软件,比如开发者工具、效率应用等
  3. 同时开发 Web 版和桌面端,比如大象网页版和大象 PC 客户端

Electron谁在用

image.png

Electron 的架构原理

Chromium架构

Chromium 的本质是 Chrome 的开源版,也是一个浏览器,浏览器也是一个桌面应用,它需要去创建窗口,右键菜单,管理浏览器 Tab 页面还有扩展程序等,处理这些事项的进程为主进程,即下图 Browser 部分。而对应每个具体页面的进程,我们称它为渲染进程,即下图 Render 部分。两个进程需要通信的话,就需要跨进程通信,即 IPC。

image.png

这个图中,我们可以看出:

  1. Chromium 是多进程架构,包括 Browser 和 多个 Render
  2. 进程间需要 IPC 通信
  3. Web 关注到的只是很小一部分

Electron 架构

由于 Electron 使用了 Chromium 来展示 Web 页面,所以 Chromium 的多进程架构,也会被使用到 Electron 中,在 Electron 中也分为主进程,渲染进程等。但是跟 Chromium 不一样的有两点,一个是 Electron 在各个进程中暴露了一些 Native API,第二是 Electron 引入了 Node.js。在一个主线程中,同一个时间下只能运行一个事件循环,但是 Node.js 的事件循环是基于 libuv,而 Chromium 的事件循环是基于 message bump,这就是 Electron 原理的重点,即如何整合事件循环,主要的思路有两种:1)将 Chromium 的 message bump 用 libuv 实现一次, 比如 NW,2)将 Node.js 集成到 Chromium,Electron 采用第二种思路实现。

image.png

Electron 安装

新建项目中安装electron

npm install electron --save-dev

安装报以下错误的话,可以尝试使用以下方式:

image.png

export ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/

npm config set registry http://registry.npm.taobao.org/

npm install electron --save-dev

Electron 主进程模块

Electron 运行 package.json 的 main 脚本的进程被称为主进程。 每个应用只有一个主进程,主进程管理原生 GUI,创建渲染进程,控制应用的生命周期。

  • app 用于控制应用程序的生命周期
  • BrowserWindow,用于创建和控制应用窗口 在 Electron 中,只有在 app 模块的 ready 事件触发后才能创建 BrowserWindows 实例。 您可以通过使用 app.whenReady() API 来监听此事件,并在其成功后调用 createWindow() 方法。
// 创建窗口,并设置宽高
let win = new BrowserWindow({ width, height ...})
// 加载页面
win.loadURL(url)
win.loadFile(path)
  • Notification,创建一个可交互的通知
let notification = new Notification({title, body, actions:[{ text, type }]})
notification.show()
  • ipcMain 是跟 ipcRenderer 进行 IPC 通信的。ipcMain.handle(channel, handler), 处理渲染进程的 channel 请求,在 handler 中 return 返回结果
  • webContents 用来加载具体的页面
  • autoUpdater,更新模块
  • globalShortcut,用来设置全局快捷键
  • clipboard,用来读写剪切板
  • crashReporter,用来监控主进程和渲染进程是否有崩溃

Electron 渲染进程

展示 Web 页面的进程称为渲染进程。通过 Node.js、 Electron 提供的 API 可以跟系统底层打交道,一个 Electron 可以有多个渲染进程。

  • ipcRenderer
  • remote,可以调用主进程的模块
  • desktopCapture,用来捕获桌面流

image.png

Electron进程间通信

进程间通信的目的:

  • 通知事件
  • 数据传输
  • 共享数据

可以使用 Electron 的 ipcMain 模块和 ipcRenderer 模块来进行进程间通信。ipcMainipcRenderer都是 EventEmitter 对象。

从渲染进程到主进程通信

  • Callback 写法
// 渲染进程通过 ipcRenderer.send() 向主进程发送事件
ipcRenderer.send(channel, ...args)

// 主进程通过 ipcMain.on() 来接收并且响应事件
ipcMain.on(channel, handler)
  • Promise 写法(Electron7.0 之后,处理请求+响应模式),这种模式下,最好自定义超时限制
// 渲染进程通过 ipcRenderer.invoke() 向主进程发送事件
ipcRenderer.invoke(channel, ...args)

// 主进程通过 ipcMain.handle() 来接收并且响应事件
ipcMain.handle(channel, handler)

主进程通知渲染进程

主进程中使用 webContents 的 send 方法去发送事件给渲染进程

webContents.send(channel)

渲染进程中,使用 ipcRenderer.on(channel, handler) 来响应事件

渲染进程之间的通信(页面间的通信)

  • 通知事件
    • 通过主进程转发(Electron 5 之前)
    • ipcRenderer.sendTo(webContentsId, channel, ...args), webContentsId 为要发送到的窗口Id
    // 进程1向进程2发送事件
    ipcRenderer.sendTo(webContentsId, channel, ...args)
    
    // 进程2接收并响应进程1的事件
    ipcRenderer.on(channel, handle)
    
  • 数据共享
    • web 技术:localStorage、sessionStorage、indexedDB
    • remote,会将数据挂载在一个全局的过程中,如果用不好,会造成程序卡顿,影响性能,所以尽量减少使用

Electron 应用原生能力

image.png

使用 Electron API 创建原生 GUI

  • BrowserWindow 应用窗口
  • Tray 托盘
  • app 设置 dock.badge
  • Menu 菜单
  • dialog 原生弹窗
  • TouchBar 苹果触控栏

使用 Electron API 获得底层能力

  • clipboard 剪切板
  • globalShortcut 全局快捷键
  • desktopCapture捕获桌面
  • shell 打开文件、URL等

webRTC

image.png

如何捕获桌面流/窗口流

  1. 第一步,通过 desktopCapturer.getSources({types:["window", "screen"]}), 提取 chromeMediaSourceId
  • Electron<5.0 是 callback 调用
  • Electron 5.0后是promise,返回的是 chromeMediaSources 列表,包含 id,name,thumbnail,display_id
  1. 第二部,通过调用
navigator.webkitGetUserMedia({
    audio: false,
    video: {
        mandatory: {
            chromeMediaSource: 'desktop',
            `chromeMediaSourceId: chromeMediaSourceId`,
            width,
            height
        }
    }
})

WebRTC NAT穿透:ICE

ICE(Interactive Connectivity Establishment) 交互式连接创建

  • 优先 STUN(Session Traversal Utilities for NAT),NAT会话穿越应用程序
  • 备选 TURN(Traversal Using Relay NAT),中断 NAT 实现的穿透
    • Full Cone NAT - 完全锥形 NAT
    • Restricted Cone NAT - 限制锥形 NAT
    • Port Restricted Cone NAT - 端口限制锥形 NAT
    • Symmetric NAT - 对称 NAT

image.png

robotjs

  • 用于控制鼠标键盘
  • 它是基于C++实现的 Node.js add-on 库
  • 支持 Mac、Windows、Linux

robotjs的安装和基本使用

  • 安装:npm install robotjs
  • 鼠标移动:robot.moveMouse(x,y)
  • 鼠标点击:mouseClick([button], [double])
  • 按键:robot.keyTap(key,[modifier])
  • 详细文档:Robotjs官方文档

robotjs 编译原生模块

  • 手动编译
// 
npm rebuild --runtime=electron --target=<electron版本> --disturl=https://atom.io/download/atom-shell --abi=<对应版本abi>
// 查看 electron 版本
process.versions.electron
// 查看 node 版本,之后再在 abi_crosswalk 文件(https://github.com/mapbox/node-pre-gyp/blob/master/lib/util/abi_crosswalk.json) 中查找 node_abi 的版本
process.versions.node

abi version 查找

或者在 package.json 中添加 rebuild 命令

"rebuild": "npm rebuild --runtime=electron --target=<electron版本> --disturl=https://atom.io/download/atom-shell --abi=<对应版本abi>"
  • electron-rebuild
npm install electron-rebuild --save-dev
npx electron-rebuild

electron 项目中 使用 process.versions.electron 查看的 node 版本,需要跟实际安装的node,也就是使用 node -v 查看的 node 版本一致,否则会有一些莫名其妙的报错。

SDP

SDP(Session Description Protocol) 是一种会话描述协议,用来描述多媒体会话,主要用于协商双方通讯过程,传递基本信息。 SDP 的格式包含多行,每行为<type>=<value>

  • <type>: 字符,代表特定的属性,比如v,代表版本
  • <value>: 结构化文本,格式与属性类型有关,UTF8编码

Electron 项目从 HTML 到安装包

  1. 下载二进制文件
  2. 添加业务代码
  3. 修改文件信息
  4. 制作镜像

image.png

打包工具选择

  • electron-builder
  • electron-forge

image.png

打包准备

  1. 证书
    • Mac: 开发者证书 99$
    • Windows: 在赛门铁克、WoSign上购买证书
  2. 对应系统的机器
  3. 软件所需的图片
    • Mac
      • 软件图标,icns格式。通过 image2icon 或者 iconutil 生成,如果图标设置的不好,或者未按照规定设置,就会设置不好。
      • dmg背景图标
      • 安装包图标 (不一定需要)
    • Windows
      • ico,软件logo
      • installerIcon (不一定需要)
      • uninstallerIcon (不一定需要)

通过 iconutil 生成 icns

image.png

electron-builder的安装 & 使用

// windows打包必须用管理员启动 cmd 安装windows-build-tools,windows必备
npm install --global --production windows-build-tools
npm install electron-builder --save-dev
// 主要是用来抹平 Mac 和 Windows 下设置环境变量的差异
npm install cross-env --save-dev
    cross env npm_config_electron_mirror="https://npm.taobao.org/mirrors/electron/" electron-builder build --mac
    cross env npm_config_electron_mirror="https://npm.taobao.org/mirrors/electron/" electron-builder build --win --ia32

npm remove electron rebuild
package.json 加入 "postinstall": "electron-builder install-app-deps"
// 用于electron Windows 更新的
npm install electron-builder-squirrel-windows

electron-builder的打包配置

  • 配置文件
    • 在 package.json 加入 build 属性 (常用方式)
    • 存放在 electron-builder.yml
  • 配置项
    • 公共配置:appId、productName、copyright、asar、files、directories
    • 各平台配置
  • 详细见文档:electron-builder Common Configuration

image.png

image.png

image.png

打包经验总结&技巧

  • 产品发布时,版本号需要升级,一般遵循 semver 语法,通常可以使用 npm version patch/mirror/major 来管理升级,
  • Windows 下需要证书签名,否则可能被杀毒软件误杀
  • Mac 下如果没有证书签名,无法使用 electron 自动更新
  • Windows 下打包可以写 nsis 逻辑修改安装包
  • 开源软件可以基于 Travis,AppVeyor 持续集成

软件更新

软件更新的难题

  • 体验问题
    • 更新体验
      • 快、影响小
      • 流畅度、用户等待耗时

更新体验常见的解决方案:

  1. 增量更新
  2. 自动更新
  • 权限问题

UAC问题常见解决方案:

  1. Windows计划任务,在安装的时候去提权,然后常驻在系统中,因此更新过程不会被限制,但跟应用程序无法交互
  2. Windows Services,在安装的时候去提权,然后常驻在系统中,因此更新过程不会被限制,可以跟应用程序交互
  3. 不操作管理员权限文件、注册表,这样软件更新就不会受到任何阻拦

手动更新

  • 用户手动下载、安装新包
  • 优点:简单稳定
  • 缺点:过程繁琐、慢、影响使用、更新率低
  • 适合场景:低频更新、用户粘性高、作为各种升级技术失败后的降级方案

image.png

文件覆盖更新

  • 程序自动替换文件更新
  • 优点:下载过程快
  • 缺点:慢、实现比较复杂、稳定性差、写文件失败
  • 适合场景:打补丁

image.png

自动更新

  • 后台下载文件、重启即最新版
  • 优点:稳定、快、打扰少
  • 缺点:实现复杂
  • 适合场景:高频更新软件、体验要求高

image.png

操作系统的应用商店更新

  • 通过各平台应用商店发布
  • 优点:统一、稳定
  • 缺点:受应用商店局限
  • 适合场景:操作系统应用商店上架的软件

软件更新对比

image.png

Electron更新 - web化

  • 将渲染进程(业务)放置在远程 HTTPS
  • 优点:更新快、体验好
  • 缺点:无法离线使用、主进程更新复杂、多版本兼容问题
  • 适用场景:重业务、壳子更新少

Electron更新 - 文件覆盖

  • 与上面说的软件更新的文件覆盖更新的思路是一样的
  • 张鑫旭的更新方法

Electron更新 - 官方自动更新

  • 基于 Squirrel 框架完成自动更新
  • 属于自动更新思路

Electron更新 - Electron-Updater

  • Electron 官方 Updater 的改版,由 electron-electron-builder 提出

  • 优点:

    • 接入简单
    • Windows 支持签名验证
    • 支持进度条
    • 基于 electron-builder 非常容易使用
  • 缺点:

增量更新

  • 增量更新:只更新需要更新的地方
  • 增量包(差分包、补丁包):新旧包的差异
  • 增量技术:
    • bsdiff/bspatch: 适用二进制文件、开源、免费、广泛使用(尤其移动端)
    • Xdelta3: 适用于二进制
    • Courgette: 谷歌提出的方案,是 bsdiff 的优化
    • RTPatch: 商业付费
    • 对比参考:Xdelta3 bsdiff Courgette三种差分算法比较

image.png

灰度发布

  • 客户端无法回滚
  • 按一定规则逐渐放量
    • 用户特征,比如根据地理位置、性别、年龄等
    • 客户端特征,比如Windows系统、Mac系统
    • 随机
    • ...

image.png

Electron官方更新存在的问题

  • 文档不清晰
  • 细节多
  • 包大 & 没有进度条

Mac客户端更新-服务端

  • 有更新,服务端返回
{
    "url": "https://mycompany.example.com/myapp/release/myrelease",
    "name": My Release Name",
    "notes": "this is some release notes innit",
    "pub_date": "2023-09-18T12:29:53+01:00"
}
  • 无更新,服务端返回 -status=204

Mac客户端更新-客户端需求

  • 证书
    • 一般 Developer Id 证书就够用了
    • 本级测试:自建证书
      • 打开"钥匙串" -> 顶部菜单栏"钥匙串访问" -> 证书助理 -> 创建证书
      • 创建证书:填写名称、身份类型:自签名、证书类型:代码签名
      • 信任证书:钥匙串搜索 -> 双击证书 -> 信任 -> 始终信任证书

Mac客户端更新-客户端

const { autoUpdater } = require('electron');
autoUpdater.setFeedUrl(更新服务url)
autoUpdater.checkForUpdate()
autoUpdater.quitAndInstall()
// 监听 ‘update-available’ , 'update-downloaded', 'error'
autoUpdater.on('update-available', () => {})
autoUpdater.on('update-downloaded', () => {
    // 提醒用户更新 do something
})
autoUpdater.on('error', () => {})

Windows客户端更新-服务端

  • 响应 feedURL/RELEASES
  • 有更新返回 RELEASES 文件内容(打包时出现的),如 image.png
  • 无更新返回 204
  • 响应 feedURL/.*nupkg,返回更新安装包

Windows客户端更新-客户端

  • 包准备
    • 安装包不能使用 NSIS,需要使用 Squirrel
    • 更新需要 Squirrel 配套的 nupkg 包
  • 客户端
    • npm install electron-squirrel-startup --save
    • 在程序开始加上 if(require('electron-squirrel-startup')) return

image.png

electron Windows客户端更新的坑

  • 第一次启动时会报错
  • 替换过程如果被中断会报错

Electron质量保障

重点是对崩溃率的监控

image.png

Electron 崩溃监控关键步骤

通常使用 Electron 自带的 CrashReporter 模块监控主进程和渲染进程,主进程和渲染进程都需要引入崩溃监控的SDK才可以监控,崩溃监控是一个独立的进程,应用崩溃后,日志是可以实时上报的。Electron 崩溃是一个 dump 格式日志。通常后端拿到的 dump 日志格式是不可读的,需要结合Electron 的 symbol 来做处理,才能得到真正的日志。 image.png

崩溃监控-经验技巧

  • 渲染进程崩溃后提示用户重新加载
  • 通过 preload 统一初始化崩溃监控
  • 主进程、渲染进程通过 process.crash() 可以模拟崩溃

崩溃治理的难点

  • 定位出错栈困难
  • 调试门槛高
  • 运行环境复杂

image.png

崩溃治理的经验总结

  • 用户操作日志和系统信息非常重要
  • 及时升级 Electron
  • 复现和定位问题比治理重要
  • 社区响应快
  • devtool 在治理内存问题非常有效

image.png

主进程异常监控

process.on('uncaughtException', function(err){
    // 上报异常
    // 异常日志
})

如何使用原生能力

image.png

Node C++ Addons(扩展)介绍

  • C++ 编写的动态链接共享对象,可以被 Node.js require 使用
  • .node 文件本质是动态链接库(Windows 的 *.dll,Mac 的 *.dylib,Linux 的 *.so)
  • 编写 C++ 扩展主流 2种写法
    • NAN(Native Abstractions for Node.js): 一次编写,到处编译 image.png
    • N-API(Node.js 的一部分,独立于 runtime v8): 同一 ABI,无需重新编译,脱离于引擎的
      • 本身是基于 C 的 API
      • C++ 封装 node-addon-api

何时使用 C++ 扩展

  • 使用 C++ 现成库
  • 性能提升,密集型计算场景
  • 代码保护,核心逻辑

编写 N-API 环境配置

  • npm install --global --production windows-build-tools(Windows必备,管理员身份运行)
  • npm install -g node-gyp(node-gyp 是一个创建项目的生成工具,他解决了一些跨平台的问题)

编写 N-API(C++)

  • 初始化
    • npm init -y
    • npm install bindings node-addon-api --save-dev
    • package.json 增加 "gypfile": true
  • 编写 binding.gyp 配置文件 image.png
  • 实现方法
  • 初始化方法
  • 定义模块

Electron 集成 dll (动态链接库)

  • 使用 node-ffi (node 版本< 10, Electron 版本 <= 6)
  • 使用 node-ffi-napi (node 版本 >= 10, Electron 版本 >= 6)
  • npm install ffi or npm install ffi-napi 本质上 node-ffi 是一个 javascript 加载和调用动态库的 Node.js 扩展,它可以让我们不编写任何 C++ 代码情况下创建于本地 dll 库的绑定,同时它还负责了 javascript 和 C 的类型转换。相比于 Node.js 的 addon,node-ffi 主要有以下几个优点:
    • 不需要源代码
    • 不需要每次都重新编译
    • 不需要写 C 的代码,只需要有一定的 C 的了解即可

但对应的,node-ffi 的缺点就是性能会有一定的折损,跟其他的 ffi 一样,调试会非常困难。因为本质上是一个黑盒调用,排查会比较困难一些。

node-ffi 具体用法:

// 引入 ffi-napi
var ffi = require('ffi-napi')
// 通过 ffi.Library 去绑定 libm 动态库
var libm = ffi.Library('libm', {
    'ceil': ['double', ['double']]
})
libm.ceil(1.5) // 2

dll 的使用场景:

  • 使用系统 API 操作或者扩展应用程序
  • 使用第三方 dll,如硬件设备进行通信

AppleScript

tell application "WeChat" to activate end

Electron 如何做端到端的测试

  • 安装驱动:npm install spectron@9 --save-dev --chromedriver_cdnurl=cdn.npm.taobao.org/dist/chrome… (每一个 electron 版本都有一个对应的 spectron 版本,所以要安装相对应的 spectron 版本,spectron 是基于 chromeDriver 的封装)
  • 选择测试框架+工具
    • ava
    • mocha + chai
    • ...
  • 在项目根目录创建 test 目录,编写测试用例
  • 在 package.json 加入 test 命令

自动化测试

image.png

Electron 体验优化

  • 流畅
    • 启动快
    • 交互快
  • Native化

启动时性能优化

  • 优先加载核心模块,其他非核心流程模块动态加载
  • web 性能优化
  • 多进程多线程技术: BrowserWindow、BrowserView、childProcess、WebWorker
  • asar 可以轻微加快 require 的速度,它主要是解决了因为 Windows 下的文件目录过长而导致的require 的性能问题

运行时性能优化

  • 让主进程保持轻量,不要在主进程中去做复杂的 IO 和重要的计算任务,建议使用一个渲染进程处理密集计算
    • 渲染进程跟主进程有 Sync IPC 操作
    • 主进程卡,UI 就会阻塞
  • 不要使用 remote (remote 的本质它是基于 IPC 的一个同步的进程间通信,同步的 IPC 会阻塞 UI 的渲染,而且 remote 的每次 get 和 set 都会触发,一旦写的不好,会特别卡)
  • 使用 requestIdleCallback,可以将一些不紧急或者低优先级的任务通过 requestIdleCallback 去执行,这个方法在浏览器空闲时才会调用函数队列,使用这个方法,我们的任务就会在空闲时间段被执行,从而不影响关键事件
  • 窗口复用,减少窗口创建的过程

Native 化

为什么会出现白屏

页面从 show 到 ready-to-show 的过程,页面还没完全加载好,用户可能会看到一个白屏。 image.png

Electron 白屏基本功
  • 在ready-to-show的时候再显示
  • 设置窗口底色
win = new BrowserWindow({
    width: 1000,
    height: 680,
    webPreferences: {
        nodeIntegration: true,
        contextIsolation: false,
    },
    show: false,
    background: '#2e2c29'
});

win.on('ready-to-show', function(){
    win.show()
})
  • 设置占位图

    • 利用 BrowserView、BrowserWindow、ChildWindow
  • 开机自启动

var AutoLaunch = require('auto-launch');
var DemoAutoLauncher = new AutoLaunch({
    name: 'Demo',
    path: '/Applications/Demo.app',
})
DemoAutoLauncher.enable()

Electron 客户端的安全

RCE (remote command/code execute) - 远程代码执行漏洞

<img onerror = "require('child_process').exec('rce.bat')" />

安全编码实践

  1. 基本 Web 安全措施
  2. 窗体开启安全选项
  3. Node可执行环境
    • 主进程 任何时候
    • 渲染进程 - 本地内容
    • 渲染进程 - 远程内容 preload阶段
    • 渲染进程 - 远程内容 运行阶段
  4. 限制链接跳转
    • https 可信域
    • 应用本地协议
    • file://
  5. 更多请查阅 Electron 安全建议

Electron 缺点

  • 包体积大,Windows > 30Mb / Mac > 50Mb, 主要是因为 electron 要达到跨平台的目的,它将整个 V8 引擎和Chromium 内核都打包进去了

    • 优化策略
      • 减小包体积
        • yarn autoclean -I
        • yarn autoclean -F
  • 源码安全 image.png

  • 没有 LTS (long-term support)

    • 只维护三个大版本
    • 最新版未知问题
    • 充斥着 workaround
  • 其他

    • 相比 Native,启动速度慢
    • 内存占用比 Native 高
    • CPU 占用
    • Mac商店的分发问题?