Electron 框架核心特性

258 阅读14分钟

Electron 框架核心特性

进程结构

Electron 继承了 Chromium 的多进程架构,之所以不采用单进程架构的最大一个原因就是为了规避:一个网站的崩溃或无响应将直接影响整个浏览器这一问题。

1. 多进程可视化模型(来自 Chrome Comic)

单个浏览器进程控制所有的标签页进程以及整个应用程序的生命周期,每个标签页在自己的进程中渲染,即使其中一个标签页崩溃也不会影响到整个应用程序。 image.png

2. Electron 进程模型

每个Electron 应用由主进程和渲染进程组成,主进程 main.js 是应用程序的入口,是唯一的。主进程运行在 Node.js 环境(默认是 CommonJS 模块化标准)中,因此它可以使用所有的 Node.js API。

image.png

主进程
  • 每个 Electron 应用都有一个主进程 main.js。
  • 作用:
    1. 通过 BrowserWindow 模块创建和管理应用程序窗口
    2. 通过 app 模块控制应用程序的生命周期
    3. 操作与用户的作业系统进行交互的原生API
渲染器进程
  • 当主进程通过 BrowserWindow 模块生成一个窗口后,Electron 会为该窗口生成一个单独的渲染器进程。
  • 作用:渲染网页内容
  • 渲染器进程中代码书写标准:
    • 以一个 HTML 文件作为渲染器进程的入口点
    • 通过 CSS 对 UI 添加样式
    • 通过 <Script> 元素添加可执行的 JavaScript 代码

也可以使用 Vue/React 等框架编写代码。由于渲染器进程依赖于 Chromium 渲染网页,因此渲染器进程中没有集成 Node.js 环境,这意味着渲染器进程无法直接访问 Node.js API (可以经过配置后在渲染器进程中集成 Node.js 环境,但这是极其危险的,因此不推荐!!!)。

预加载 Preload 脚本

渲染器进程的用户界面如何安全地与 Node.js 和 Electron 原生桌面功能进行交互,实际上在渲染器进程中是无法直接导入 Node.js 和 Electron 内容脚本的,但是可以通过 Preload 脚本 借助 contextBridge 模块从隔离上下文中创建一个安全的、双向的、同步的桥梁。

  • 在主进程 main.js 中,预加载脚本通过 BrowserWindow 构造方法中的 webPreferences 附加到主进程
  • 预加载脚本与浏览器共享一个全局 Window 对象,且还可以访问 Node.js API,所以预加载脚本可以通过在全局 Window 对象中暴露任意 API 来增强渲染器
  • 预加载脚本默认 contextIsolation(上下文隔离)的,通俗来讲就是如果你在预加载脚本中设置 window.hello = 'wave' 并且启用了上下文隔离,当网站尝试访问 window.hello 对象时将返回 undefined
  • 上下文隔离的存在就是为了渲染器进程更加的安全,如果渲染器进程需要一些具有特权的 API,可以在预加载脚本中使用 contextBridge 模块来安全地实现交互
效率进程

每个Electron应用程序都可以使用主进程生成多个子进程 UtilityProcess API。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。效率进程可用于托管,例如:不受信任的服务, CPU 密集型任务或以前容易崩溃的组件托管在主进程或使用 Node.jschild_process.fork API 生成的进程中。效率进程和 Node 生成的进程之间的主要区别.js child_process模块是实用程序进程可以建立通信通道与使用 MessagePort 的渲染器进程。 当需要从主进程派生一个子进程时,Electron 应用程序可以总是优先使用 效率进程 API 而不是Node.js child_process.fork API。

进程通信(IPC)

在进程模型中主进程负责创建和管理窗口、控制应用程序的生命周期以及操作原生 API,而渲染器进程则负责渲染网页内容,由此可见两个进程模型具有不同的职责,因此进程间通信(IPC)是执行许多常见任务的唯一方法。例如从 UI 调用原生 API 或从原生菜单触发 Web 内容修改。

IPC 通道

