一、Electron 进程简介

WX20210523-205346.png

1.1 主进程

Electron 运行 package.json 的 main 脚本的进程被称为主进程

  • 每个应用只有一个主进程;
  • 管理原生GUI,典型的窗口(BrowserWindow、Tray、Dock、Menu);
  • 创建渲染进程;
  • 控制应用生命周期(app);

示例:

  1. const { app, BrowserWindow } = require('electron') // 主进程引入 app、BrowserWindow 模块
  2. let win = new BrowserWindow({ width, height, ...}) // 创建窗口,并设置宽高
  3. win.loadURL(url)、win.loadFile(path) // 加载页面
  4. let notification = new Notification({title, body, actions:[{text, type}]})
  5. 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 模块引入即可。例子:

  1. const { ipcRender } = require('electron') // 渲染进程引入ipcRender
  2. // 通过 ipcRender.invoke 发送一个请求到主进程去响应
  3. ipcRender.invoke(channel, ...arges).then(result => { handleResult })

二、搭建简单样例

简单样例:番茄闹钟

主进程(main.js)

  1. // 主进程
  2. const { app, BrowserWindow, Notification, ipcMain } = require('electron');
  3. let win;
  4. app.whenReady().then(() => {
  5. createMainWindow();
  6. setTimeout(handleIPC, 10000);
  7. });
  8. // 创建窗口
  9. function createMainWindow() {
  10. // 注:窗口需要挂在一个全局的变量,不然可能被垃圾回收掉,即窗口突然间没了
  11. win = new BrowserWindow({
  12. width: 1000,
  13. height: 600,
  14. webPreferences: {
  15. // 设置了Node环境的开启。
  16. // 注: 因为在Electron新版本,为了安全考虑,默认会将Node给禁掉。但由于用的是本地文件,也可以开启Node环境。
  17. nodeIntegration: true,
  18. contextIsolation: false,
  19. },
  20. });
  21. // 加载本地HTML
  22. win.loadFile('./index.html');
  23. // 打开调试终端
  24. win.webContents.openDevTools();
  25. }
  26. function handleIPC() {
  27. /**
  28. * handler最后要return一个结果,故将notification的整个过程用Promise给包起来,让其执行完成之后再返回过去
  29. */
  30. ipcMain.handle('work-notification', async function () {
  31. let res = await new Promise((resolve, reject) => {
  32. // 创建一个通知
  33. let notification = new Notification({
  34. title: '任务结束',
  35. body: '是否开始休息',
  36. actions: [{ text: '开始休息', type: 'button' }],
  37. closeButtonText: '继续工作',
  38. });
  39. notification.show();
  40. // action: 代表点击按钮
  41. notification.on('action', () => {
  42. resolve('rest');
  43. });
  44. // close: 代表关闭事件
  45. notification.on('close', () => {
  46. resolve('work');
  47. });
  48. });
  49. // 将结果返回渲染进程
  50. return res;
  51. });
  52. }

渲染进程(renderer.js)

  1. // 渲染进程
  2. const { ipcRenderer } = require('electron');
  3. const Timer = require('timer.js');
  4. function startWork() {
  5. let workTimer = new Timer({
  6. ontick: (ms) => {
  7. // 秒钟跳动的时候渲染页面
  8. updateTime(ms);
  9. },
  10. onend: () => {
  11. // 时间结束后发出一个通知
  12. notification();
  13. },
  14. });
  15. workTimer.start(10); // 启动定时器,时间为10秒
  16. }
  17. // 渲染时间
  18. function updateTime(ms) {
  19. // ms 表示剩余的毫秒数
  20. let timerContainer = document.getElementById('timer');
  21. let s = (ms / 1000).toFixed(0); // 转化成总秒数
  22. let second = s % 60;
  23. let minute = (s / 60).toFixed(0);
  24. // 利用 padStart 做一个补位
  25. timerContainer.innerHTML = `${minute.toString().padStart(2, 0)}:${second.toString().padStart(2, 0)}`;
  26. }
  27. async function notification() {
  28. let res = await ipcRenderer.invoke('work-notification');
  29. if (res === 'rest') {
  30. setTimeout(() => {
  31. alert('休息');
  32. }, 5 * 1000);
  33. } else if (res === 'work') {
  34. startWork();
  35. }
  36. }
  37. startWork();

