Electron 应用开发 - 基础概念
# 主进程和渲染进程
Electron 继承了 Chromium 的多进程架构,Chromium 始于主进程,从主进程派生出多个渲染进程。
渲染进程可以理解为浏览器窗口,主进程保存对渲染进程的引用,可以根据需要创建或销毁渲染进程。
# 主进程
主进程是 Electron 应用的核心,通常由一个 main.js
定义,可以在 package.json
中指定。
{
"main": "main.js"
}
2
3
main.js
作为程序的入口,负责管理整个应用的生命周期、窗口管理、原生功能调用等。
可以使用 app
模块来管理应用程序的生命周期:
const { app } = require("electron")
app.on("ready", () => {
console.log("App is ready")
})
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit()
}
})
2
3
4
5
6
7
8
9
10
11
# app 常用生命周期
will-finish-launching
- 当 Electron 完成基本的初始化时触发ready
- 当 Electron 完成初始化时触发- 在此事件中,可以创建浏览器窗口、设置菜单、注册全局快捷键等
window-all-closed
- 当所有窗口都关闭时触发- 在 macOS 上,即使所有窗口都关闭,应用程序也不会退出,而是保持活动状态
- 在 Windows 和 Linux 上,所有窗口关闭时,应用程序将退出
before-quit
- 当应用程序开始退出之前触发- 在此事件中,可以取消退出
will-quit
- 当应用程序开始退出时触发quit
- 当应用程序退出时触发- 执行一些清理操作
# 创建窗口
const { BrowserWindow } = require("electron")
const win = new BrowserWindow({ width: 800, height: 600 })
win.loadURL("https://www.cellinlab.com")
win.on("closed", () => {
win = null
})
win.once("ready-to-show", () => {
win.show()
})
2
3
4
5
6
7
8
9
10
11
12
13
BrowserWindow
常用生命周期:
closed
- 当窗口关闭时触发focus
- 当窗口获得焦点时(激活)触发show
- 当窗口显示时触发hide
- 当窗口隐藏时触发maximize
- 当窗口最大化时触发minimize
- 当窗口最小化时触发
# 调用原生功能
主进程中可以调用原生功能,如创建系统托盘、创建菜单、注册全局快捷键等。
const { clipboard, globalShortcut, Menu } = require("electron")
// write text to clipboard
clipboard.writeText("Hello Electron")
clipboard.readText()
// register global shortcut
globalShortcut.register("CommandOrControl+X", () => {
console.log("CommandOrControl+X is pressed")
})
// create menu
const template = [
{
label: "Edit",
submenu: [
{
label: "Copy",
accelerator: "CmdOrCtrl+C",
role: "copy",
},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
role: "paste",
},
],
},
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 渲染进程
渲染进程是 Electron 应用的 UI 线程,每个渲染进程对应一个窗口,通常由 HTML、CSS、JavaScript 构建。
渲染进程和主进程之间通过 IPC 通信,渲染进程通过 remote
模块调用主进程的功能。
# 预加载脚本 preload.js
预加载脚本包含了那些执行于渲染进程上下文中的脚本,且先于网页内容开始加载。在 preload.js
中,不仅可以使用 Node.js 的 API,还可以使用 Electron 渲染进程的 API 和 DOM API。还可以通过 IPC
与主进程通信,实现调用主进程的功能。
preload.js
虽然运行于渲染器的环境中,却因此拥有了更多的权限。
// main.js
const { BrowserWindow } = require("electron")
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "preload.js"),
},
})
2
3
4
5
6
7
8
// preload.js
const fs = require("fs")
window.myAPI = {
fsExists: fs.existsSync,
}
2
3
4
5
6
<script>
window.myAPI.fsExists("path")
</script>
2
3
需要注意的是,从 Electron 12 开始,
contextIsolation
默认为true
,preload.js
中的全局变量不会注入到页面中,需要通过contextBridge
暴露 API。
# 进程间通信
# ipcMain & ipcRenderer
ipcMain
是一个仅在主进程中以异步方式工作的模块,处理与渲染进程的通信。
ipcRenderer
是一个仅在渲染进程中以异步方式工作的模块,处理与主进程的通信。
以上两者都继承自 EventEmitter
,可以通过 on
方法监听事件,通过 send
方法发送事件。
EventEmitter.on("channel", (event, arg) => {})
EventEmitter.send("channel", arg)
2
3
# 渲染进程 -> 主进程
渲染进程通过 ipcRenderer
发送消息给主进程:
ipcRenderer.send(channel, ...args)
发信息
// in renderer process const { ipcRenderer } = require("electron") ipcRenderer.send("message", "Hello Electron")
1
2
3
4接收信息
// in main process const { ipcMain } = require("electron") ipcMain.on("message", (event, arg) => { console.log(arg) // Hello Electron })
1
2
3
4
5
6回复信息
// in main process const { ipcMain } = require("electron") ipcMain.on("message", (event, arg) => { console.log(arg) // Hello Electron event.reply("reply", "Hello Renderer") })
1
2
3
4
5
6
7// in renderer process const { ipcRenderer } = require("electron") ipcRenderer.on("reply", (event, arg) => { console.log(arg) // Hello Renderer })
1
2
3
4
5
6
ipcRenderer.invoke(channel, ...args)
发信息
// in renderer process const { ipcRenderer } = require("electron") ipcRenderer.invoke("message", "Hello Electron").then((result) => { console.log(result) // Hello Renderer })
1
2
3
4
5
6接收信息并回复
// in main process const { ipcMain } = require("electron") ipcMain.handle("message", async (event, arg) => { console.log(arg) // Hello Electron return "Hello Renderer" })
1
2
3
4
5
6
7
ipcRenderer.sendSync(channel, ...args)
发信息
// in renderer process const { ipcRenderer } = require("electron") const result = ipcRenderer.sendSync("message", "Hello Electron") console.log(result) // Hello Renderer
1
2
3
4
5接收信息
// in main process const { ipcMain } = require("electron") ipcMain.on("message", (event, arg) => { console.log(arg) // Hello Electron event.returnValue = "Hello Renderer" })
1
2
3
4
5
6
7
# 主进程 -> 渲染进程
主进程通过 webContents
主动发送消息给渲染进程:
webContents.send(channel, ...args)
发信息
// in main process const { BrowserWindow } = require("electron") const win = new BrowserWindow() win.webContents.send("message", "Hello Renderer")
1
2
3
4
5
6接收信息
// in renderer process const { ipcRenderer } = require("electron") ipcRenderer.on("message", (event, arg) => { console.log(arg) // Hello Renderer })
1
2
3
4
5
6
# 渲染进程 -> 渲染进程
默认情况下,渲染进程之间是无法直接通信的,但可以通过主进程转发消息实现。
主进程转发
// main.js const { ipcMain } = require("electron") function createWin_1() { let win = new BrowserWindow() win.loadURL("win_1.html") win.on("closed", () => { win = null }) return win } function createWin_2() { let win = new BrowserWindow() win.loadURL("win_2.html") win.on("closed", () => { win = null }) return win } app.on("ready", () => { const win_1 = createWin_1() const win_2 = createWin_2() ipcMain.on("msgFromWin_1", (event, arg) => { win_2.webContents.send("msgToWin_2", arg) }) ipcMain.on("msgFromWin_2", (event, arg) => { win_1.webContents.send("msgToWin_1", arg) }) })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33<!-- win_1.html --> <script> const { ipcRenderer } = require("electron") ipcRenderer.send("msgFromWin_1", "Hello Win_2") ipcRenderer.on("msgToWin_1", (event, arg) => { console.log(arg) }) </script>
1
2
3
4
5
6
7
8
9
10<!-- win_2.html --> <script> const { ipcRenderer } = require("electron") ipcRenderer.send("msgFromWin_2", "Hello Win_1") ipcRenderer.on("msgToWin_2", (event, arg) => { console.log(arg) }) </script>
1
2
3
4
5
6
7
8
9
10使用
MessagePort
MessagePort
是基于 MDN 的 Web API 实现,可以在渲染进程直接创建,也可以在主进程创建。MessagePort
可以通过postMessage
方法发送消息,通过onmessage
监听消息。
// in main process const { BrowserWindow, app, MessageChannelMain } = require("electron") app.whenReady().then(async () => { const win_1 = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, preload: "preload_1.js", }, }) const win_2 = new BrowserWindow({ show: false, webPreferences: { contextIsolation: false, preload: "preload_2.js", }, }) const { port1, port2 } = new MessageChannelMain() win_1.once("ready-to-show", () => { win_1.webContents.postMessage("port", null, [port1]) }) win_2.once("ready-to-show", () => { win_2.webContents.postMessage("port", null, [port2]) }) })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30// in renderer process const { ipcRenderer } = require("electron") ipcRenderer.on("port", (event, ports) => { window.electronMsgPort = ports[0] window.electronMsgPort.onmessage = (event) => { console.log(event.data) } window.electronMsgPort.postMessage("Hello Win_2") })
1
2
3
4
5
6
7
8
9
10
11
12
# 原生能力
# Electron 原生 GUI
BrowserWindow
应用窗口Tray
应用托盘Menu
应用菜单Notification
应用通知dialog
应用对话框
# Electron 操作系统 API
clipboard
剪贴板globalShortcut
全局快捷键screen
屏幕desktopCapturer
音视频捕获powerMonitor
电源管理
# Nodejs API
可以在主进程和渲染进程中使用 Nodejs API,但需要注意的是,渲染进程中的 Nodejs API 是通过 contextBridge
暴露的,需要在 preload.js
中使用。
# Nodejs 调用原生功能
# 使用 C++ 扩展 Nodejs
- 原生模块
当一个 Node.js 的 C++ 模块在 OSX 下编译会得到一个后缀是
_.node
的模块,本质上是_.dylib
的动态链接库;而在 Windows 上本质上是\*.dll
的动态链接库。在 Linux 下则是.so
的动态链接库。 node-gyp
node-gyp
是一个跨平台的命令行工具,用于编译 Node.js 的 C++ 模块。
- 编写 C++ 扩展的方式
- NAN(Native Abstractions for Node.js)
- 提供了一组跨不同版本的 Node.js API 的抽象,帮助开发者编写跨版本兼容的 Node.js C++ 扩展。
- N-API(Node-API)
- Node.js 提供的一组 API,用于编写跨版本兼容的 Node.js C++ 扩展。
- Native Addon
- NAN(Native Abstractions for Node.js)
# 使用 node-ffi-napi 调用动态链接库
node-ffi-napi 是一个使用纯 JavaScript 调用动态链接库的 Node.js 模块。可以用于在不编写任何 C++ 代码的情况下,调用动态链接库。
# 使用 Rust 构建 Node 原生模块
除了使用 C++ 扩展 Nodejs,还可以使用 Rust 构建 Node 原生模块。
# Nodejs 调用 OS 脚本
可以使用 Nodejs 调用系统集成好的一些能力。
# WinRT
WinRT 是 Windows Runtime 的简称,是 Windows 8 引入的一种新的应用程序编程模型,用于开发 Windows 8 应用程序。它提供了一系列的 API,用于访问 Windows 系统的各种功能,如文件系统、网络、设备、传感器等。
在 Windows 中,可以使用 NodeRT 调用 WinRT API。
# AppleScript
AppleScript 是 macOS 系统的脚本语言,可以用于控制 macOS 系统的各种应用程序。
在 macOS 中,可以使用 node-applescript 调用 AppleScript。
# Shell
Shell 提供了与操作系统交互的接口,可以通过 Shell 调用系统命令。
可以通过 Nodejs 的 child_process
模块调用 Shell。
# Electron 跨平台兼容性
# Electron Native API 的平台差异
Electron 的 Native API 在不同平台上的实现是不同的,需要注意的是,不同平台上的实现可能会有差异。
# 操作系统的差异
操作系统本身存在差异,在调用不同操作系统的 API 时,需要注意差异。
const { platform } = require("process")
if (platform === "darwin") {
// macOS
} else if (platform === "win32") {
// Windows
} else if (platform === "linux") {
// Linux
}
2
3
4
5
6
7
8
9
# 用户习惯的差异
不同操作系统的用户习惯是不同的,需要注意根据用户习惯调整应用程序的行为。如:在 macOS 上,关闭窗口并不会退出应用程序,而在 Windows 和 Linux 上,关闭窗口会退出应用程序。
# 文件路径的差异
不同操作系统的文件路径是不同的,需要注意文件路径的差异。
const { app } = require("electron")
app.getPath("appData")
// macOS: /Users/<user>/Library/Application Support
// Windows: C:\Users\<user>\AppData\Roaming
// Linux: /home/<user>/.config
2
3
4
5
6
在进行文件路径拼接时,可以使用 path.join
方法,避免手动拼接路径。
const path = require("path")
path.join("foo", "bar", "baz/asdf", "quux", "..")
// macOS: foo/bar/baz/asdf
// Windows: foo\bar\baz\asdf
// Linux: foo/bar/baz/asdf
2
3
4
5
6