Electron 中通过 ipcMainipcRenderer 模块构建 IPC 通道。 通道的构建是任意双向的:

  • 任意: 通道名可以任意取
  • 双向: 两个模块中可以使用相同的通道名

IPC 通道构建模式

渲染器进程 -> 主进程 (单通道)

在 预加载脚本 preload.js 中通过 contextBridge 模块向渲染器进程暴露一个方法 A (由于安全原因并不直接向渲染器进程暴露 ipcRenderer.send API),并在该方法内通过 ipcRenderer.send 发送消息,渲染器进程便可通过调用方法 A 从而间接使用 ipcRenderer.send 发送消息,而在主进程 main.js 中便可以通过 ipcMain.on 接收消息。

方法介绍 (更多方法介绍参考官方文档):

  • ipcRenderer.send( channel, ...args)
    • channel: String 通道名
    • ...args: any[] 任意参数
  • ipcMain.on( channel, listener)
    • channel : String 通道名
    • listener: Function 回调函数
渲染器进程 <-> 主进程 (双通道)

使用方法同上,不过由 ipcRenderer.invokeipcMain.handle 两个API配合完成。

主进程 -> 渲染器进程

主进程将消息发送到渲染器进程需要指定往哪个渲染器进程发送,因此消息需要借助某一个渲染器进程的 Web Contents 实例的 send 方法向该渲染器进程发送消息,然后需要借助 ipcMain API 的 on 方法在应用程序准备好后监听通道 A 的消息,通过预加载脚本 preload.js 暴露出方法 B,在方法 B 中 通过 ipcRenderer API 的 on 方法监听通道 A,在渲染进程中通过预加载脚本暴露的方法 B 即可读取到主进程发送的消息。

渲染器进程 -> 渲染器进程

渲染器进程之间不能之间通过 IPC 通信,但是可以灵活的使用 IPC 通信来间接的达到渲染进程之间的通信:

  • 通过主进程作为桥梁实现渲染进程之间的通信
  • 从主进程将一个 MessagePort 传递到两个渲染器。在初始设置后渲染器之间直接进行通信。详细使用参见官网

注意: 通常的 IPC 方法,例如 send 和 invoke 不能用来传输 MessagePort, 只有 postMessage 方法可以传输 MessagePort。

打包

使用 Electron Forge

Electron 的核心模块中没有捆绑任何用于打包或分发文件的工具。 如果您在开发模式下完成了一个 Electron 应用,需要使用额外的工具来打包应用程序 (也称为可分发文件) 并分发给用户 。 可分发文件可以是安装程序 (例如 Windows 上的 MSI) 或者绿色软件 (例如 macOS 上的 .app 文件)

Electron Forge 是一个处理 Electron 应用程序打包与分发的一体化工具。 在工具底层,它将许多现有的 Electron 工具 (例如 @electron/packager、 @electron/osx-sign、electron-winstaller 等) 组合到一起,因此您不必费心处理不同系统的打包工作。

导入项目到 Forge

将 Electron Forge 的 CLI 工具包安装到项目的 devDependencies 依赖中,然后使用现成的转化脚本将项目导入至 Electron Forge。

    npm install --save-dev @electron-forge/cli
    npx electron-forge import

转换脚本完成后,Forge 会将一些脚本添加到 package.json 文件中。

package.json

    //...
    "scripts": {
        "start": "electron-forge start",
        "package": "electron-forge package",
        "make": "electron-forge make"
    },
    //...

注意到此时在 devDependencies 下安装了更多的软件包以及导出了一个新的配置文件 forge.config.js。可以在预填充的配置中看到多个 makers (生成可分发应用程序包的包),每个目标平台一个。

创建一个可分发版本

要创建可分发文件,请使用项目中的 make 脚本,该脚本最终运行了 electron-forge make 命令。

    npm run make

该 make 命令包含两步:

  • 它将首先运行 electron-forge package ,把您的应用程序 代码与Electron 二进制包结合起来。 完成打包的代码将会被生成到一个特定的文件夹中。
  • 然后它将使用这个文件夹为每个 maker 配置生成一个可分发文件。