package.json

  1. {
  2. "name": "test",
  3. "version": "1.0.0",
  4. "main": "main.js",
  5. "scripts": {
  6. "start": "electron ."
  7. },
  8. "dependencies": {
  9. "electron": "^13.0.0",
  10. "timer.js": "^1.0.4"
  11. }
  12. }

三、进程间通信

进程间通信的目的有以下几类:

  • 通知事件
  • 数据传输
  • 共享数据

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 发送事件。

示例:主进程

  1. const { app, BrowserWindow } = require('electron');
  2. let win;
  3. function createWindow() {
  4. win = new BrowserWindow({
  5. width: 800,
  6. height: 600,
  7. webPreferences: {
  8. nodeIntegration: true,
  9. contextIsolation: false,
  10. },
  11. });
  12. win.loadFile('index.html');
  13. }
  14. app.whenReady().then(() => {
  15. createWindow();
  16. setTimeout(() => {
  17. handleIPC();
  18. }, 2000);
  19. });
  20. function handleIPC() {
  21. win.webContents.send('do-test');
  22. }

渲染进程

  1. const { ipcRenderer } = require('electron');
  2. ipcRenderer.on('do-test', () => {
  3. alert('do some work');
  4. });

3.3 进程通信:从渲染进程到渲染进程

渲染进程与渲染进程间通信:

  • 通知事件:
    • 通过主进程转发(Electron 5之前)
    • ipcRenderer.sendTo(Electron 5之后)
  • 数据共享
    • Web 技术(localStorage、sessionStorage、indexedDB)
    • 使用 remote(不推荐,用的不好话会导致程序卡顿、影响性能)

示例:主进程

  1. const { app, BrowserWindow } = require('electron');
  2. let win1;
  3. let win2;
  4. app.whenReady().then(() => {
  5. win1 = new BrowserWindow({
  6. width: 600,
  7. height: 600,
  8. webPreferences: {
  9. nodeIntegration: true,
  10. contextIsolation: false,
  11. // 在electron 10.0.0之后,remote模块默认关闭
  12. // 必须手动设置webPreferences中的enableRemoteModule为true之后才能使用
  13. enableRemoteModule: true, // 这里是关键设置
  14. },
  15. });
  16. win1.loadFile('./index1.html');
  17. win1.webContents.openDevTools();
  18. win2 = new BrowserWindow({
  19. width: 600,
  20. height: 600,
  21. webPreferences: {
  22. nodeIntegration: true,
  23. contextIsolation: false,
  24. },
  25. });
  26. win2.loadFile('./index2.html');
  27. win2.webContents.openDevTools();
  28. /**
  29. * 在渲染页面里面要去发送消息给另外一个页面,需要知道这个页面对应
  30. * 的webContents.id。为了共享id,将id放置到全局对象global中
  31. */
  32. global.sharedObject = {
  33. win2WebContentsId: win2.webContents.id,
  34. };
  35. });

渲染进程1

  1. const { ipcRenderer, remote } = require('electron');
  2. let sharedObject = remote.getGlobal('sharedObject');
  3. let win2WebContentsId = sharedObject.win2WebContentsId;
  4. // 给渲染进程2发送消息
  5. ipcRenderer.sendTo(win2WebContentsId, 'do-test', 1);

渲染进程2

  1. const { ipcRenderer } = require('electron');
  2. ipcRenderer.on('do-test', (e, a) => {
  3. alert('渲染进程2处理任务:', a);
  4. });

注:
开发IPC通信时遇到的一些坑:

  • 少用 remote 模块,甚至不用;
  • 不要用 sync 模式(写得不好,整个应用会卡死);
  • 在 请求 + 响应的通信模式下,需要在响应的时候设置一个时长限制,当我们的应用响应超时的时候,需要直接 Response 一个异常的超时事件让业务处理,然后去做对应的交互;