一、Electron 进程简介
1.1 主进程
Electron 运行 package.json 的 main 脚本的进程被称为主进程:
- 每个应用只有一个主进程;
- 管理原生GUI,典型的窗口(BrowserWindow、Tray、Dock、Menu);
- 创建渲染进程;
- 控制应用生命周期(app);
示例:
const { app, BrowserWindow } = require('electron') // 主进程引入 app、BrowserWindow 模块
let win = new BrowserWindow({ width, height, ...}) // 创建窗口,并设置宽高
win.loadURL(url)、win.loadFile(path) // 加载页面
let notification = new Notification({title, body, actions:[{text, type}]})
notification.show()
- app,用于控制应用生命周期。举例:
app.on('ready', callback)
用于在应用就绪后开始业务; - BrowserWindow,用于创建和控制窗口;
- Notification,创建 Notification;
ipcMain.handle(channel, handler)
处理渲染进程的 channel 请求,在 handler 中 return 返回结果。
1.2 渲染进程
展示 Web 页面的进程被称为渲染进程:
- 通过 Node.js 、Electron 提供的 API 可以跟系统底层打交道;
- 一个 Electron 应用可以有多个渲染进程;
注:
在浏览器中 Web 页面是运行在沙盒环境中,无法访问操作系统的原生资源。而 Electron 可以让我们使用 Node.js来去访问系统底层。
示例:
引入模块,各进程直接在 electron 模块引入即可。例子:
const { ipcRender } = require('electron') // 渲染进程引入ipcRender
// 通过 ipcRender.invoke 发送一个请求到主进程去响应
ipcRender.invoke(channel, ...arges).then(result => { handleResult })
二、搭建简单样例
简单样例:番茄闹钟
主进程(main.js)
// 主进程
const { app, BrowserWindow, Notification, ipcMain } = require('electron');
let win;
app.whenReady().then(() => {
createMainWindow();
setTimeout(handleIPC, 10000);
});
// 创建窗口
function createMainWindow() {
// 注:窗口需要挂在一个全局的变量,不然可能被垃圾回收掉,即窗口突然间没了
win = new BrowserWindow({
width: 1000,
height: 600,
webPreferences: {
// 设置了Node环境的开启。
// 注: 因为在Electron新版本,为了安全考虑,默认会将Node给禁掉。但由于用的是本地文件,也可以开启Node环境。
nodeIntegration: true,
contextIsolation: false,
},
});
// 加载本地HTML
win.loadFile('./index.html');
// 打开调试终端
win.webContents.openDevTools();
}
function handleIPC() {
/**
* handler最后要return一个结果,故将notification的整个过程用Promise给包起来,让其执行完成之后再返回过去
*/
ipcMain.handle('work-notification', async function () {
let res = await new Promise((resolve, reject) => {
// 创建一个通知
let notification = new Notification({
title: '任务结束',
body: '是否开始休息',
actions: [{ text: '开始休息', type: 'button' }],
closeButtonText: '继续工作',
});
notification.show();
// action: 代表点击按钮
notification.on('action', () => {
resolve('rest');
});
// close: 代表关闭事件
notification.on('close', () => {
resolve('work');
});
});
// 将结果返回渲染进程
return res;
});
}
渲染进程(renderer.js)
// 渲染进程
const { ipcRenderer } = require('electron');
const Timer = require('timer.js');
function startWork() {
let workTimer = new Timer({
ontick: (ms) => {
// 秒钟跳动的时候渲染页面
updateTime(ms);
},
onend: () => {
// 时间结束后发出一个通知
notification();
},
});
workTimer.start(10); // 启动定时器,时间为10秒
}
// 渲染时间
function updateTime(ms) {
// ms 表示剩余的毫秒数
let timerContainer = document.getElementById('timer');
let s = (ms / 1000).toFixed(0); // 转化成总秒数
let second = s % 60;
let minute = (s / 60).toFixed(0);
// 利用 padStart 做一个补位
timerContainer.innerHTML = `${minute.toString().padStart(2, 0)}:${second.toString().padStart(2, 0)}`;
}
async function notification() {
let res = await ipcRenderer.invoke('work-notification');
if (res === 'rest') {
setTimeout(() => {
alert('休息');
}, 5 * 1000);
} else if (res === 'work') {
startWork();
}
}
startWork();
package.json
{
"name": "test",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron ."
},
"dependencies": {
"electron": "^13.0.0",
"timer.js": "^1.0.4"
}
}
三、进程间通信
进程间通信的目的有以下几类:
- 通知事件
- 数据传输
- 共享数据
3.1 进程通信:从渲染进程到主进程
- Callback 写法
- ipcRenderer.send(channel, …args)
- ipcMain.on(channel, handler)
- Promise 写法 (Electron 7.0 之后)
- ipcRenderer.invoke(channel, …args)
- ipcMain.handle(channel, handler)
注:
- ipcMain、ipcRenderer都是 EventEmitter 对象;
3.2 进程通信:从主进程到渲染进程
主进程通知渲染进程:
- ipcRenderer.on(channel, handler)
- webContents.send(channel) // 找到具体的窗体内容
为什么不采用 ipcMain.send ? 因为 Electron 只有一个主进程,多个渲染进程。如果调用 ipcMain.send 的话,主进程到底发给哪个渲染进程呢。因此,在Electron里面的一个做法是需要找到具体的窗体内容(即webContents),然后可以通过webContents.send 发送事件。
示例:主进程
const { app, BrowserWindow } = require('electron');
let win;
function createWindow() {
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
win.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
setTimeout(() => {
handleIPC();
}, 2000);
});
function handleIPC() {
win.webContents.send('do-test');
}
渲染进程
const { ipcRenderer } = require('electron');
ipcRenderer.on('do-test', () => {
alert('do some work');
});
3.3 进程通信:从渲染进程到渲染进程
渲染进程与渲染进程间通信:
- 通知事件:
- 通过主进程转发(Electron 5之前)
- ipcRenderer.sendTo(Electron 5之后)
- 数据共享
- Web 技术(localStorage、sessionStorage、indexedDB)
- 使用 remote(不推荐,用的不好话会导致程序卡顿、影响性能)
示例:主进程
const { app, BrowserWindow } = require('electron');
let win1;
let win2;
app.whenReady().then(() => {
win1 = new BrowserWindow({
width: 600,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
// 在electron 10.0.0之后,remote模块默认关闭
// 必须手动设置webPreferences中的enableRemoteModule为true之后才能使用
enableRemoteModule: true, // 这里是关键设置
},
});
win1.loadFile('./index1.html');
win1.webContents.openDevTools();
win2 = new BrowserWindow({
width: 600,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
});
win2.loadFile('./index2.html');
win2.webContents.openDevTools();
/**
* 在渲染页面里面要去发送消息给另外一个页面,需要知道这个页面对应
* 的webContents.id。为了共享id,将id放置到全局对象global中
*/
global.sharedObject = {
win2WebContentsId: win2.webContents.id,
};
});
渲染进程1
const { ipcRenderer, remote } = require('electron');
let sharedObject = remote.getGlobal('sharedObject');
let win2WebContentsId = sharedObject.win2WebContentsId;
// 给渲染进程2发送消息
ipcRenderer.sendTo(win2WebContentsId, 'do-test', 1);
渲染进程2
const { ipcRenderer } = require('electron');
ipcRenderer.on('do-test', (e, a) => {
alert('渲染进程2处理任务:', a);
});
注:
开发IPC通信时遇到的一些坑:
- 少用 remote 模块,甚至不用;
- 不要用 sync 模式(写得不好,整个应用会卡死);
- 在 请求 + 响应的通信模式下,需要在响应的时候设置一个时长限制,当我们的应用响应超时的时候,需要直接 Response 一个异常的超时事件让业务处理,然后去做对应的交互;