在脚本运行后,您应该看到一个 out 文件夹,其中包括可分发文件与一个包含其源码的文件夹。

macOS output example

    out/
    ├── out/make/zip/darwin/x64/my-electron-app-darwin-x64-1.0.0.zip
    ├── ...
    └── out/my-electron-app-darwin-x64/my-electron-app.app/Contents/MacOS/my-electron-app

out/make 文件夹中的应用程序应该可以启动了! 现在,已经成功创建了你的第一个 Electron 程序。

代码签名

为了将桌面应用程序分发给最终用户,我们 强烈建议 您对 Electron 应用进行 代码签名。 代码签名是交付桌面应用程序的重要组成部分,并且它对于应用程序的自动更新功能 (将会在教程最后部分讲解) 来说是必需的。 代码签名是一种可用于证明桌面应用程序是由已知来源创建的安全技术。 Windows 和 macOS 拥有其特定的代码签名系统,这将使用户难以下载或启动未签名的应用程序。 在 macOS 上,代码签名是在应用程序打包时完成的。 而在 Windows 中,则是对可分发文件进行签名操作。 如果您已经拥有适用于 Windows 和 macOS 的代码签名证书,可以在 Forge 配置中设置您的凭据。

欲了解更多代码签名的信息,请参阅Forge文档中的 签署 macOS 应用程序 指南。

使用 electron-builder

electron-builder 是一个完整的解决方案,用于打包和构建适用于 macOS、Windows 和 Linux 的可分发 Electron 应用程序,并开箱即用地支持“自动更新”。

electron-builder 强烈推荐使用 yarn

安装yarn
    npm install -g yarn
安装 electron-builder
    yarn add electron-builder --dev
快速配置
  1. 指定应用程序 package.json 文件中的标准字段 name - 名称、description - 描述 和 version - 作者。
  2. package.json 的顶级配置下添加 build 字段如下:
        "build": {
            "appId": "your.id",
            "mac": {
            "category": "your.app.category.type",
            "target": "dmg"
            }
        }
    

    查看所有配置

  3. 添加图标
  4. 将脚本添加到 package.json:
    
        "scripts": {
            "pack": "electron-builder --dir",
            "dist": "electron-builder"
        }
    
  5. 运行 yarn dist(以可分发格式打包(例如 dmg、windows 安装程序、deb 包))或 yarn run pack(仅生成包目录而不真正打包它。这对于测试目的很有用)。

为了确保您的本机依赖项始终与电子版本匹配,只需将脚本添加"postinstall": "electron-builder install-app-deps"到您的package.json.

  1. 如果您有自己的本机插件,并且它们是应用程序的一部分(而不是作为依赖项),请将 nodeGypRebuild 设置为 true
  2. 对应用程序签名。

更多详细配置参见官网 electron-builder

安全原则

只加载安全的内容

任何不属于你的应用的资源都应该使用像HTTPS这样的安全协议来加载。 换言之, 不要使用不安全的协议 (如 HTTP)。 同理,我们建议使用WSS,避免使用WS,建议使用FTPS ,避免使用FTP,等等诸如此类的协议。

尽量不要为远程内容启用 Node.js 集成

如果攻击者跳过渲染进程并在用户电脑上执行恶意代码,那么这种跨站脚本(XSS) 攻击的危害是非常大的。 跨站脚本攻击很常见,通常情况下,威力仅限于执行代码的网站。 禁用Node.js集成有助于防止XSS攻击升级为“远程代码执行” (RCE) 攻击。

启用上下文隔离

上下文隔离是Electron的一个特性,它允许开发者在预加载脚本里运行代码,里面包含Electron API和专用的JavaScript上下文。 实际上,这意味全局对象如 Array.prototype.push 或 JSON.parse等无法被渲染进程里的运行脚本修改。

Electron使用了和Chromium相同的Content Scripts技术来开启这个行为。

即便使用了 nodeIntegration: false, 要实现真正的强隔离并且防止使用 Node.js 的功能, contextIsolation 也 必须 开启.

启用进程沙盒化

