概述
Electron 继承了来自 Chromium 的多进程架构,网页浏览器的基本架构是单个浏览器进程控制不同标签页进程,以及整个应用程序的生命周期。这样可以避免单个浏览器的无响应不会影响到整个浏览器。
Electron 应用的大致工作流程是:启动APP——主进程创建window——win加载页面(渲染进程)
Electron 应用程序的结构非常相似。 作为应用开发者,你将控制两种类型的进程:主进程和渲染器进程。
主进程(Main Process)
是应用的核心,在应用启动时运行,并在整个应用的生命周期中保持活动状态。
- 每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。可以看做是
package.json
中main
属性对应的文件。 - 一个应用只会有一个主进程。
- 只有主进程可以进行 GUI 的 API 操作,所以渲染进程要操作原生API,需要建立和主进程的通信。
主要功能:
- 创建和管理应用程序窗口
- 管理窗口生命周期
- 使用原生API
渲染进程(Renderer Process)
是应用的用户界面部分,渲染线程运行在 Chromium 内核中,可以像 Web 浏览器一样加载和呈现 HTML、CSS 和 JavaScript。
- 一个应用可以有多个渲染进程。
- Windows 中展示的界面通过渲染进程表现。
- 每个窗口都是一个独立的渲染线程。
- 无法直接操作原生API。
**主要功能:**渲染页面
主进程和渲染进程的通信
1. ipcMain
和 ipcRenderer
主进程和渲染进程分别执行不同的职责,它们之间需要通过进程间通信(IPC)进行数据交换。Electron 提供了 ipcMain
和 ipcRenderer
模块来实现。
ipcMain
:从主进程到渲染进程的异步通信。主要作用:
- 在主进程使用时,处理从渲染进程(网页)发送过来的同步或者异步消息。
- 从主进程向渲染进程发送消息。
示例:
监听 channel, 当新消息到达,将通过 listener(event, args…) 调用 listener。
**// channel 就是监听的 key ,是渲染进程发送消息设置的 key // listener 当监听到消息时的处理方法 ipcMain.on(channel, listener)**
添加一次性
listener
函数。 这个listener
只会在channel
下一次收到消息的时候被调用,之后这个监听器会被移除。ipcMain.once(channel, listener)
当一个渲染进程调用
ipcRenderer.invoke(channel, ...args)
时,主进程添加一个处理器处理结果。如果listener
是一个有返回值的方法,那么 Promise 的最终结果就是远程调用的返回值。例如:渲染进程调用一个 invoke ,带返回值。
async () => { const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2) // ... }
主进程设置处理器,并接收方法返回值
ipcMain.handle('my-invokable-ipc', async (event, ...args) => { const result = await somePromise(...args) return result })
ipcRenderer
:从渲染器进程到主进程的异步通信。主要作用:
- 从渲染进程(web页面)发送异步或同步的消息到主进程。
- 接收主进程回复的消息。
示例:
监听或取消监听 channel, 当新消息到达,将通过 listener(event, args…) 调用 listener。
**// channel 就是监听的 key ,是渲染进程发送消息设置的 key // listener 当监听到消息时的处理方法 ipcRenderer.on(channel, listener) // 只监听一次 ipcRenderer.once(channel, listener) // 大概是防止内存泄漏之类的吧 ipcRenderer.off(channel, listener)**
通过
channel
向主进程发送异步消息,可以发送任意参数。**// channel 就是监听的 key ,是渲染进程发送消息设置的 key // listener 当监听到消息时的处理方法 ipcRenderer.send(channel, ...args) // 主进程监听发送的消息,监听到之后进行处理 ipcMain.on(channel, listener)**
通过
channel
向主过程发送消息,并异步等待结果。(有返回值的方法)// 渲染进程 ipcRenderer.invoke('some-name', someArgument).then((result) => { // ... }) // 主进程 ipcMain.handle('some-name', async (event, someArgument) => { const result = await doSomeWork(someArgument) return result })
2. nodeIntegration
和 contextIsolation
从Electron 12开始,为了提高安全性,Electron建议在渲染进程中禁用nodeIntegration
并启用contextIsolation
。这时候实现进程间通信就需要使用preload.js
脚本。
nodeIntegration
: 是否可以使用 Node.js 功能的配置。当nodeIntegration
设置为true
时,渲染进程中可以使用 Node.js 的所有功能。在 Electron 应用中通常禁用nodeIntegration
,减少渲染进程中执行恶意代码的风险。contextIsolation
:启用contextIsolation
后,Electron 将为每个渲染进程创建一个独立的 JavaScript 上下文环境。
代码设置:
const createWindow = () => { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, show:false, // 是否在启动时展示 frame:true, // 是否可以拖动 title:"MyTest", webPreferences: { // 加载渲染进程 preload: path.join(__dirname, 'preload.js'), // 加载进程间通信 contextIsolation:true, // 启用上下文隔离 nodeIntegration:false // 禁用渲染进程调用 Node.js }, }); // and load the index.html of the app. mainWindow.loadFile(path.join(__dirname, 'index.html')); // 监听 ready-to-show ,准备好之后才调用show,解决启动时白屏问题 mainWindow.once('ready-to-show',()=>{ mainWindow.show() }) };
3. preload.js
预加载脚本可以在渲染进程加载前运行,通过 contextBridge
将安全的 API 暴露给渲染进程。
contextBridge
是指在隔离的上下文中创建一个安全的、双向的、同步的桥梁。
API 调用示例:
contextBridge.exposeInMainWorld(apiKey, api)
/** * apiKey string - 将 API 注入到 窗口 的键。 API 将可通过 window[apiKey] 访问。 * api any - 你的 API可以是什么样的以及它是如何工作的相关信息如下。 */ contextBridge.exposeInMainWorld(apiKey, api) // 调用示例 const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electron',{ openSubWindow:()=> ipcRenderer.send('open-sub-window') })
contextBridge.exposeInIsolatedWorld(worldId, apiKey, api)
/** * worldId Integer - 要注入 API 的 world 的 ID。 0 是默认 world,999 的 world 被 Electron 的 contextIsolation 使用。 使用 999 would 为 preload 上下文暴露对象。 我们建议使用 1000+ 来创建隔离的 world。 * apiKey string - 将 API 注入到 window 的键。 API 将可通过 window[apiKey] 访问。 * api any - 你的 API可以是什么样的以及它是如何工作的相关信息如下。 */ contextBridge.exposeInIsolatedWorld(worldId, apiKey, api) // 调用示例: const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInIsolatedWorld( 1004, 'electron', { doThing: () => ipcRenderer.send('do-a-thing') } )
需要调用的地方(如果是第二种方式则是在 id =1004 的world):
// 按钮点击事件 document.getElementById('sub_btn').addEventListener('click',()=>{ // 渲染进程调用 preload.js 暴露的 API window.electron.openSubWindow() })
4. 主进程和渲染进程通信示例
用一个创建子窗口的例子理解一下主进程和渲染进程的通信。
在主窗口页面增加一个按钮,触发按钮后创建一个子窗口。
在主窗口页面增加一个按钮,id = sub_btn,方便设置监听事件查找。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello World!</title> <link rel="stylesheet" href="index.css" /> </head> <body> <h1>💖 Hello World!HIHI</h1> <p>Welcome to your Electron application.</p> <h2>打开一个新窗口</h2> <button id="sub_btn">点击打开一个新窗口</button> </body> <script src="subIndex.js"></script> </html>
上面说到新版 Electron 进程间的通信都需要通过
preload.js
,在主窗口设置preload.js
以及禁用渲染进程调用Node.js
,开启上下文隔离(这个其实默认就是开启的,不设置也行)// node.js 的路径 const path = require('node:path'); const createWindow = () => { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, show:false, frame:true, title:"MyTest", webPreferences: { // 加载 preload.js preload: path.join(__dirname, 'preload.js'), // 设置环境隔离 contextIsolation:true, // 设置禁用渲染进程禁止调用 node.js nodeIntegration:false }, }); // and load the index.html of the app. mainWindow.loadFile(path.join(__dirname, 'index.html')); mainWindow.once('ready-to-show',()=>{ mainWindow.show() }) // Open the DevTools. mainWindow.webContents.openDevTools(); };
__dirname
:指向当前正在执行的脚本的路径。path.join
: 将多个路径联结在一起,创建一个跨平台的路径字符串。
在
preload.js
对渲染进程暴露API// 导包,使用 ipcRender 是 从渲染器进程到主进程的异步通信,是一个 是一个 EventEmitter 的实例 // 参考文档:https://www.electronjs.org/zh/docs/latest/api/ipc-renderer const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electron',{ // 发送 channel 是 open-sub-window 的事件 openSubWindow:()=> ipcRenderer.send('open-sub-window') })
渲染进程调用 API ,执行业务逻辑。(在主进程的 html 文件已经引用了 渲染进程的 script 文件)
使用
window.key.API
方式调用,key 就是在preload.js
里声明的 world 环境,API 就是在preload.js
里声明的对渲染进程暴露的方法。document.getElementById('sub_btn').addEventListener('click',()=>{ window.electron.openSubWindow() })
在主进程监听渲染进程发送的消息,并处理任务
// 主进程监听渲染进程发送的 channel open-sub-window 消息。 ipcMain.on('open-sub-window',(event)=>{ // 处理逻辑 if(!childWindow){ createChildWindow(); }else{ childWindow.show() } }) // 创建子窗口逻辑 function createChildWindow(){ childWindow = new BrowserWindow({ width:400, height:400, parent:BrowserWindow.getAllWindows()[0], modal:true, frame:true, webPreferences:{ preload: path.join(__dirname, 'preload.js'), contextIsolation: true, enableRemoteModule: false, } }); // 加载子窗口页面 childWindow.loadFile(path.join(__dirname, 'subIndex.html')); childWindow.on('closed',function(){ childWindow = null; }) }
完成以上过程,通过渲染进程和主进程间通信完成创建子窗口功能。
这系列文章是记录做游戏启动下载器的整个过程,启动下载器目前已有成品,在调试中,在这个过程中把学习路径都记录下来。由于启动下载器的页面都是H5 写,要先转战学习 vue3 的学习记录,后续的打包、签名内容后续更新。
参考文档
ipcMain: https://www.electronjs.org/zh/docs/latest/api/ipc-main
ipcRenderer: https://www.electronjs.org/zh/docs/latest/api/ipc-renderer
contextBridge: https://www.electronjs.org/zh/docs/latest/api/context-bridge
使用预加载脚本: https://www.electronjs.org/zh/docs/latest/tutorial/tutorial-preload
进程间通信: https://www.electronjs.org/zh/docs/latest/tutorial/ipc