技术架构
2228字约7分钟
2024-12-03
技术架构
- Chromium: 支持最新特性的浏览器。
- Node.js: 运行 JavaScript 代码的运行时环境。
- Native apis: 提供统一的原生界面能力,如文件系统、网络、进程管理等。
工作流程
- Main Process: 主进程,负责管理应用程序的生命周期、窗口管理、菜单等。
- Renderer Process: 渲染进程,负责渲染页面、处理用户交互等。
- IPC: 各进程间通信。
Main Process (主进程)
每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。
窗口管理
主进程的主要目的是使用 BrowserWindow
模块创建和管理应用程序窗口。
const { BrowserWindow } = require('electron/main')
// 创建窗口
const win = new BrowserWindow({ width: 800, height: 1500 })
// 加载页面
win.loadURL('./index.html')
// 可通过 webContents 对象访问网页内容
const contents = win.webContents
console.log(contents)
注意
注意:渲染器进程也是为 web embeds
而被创建的,例如 BrowserView
模块。 嵌入式网页内容也可访问 webContents
对象。
由于 BrowserWindow
模块是一个 EventEmitter
, 所以您也可以为各种用户事件 ( 例如,最小化 或 最大化您的窗口 ) 添加处理程序。
当一个 BrowserWindow
实例被销毁时,与其相应的渲染器进程也会被终止。
应用程序生命周期
主进程还能通过 Electron 的 app
模块来控制您应用程序的生命周期。
// 在非MacOS平台上,当没有打开任何窗口时退出应用程序。
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
生命周期事件:
ready
:当 Electron 完成初始化并准备好创建浏览器窗口时发出。dom-ready
:一个窗口中的文本加载完成。did-finish-load
:导航完成时触发。window-all-closed
:当所有窗口都被关闭时发出。before-quit
:在关闭窗口之前触发。will-quit
:当应用程序即将退出时发出。quit
:当所有窗口都被关闭时发出。
原生 API
为了使 Electron 的功能不仅仅限于对网页内容的封装,主进程也添加了自定义的 API 来与用户的操作系统进行交互。 Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。
Renderer Process (渲染进程)
每个 Electron 应用都会为每个打开的 BrowserWindow
( 与每个网页嵌入 ) 生成一个单独的渲染器进程。 洽如其名,渲染器负责渲染网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 (至少就目前使用的 Chromium 而言是如此) 。
提示
渲染器无权直接访问 require
或其他 Node.js API
。 为了在渲染器中直接包含 NPM
模块,您必须使用与在 web
开发时相同的打包工具 (例如 webpack
或 parcel
)
注意
为了方便开发,可以用完整的 Node.js
环境生成渲染器进程。 在历史上,这是默认的,但由于安全原因,这一功能已被禁用。
这句话的意思是:
在 Electron 应用的开发过程中,曾经可以使用完整的 Node.js
环境来生成渲染器进程。这样做可能会带来一些开发上的便利,因为开发人员可以在渲染器进程中直接使用 Node.js
的各种功能和 API,例如 require
模块、文件系统操作、网络操作等,这在开发阶段可能会使开发人员更方便地进行调试和开发工作。
然而,由于安全方面的考虑,这种做法现在已经被禁止了。因为渲染器进程通常会处理用户界面和用户输入,如果允许其使用完整的 Node.js
环境,可能会导致一些安全风险。例如,恶意代码可能会利用渲染器进程中的 Node.js
功能来访问用户的文件系统、执行恶意脚本、窃取用户信息等,从而对用户的计算机造成损害。因此,为了保障应用程序的安全性,现在 Electron 应用的渲染器进程不再允许使用完整的 Node.js
环境,以防止潜在的安全漏洞。
这样的限制可以将渲染器进程的功能限制在处理网页内容和用户交互上,遵循网页标准,而将涉及系统操作和敏感信息处理的功能限制在主进程中,通过进程间通信(IPC)来实现渲染器进程和主进程之间的信息交互,从而提高整个应用程序的安全性。
在开发 Electron 应用时,开发人员需要遵循这一限制,不能在渲染器进程中直接使用 Node.js
环境,而是需要使用其他方式(如使用 preload 脚本或进程间通信)来实现所需的功能,同时确保应用程序的安全性。
在 Electron 14 及更高版本中,您可以通过在 package.json
中添加 "contextIsolation": false
来重新启用此功能。
Preload 脚本
如上所说,由于安全方面的考虑,渲染器进程无法直接访问 Node.js
环境。 为了在渲染器进程中使用 Node.js
模块,您可以使用 preload
脚本。
preload
脚本是一个运行在渲染器进程中的 JavaScript 文件,它可以访问 Node.js
环境。 它可以通过在 BrowserWindow
的选项中指定 preload
脚本来加载。
const { BrowserWindow } = require('electron/main')
// 创建窗口
const win = new BrowserWindow({
width: 800,
height: 1500,
webPreferences: {
// 加载 preload 脚本
preload: './preload.js'
}
})
因为预加载脚本与浏览器共享同一个全局 Window
接口,并且可以访问 Node.js API
,所以它通过在全局 window
中暴露任意 API
来增强渲染器,以便你的网页内容使用。
虽然预加载脚本与其所附着的渲染器在共享着一个全局 window
对象,但您并不能从中直接附加任何变动到 window 之上,因为 contextIsolation
[语境隔离] 是默认开启的。
const { contextBridge } = require('electron/main')
// 暴露一个 myAPI 到渲染器进程中的全局变量中
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
// 渲染进程中不能直接更改 IPC 中定义的 window 变量
window.myAPI.desktop = false
console.log(window.myAPI)
// => { desktop: true }
此功能对两个主要目的來說非常有用:
- 通过暴露
ipcRenderer
模块于渲染器中,您可以使用 进程间通讯 ( inter-process communication, IPC ) 来从渲染器触发主进程任务 ( 反之亦然 ) 。 - 如果您正在为远程 URL 上托管的现有 web 应用开发 Electron 封裝,则您可在渲染器的
window
全局变量上添加自定义的属性,好在 web 客户端用上仅适用于桌面应用的设计逻辑 。
效率进程
效率进程就是Electron应用程序主进程用UtilityProcess API
创建的子进程。
UtilityProcess API
为 Electron 应用程序提供了一种创建和管理子进程的机制。这些子进程可以执行一些辅助性的任务,例如执行后台计算、处理文件操作、进行网络请求等,而不影响主进程和渲染进程的正常运行。
使用子进程可以带来一些好处,例如:
- 资源隔离:将不同的任务分配到不同的子进程中,避免一个任务的异常影响到整个应用程序的稳定性。
- 性能提升:可以利用多核处理器的优势,将一些耗时的任务分配到不同的子进程中并行处理,提高应用程序的整体性能。
以下是一个简单的示例代码,展示如何使用 UtilityProcess API
创建一个子进程:
// 主进程
const { app, utilityProcess } = require('electron')
app.whenReady().then(() => {
// 创建一个子进程
const childProcess = utilityProcess.fork('./childProcess.js')
// 监听子进程的消息
childProcess.on('message', (message) => {
console.log('Received message from childProcess:', message)
})
// 向子进程发送消息
childProcess.postMessage('Hello from child process')
})
// 子进程
// 监听主进程的消息
process.parentPort.once('message', (e) => {
console.log('Received message from mainProcess:', e.data);
})
// 子线程发送信息给主线程
const timer = setTimeout(() => {
process.parentPort.postMessage('Hello from main process')
clearTimeout(timer)
}, 2000);
在上述代码中:
utilityProcess.fork('./subProcess.js')
用于创建一个子进程,./subProcess.js 是子进程的入口文件。subProcess.on('message', (message) => {...})
用于监听子进程发送过来的消息。subProcess.postMessage('Hello from main process')
用于向子进程发送消息。
通过这种方式,主进程可以与子进程进行通信,将一些任务分配给子进程执行,从而提高应用程序的性能和稳定性。