沙盒 是一项 Chromium 功能,它使用操作系统来显著地限制渲染器进程可以访问的内容。 您应该在所有渲染器中启用沙盒。 不建议在一个未启动沙盒的进程(包括主进程)中加载、阅读或处理任何不信任的内容。

不要禁用 webSecurity

禁用 webSecurity 将会禁止同源策略并且将 allowRunningInsecureContent 属性置 true。 换句话说,这将使得来自其他站点的非安全代码被执行。

Content Security Policy(内容安全策略)

CSP允许Electron通过服务端内容对指定页面的资源加载进行约束与控制。 如果你定义example.com这个源,所属这个源的脚本都允许被加载,反之https://evil.attac… 对于提升你的应用安全性,设置CSP是个很方便的办法。

不要设置 allowRunningInsecureContent 为 true

默认情况下,Electron不允许网站在HTTPS中加载或执行非安全源(HTTP) 中的脚本代码、CSS或插件。 将allowRunningInsecureContent属性设为true将禁用这种保护。

当网站的初始内容通过HTTPS加载并尝试在子请求中加载HTTP的资源时,这被称为"混合内容"。

不要开启实验性功能

如名称所示,实验性功能是实验性的,尚未对所有 Chromium 用户启用。 此外,它们对整个 Electron 的影响很可能没有经过测试。

不要使用 enableBlinkFeatures

Blink是Chromium里的渲染引擎名称。 就像experimentalFeatures一样,enableBlinkFeatures属性将使开发者启用被默认禁用的特性

不要在 WebViews 中使用 allowpopups

开启allowpopups属性将使得BrowserWindows可以通过window.open()方法创建。 否则, 标签内不允许创建新窗口。如果你不需要弹窗,最好使用默认值以关闭新BrowserWindows的创建。 以下是最低的权限要求原则:若非必要,不要再网站中创建新窗口。

创建WebView前确认其选项

由于 存在在DOM中,因此即使Node继承被禁用,它也可以通过运行在您的 网站上的脚本创建它们。

Electron 可以让开发者关闭各种控制渲染进程的安全特性。 通常情况下,开发者并不需要关闭他们中的任何一种 - 因此你不应该允许创建不同配置的标签

禁用或限制网页跳转

导航是一种常见的攻击媒介。 如果攻击者可以诱使你的应用导航离开其当前页面,则他们可能会强制你的应用在 Internet 上打开网站。 即使你的webContents被配置为增强安全(如禁用nodeIntegration或启用contextIsolation),让你的应用打开一个任意的网站依旧是非常简单的操作。

一种常见的攻击模式是,攻击者诱导你的应用的用户与此应用进行能够使其导航到攻击者的某个页面的互动。 这通常是通过链接、插件或其他用户生成的内容完成的。

禁用或限制新窗口创建

如果您有已知的窗口组,那么限制您的应用程序创建额外的窗口是一个好主意。

与导航非常相似,创建新 webContents 是 一种常见的攻击方式。 攻击者试图诱使您的应用创建新的窗口、框架、 或其他渲染过程,拥有比以前更多的权限; 或 打开之前无法打开的页面。

如果您除了知道需要创建的窗口之外,还不需要创建 窗口,则禁用创建可以免费为您带来一些额外的 安全性。 对于打开一个 BrowserWindow 并且不需要在运行时打开任意数量的附加 窗口的应用来说,情况通常如此。

不要对不可信的内容使用 shell.openExternal

shell模块的openExternal API允许使用桌面的本地实用程序打开给定的协议URI。例如,在macOS上,此功能与开放终端命令实用程序类似,将基于URI和文件类型关联打开特定的应用程序。

验证所有 IPC 消息的 sender

应始终验证传入的 IPC 消息的 sender 属性,确保 未使用不受信任的渲染器执行动作或向不受信任的渲染器发送信息。

从理论上讲,所有 Web Frame 都可以将 IPC 消息发送到主进程,包括在某些情况下 iframe 和子窗口。 如果您的 IPC 消息通过 event.reply 向发件人返回 用户数据,或者执行了渲染器 无法本机执行的特权操作,则应确保您没有侦听第三方 web frame。