进程模型、进程间通信 与Node模块使用

进程模型、进程间通信 与Node模块使用

进程模型

每个Electron的应用程序都有一个主入口文件,它所在的进程被成为主进程(Main Process。而主进程中创建的窗体都有自己运行的进程,称为渲染进程( Renderer Process。每个Electron的应用程序有且仅有一个主进程,但可以有多个渲染进程。简单理解下主进程就相当于浏览器,而渲染进程就相当于在浏览器上打开的一个个网页。

主进程主要工作就是 控制应用程序生命周期 和 管理窗口、菜单、托盘等 ,另外主进程运行在Node.js环境中,所以它可以使用各种Node.js模块,也可以调用操作系统中的各种资源等。渲染进程主要就是用来显示下网页、跑跑前端代码,这里只能用前端的语法规则,没法使用Node.js的语法和模块。

早期版本的Electron渲染进程是可以直接使用Node.js的语法和模块的,现在版本中出于安全考虑所以无法直接使用(虽然可以通过配置解锁,但渲染进程中使用Node.js的一些功能这个需求还是存在的,所以Electron现在提供了预加载(preload)的功能。预加载调用一个JS脚本,它会在网页被加载前运行,它既可以使用Node.js的功能,又可以访问网页上的window对象(默认情况下并不能直接访问,得通过contextBridge模块。所以可以在这里将Node.js的功能传递给window对象,这样渲染进程就可以使用这些功能了。

进程间通讯

上面内容中可以知道默认情况下渲染进程只能使用前端的语法规则,所以它和相当于后台的主进程间只能通过http或是websocket等方式进行通讯,这种方式并不方便,所以Electron还提供了一些别的方式用于处理这方面问题。Electron中使用ipcMainipcRenderer两个模块来处理进程间通讯,这两个是基于Node.js方式的模块,所以根据上面的内容使用上会有些注意点,主要就是怎么用的问题。

不安全方式

上一章内容中有说到早期版本的Electron渲染进程是可以直接使用Node.js的语法和模块的,现在版本中出于安全考虑所以无法直接使用,但可以通过配置解锁。所以我们可以配置下,然后就可以直接在渲染进程中使用Node.js的语法和模块,比如用于进程间通讯的ipc模块。使用上来说这是最简单的方式。

下面是个简单的例子,分别改写main.jsindex.html文件:

const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");

function createWindow() {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true, // 启用node环境
      contextIsolation: false, // 禁用上下文隔离
    },
  });
  mainWindow.loadFile("index.html");
  mainWindow.webContents.openDevTools();

  setInterval(() => {
    // 使用下面方法向mainWindow发送消息,消息事件名称为 main-send ,内容为 hello
    mainWindow.webContents.send("main-send", "hello");
  }, 5000);
}

// 使用ipcMain.on方法监听 renderer-send 事件
ipcMain.on("renderer-send", (event, arg) => {
  console.log(arg);
  // 使用下面方法对产生事件的对象进行应答,应答时事件名为main-reply,内容为pong
  event.reply("main-reply", "pong");
});

app.whenReady().then(() => {
  createWindow();
  app.on("activate", function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on("window-all-closed", function () {
  if (process.platform !== "darwin") app.quit();
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>test</title>
  </head>
  <body>
    <h1 id="txt">Hello World!</h1>
    <script>
      // 渲染进程使用ipcRenderer模块
      const { ipcRenderer } = require("electron");

      // 使用ipcRenderer.send方法发送消息,消息事件名称为 renderer-send ,内容为 ping
      setInterval(() => {
        ipcRenderer.send("renderer-send", "ping");
      }, 3000);

      // 使用ipcRenderer.on方法监听 main-reply 事件
      ipcRenderer.on("main-reply", (event, arg) => {
        console.log(arg);
      });

      // 使用ipcRenderer.on方法监听 main-send 事件
      ipcRenderer.on("main-send", (event, arg) => {
        console.log(arg);
      });
    </script>
  </body>
</html>

上面就是个简单的通讯演示了,通过设置nodeIntegration: truecontextIsolation: false在渲染进程中就可以直接使用Node.js的语法和模块了。ipcMainipcRenderer两个模块分别用于主进程和渲染进程。传递消息时消息都会有个事件名称,两个模块分别用各自的on()方法来监听消息事件。只有ipcRenderer可以主动向ipcMain发送消息, ipcMain只能在监听到来自ipcRenderer的事件后才可以返回消息。

主线程中可以使用BrowserWindow对象的webContents.send()方法主动向该对象渲染进程发送消息,该渲染进程中同样使用ipcRenderer.on()来监听此消息。上面演示中ipcRenderer发送和ipcMain返回消息用的都是异步方法,它们还有同步方法可用,可以参考Electron官方的API文档。

预加载方式

上面的方式Electron现在并不推荐,现在推荐的是用预加载的方式把Node.js的一些内容传递给渲染进程。下面是个简单的例子,现在添加一个preload.js文件,内容如下:

const { contextBridge, ipcRenderer } = require("electron");

// 使用contextBridge.exposeInMainWorld()方法将
// Function、String、Number、Array、Boolean、对象等
// 传递给渲染进程的window对象
contextBridge.exposeInMainWorld("myAPI", () => {
  setInterval(() => {
    ipcRenderer.send("renderer-send", "ping");
  }, 3000);
  ipcRenderer.on("main-reply", (event, arg) => {
    console.log(arg);
  });
  ipcRenderer.on("main-send", (event, arg) => {
    console.log(arg);
  });
});

分别改写main.jsindex.html文件:

// main.js中只需要改写下面内容就行了
const mainWindow = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, "preload.js"), // 使用预加载脚本
  },
});
<!-- index.html中只要改写下面内容就好了 -->
<script>
  window.myAPI();
</script>

上面就是个简单的预加载的使用方式了,可以看到从使用角度来说其实也没太大差别,无非就是使用contextBridge.exposeInMainWorld()方法来传递Node.js的内容给了window对象。

下一页