介绍
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux------不需要原生桌面端开发经验。
安装
shell
npm install --save-dev electron
由于众所周知的原因,你可能安装不上依赖,此时可以设置镜像。
首先查看你的镜像源。
shell
nrm ls
确保你使用的镜像源是 npm 源。
打开npm的配置文件:
shell
npm config edit
在里面新增如下配置:
shell
registry=https://registry.npmmirror.com
electron_mirror=https://cdn.npmmirror.com/binaries/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
然后关闭窗口,重新安装依赖。
创建第一个应用程序
在 Electron 中,每个窗口展示一个页面,后者可以来自本地的 HTML,也可以来自远程 URL。 在本例中,您将会装载本地的文件。 在您项目的根目录中创建一个 index.html 文件,并写入下面的内容:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
</body>
</html>
在根目录下面新增 index.js,内容如下:
js
const { app, BrowserWindow } = require('electron')
// createWindow() 函数将您的页面加载到新的 BrowserWindow 实例中
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
// 在应用准备就绪时调用函数
app.whenReady().then(() => {
createWindow()
// 如果没有窗口打开则打开一个窗口 (macOS)
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
// 关闭所有窗口时退出应用 (Windows & Linux)
// 监听 app 模块的 window-all-closed 事件,并调用 app.quit() 来退出您的应用程序。此方法不适用于 macOS
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
- app: 管理应用程序的事件生命周期。
- BrowserWindow: 负责创建和管理应用窗口。
配置 package.json 的文件:
json
{
"scripts": {
"main": "index.js",
"start": "electron ."
}
}
现在运行 npm run start 就可以启动项目了。
使用预加载脚本
Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,您也可以访问 Node.js 内置模块 和所有通过 npm 安装的包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。
BrowserWindow 的预加载脚本运行在具有 HTML DOM 和 Node.js、Electron API 的有限子集访问权限的环境中。
预加载脚本在渲染器加载网页之前注入。 如果你想为渲染器添加需要特殊权限的功能,可以通过 contextBridge 接口定义 全局对象。
在项目的根目录新建一个 preload.js 文件。该脚本通过 versions 这一全局变量,将 Electron 的 process.versions 对象暴露给渲染器。
js
// preload.js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// 除函数之外,我们也可以暴露变量
})
为了将脚本附在渲染进程上,在 BrowserWindow 构造器中使用 webPreferences.preload 传入脚本的路径。
js
// index.js
const { app, BrowserWindow } = require('electron')
// 新增下面这行代码
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
// 新增
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
})
在项目的根目录新增一个 renderer.js 文件,内容如下:
js
const information = document.getElementById('info')
information.innerText = `本应用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})`
然后把 renderer.js 脚本插入到 index.html 文件的 body 标签后面。
js
<script src="./renderer.js"></script>
总结一下:我们在 createWindow 的时候把 preload.js 插入到了 html 文件中,通过 contextBridge 设置了全局对象,在渲染进程就能访问到我们设置的特殊全局属性 versions。
在进程之间通信(双向)
单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收。比较简单,请查看官网。
Electron 的主进程和渲染进程有着清楚的分工并且不可互换。 这代表着无论是从渲染进程直接访问 Node.js 接口,亦或者是从主进程访问 HTML 文档对象模型 (DOM),都是不可能的。
解决这一问题的方法是使用进程间通信 (IPC)。可以使用 Electron 的 ipcMain 模块和 ipcRenderer 模块来进行进程间通信。 为了从你的网页向主进程发送消息,你可以使用 ipcMain.handle 设置一个主进程处理程序(handler),然后在预处理脚本中暴露一个被称为 ipcRenderer.invoke 的函数来触发该处理程序(handler)。
我们将向渲染器添加一个叫做 ping() 的全局函数,该函数返回一个字符串 'pong'。
修改 preload.js 文件:
js
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping') // 新增一个全局方法 versions.ping()
// 除函数之外,我们也可以暴露变量
})
在主进程中设置 handle 监听器:
js
// index.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
// 监听渲染进程触发的方法,并返回一个字符串
ipcMain.handle('ping', () => 'pong')
createWindow()
})
在 renderer.js 中新增下面的内容:
js
// renderer.js
const func = async () => {
// 调用 'ping' 方法,并等待返回值
const response = await window.versions.ping()
console.log(response) // 打印 'pong'
}
func()
我们已经知道如何使用进程间的通信,如此一来就能在渲染进程,点击按钮触发主进程的相关操作。根据官网案例,增加案例复杂程度,设计一个可以切换白天/黑暗模式的桌面软件。
主进程到渲染器进程
- 使用 webContents 模块发送消息
js
// index.js 省略部分代码
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// 使用 webContents.send() 来向渲染进程发送消息
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => win.webContents.send('update-counter', 1),
label: 'Increment'
},
{
click: () => win.webContents.send('update-counter', -1),
label: 'Decrement'
}
]
}
])
Menu.setApplicationMenu(menu)
win.loadFile('index.html')
// 打开开发工具
win.webContents.openDevTools()
}
- 通过预加载脚本暴露 ipcRenderer.on
js
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
})
- 在渲染页面监听事件
js
// renderer.js 省略部分代码
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
})
- 可选:返回一个回复
对于从主进程到渲染器进程的 IPC,没有与 ipcRenderer.invoke 等效的 API。 不过,您可以从 ipcRenderer.on 回调中将回复发送回主进程。
js
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
// 给主进程发送一个 counter-value 事件
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
在主进程中,监听 counter-value 事件并适当地处理它们。
js
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
完成官方的案例,实现一个客户端菜单操作。
渲染进程之间进行通信
没有直接的方法可以使用 ipcMain 和 ipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:
- 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
- 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。
第一种方式,相当于把主进程当作中间的桥梁,交互比较复杂。直接上代码 渲染进程之前进行通信演示
第二种方式,通过 MessageChannelMain 建立2个通道,在通过 webContents.postMessage 在主进程把对应的通道发送到渲染进程。preload.js 中我们可以监听 'port' 事件,通过 window.postMessage 将 port 端口发送到同源的页面上。同样的,同源的页面也可以用 window.onmessage 监听发送来的数据。
js
// index.js
// 省略部分代码
// 创建窗口
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true, // Electron 12.0.0 及以上版本默认启用
preload: path.join(__dirname, 'preloadMain.js')
}
})
mainWindow.loadFile('index.html')
mainWindow.webContents.openDevTools()
const secondaryWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preloadSecondary.js')
}
})
secondaryWindow.loadFile('second.html')
secondaryWindow.webContents.openDevTools()
// 建立通道
const { port1, port2 } = new MessageChannelMain()
// webContents准备就绪后,使用postMessage向每个webContents发送一个端口。
mainWindow.once('ready-to-show', () => {
// 通知渲染进程,这里有一个 port 方法,可以与另外一个窗口通信
mainWindow.webContents.postMessage('port', null, [port1])
})
secondaryWindow.once('ready-to-show', () => {
secondaryWindow.webContents.postMessage('port', null, [port2])
})
js
// preload.js
const { ipcRenderer } = require('electron')
const windowLoaded = new Promise((relose)=> {
window.onload = relose
})
ipcRenderer.on('port', async (event) => {
await windowLoaded // 等待页面加载完成
window.postMessage('channel-port', '*', event.ports)
})
html
<!--渲染进程页面-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Main Page</h1>
<script>
window.onmessage = (event) => {
if (event.source === window && event.data === 'channel-port') {
const [port] = event.ports;
port.onmessage = (event) => { // 打印接收到的消息
console.log(event.data);
}
port.postMessage('i am main page'); // 测试发送消息
}
}
</script>
</body>
</html>
完整代码,请查看 渲染进程之间直接通信。
使用 Electron Forge 打包
Electron 的核心模块中没有捆绑任何用于打包或分发文件的工具。 如果您在开发模式下完成了一个 Electron 应用,需要使用额外的工具来打包应用程序 (也称为可分发文件) 并分发给用户 。 可分发文件可以是安装程序 (例如 Windows 上的 MSI) 或者绿色软件 (例如 macOS 上的 .app 文件)。
Electron Forge 是一个处理 Electron 应用程序打包与分发的一体化工具。 在工具底层,它将许多现有的 Electron 工具 (例如 @electron/packager、 @electron/osx-sign、electron-winstaller 等) 组合到一起,因此您不必费心处理不同系统的打包工作。
安装依赖
shell
npm install --save-dev @electron-forge/cli @electron/fuses @electron-forge/plugin-fuses
npx electron-forge import
转换脚本完成后,Forge 会将一些脚本添加到您的 package.json 文件中。
json
{
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make"
}
}
要创建可分发文件,请使用项目中的 make 脚本,该脚本最终运行了 electron-forge make 命令。
shell
npm run make
该 make 命令包含两步:
它将首先运行 electron-forge package ,把您的应用程序 代码与 Electron 二进制包结合起来。 完成打包的代码将会被生成到一个特定的文件夹中。 然后它将使用这个文件夹为每个 maker 配置生成一个可分发文件。 在脚本运行后,您应该看到一个 out 文件夹,其中包括可分发文件与一个包含其源码的文件夹。
最后一步,我们需要对代码进行签名,请查看其他资料。