关于前端:Electron实战之进程间通信

3次阅读

共计 6870 个字符,预计需要花费 18 分钟才能阅读完成。

过程间通信(IPC)并非仅限于 Electron,而是源自甚至早于 Unix 诞生的概念。只管“过程间通信”这个术语确实发明于何时并不分明,但将数据传递给另一个程序或过程的理念能够追溯至 1964 年,过后 Douglas McIlroy 在 Unix 的第三版(1973 年)中形容了 Unix 管道的概念。

We should have some ways of coupling programs like garden hose--screw in another segment when it becomes when it becomes necessary to massage data in another way.

例如,咱们能够通过应用管道操作符(|)将一个程序的输入传递到另一个程序。

# 列出当前目录下的所有.ts 文件
ls | grep .ts

在 Unix 零碎中,管道只是 IPC 的一种模式,还有许多其余模式,比方信号、音讯队列、信号量和共享内存。

一、ipcMain 和 ipcRenderer

与 Chromium 雷同,Electron 应用过程间通信(IPC)来在过程之间进行通信,在介绍 Electron 过程间通信前,咱们必须先认识一下 Electron 的 2 个模块。

  • ipcMain 是一个仅在主过程中以异步形式工作的模块,用于与渲染过程替换音讯。
  • ipcRenderer 是一个仅在渲染过程中以异步形式工作的模块,用于与主过程替换音讯。

ipcMain 和 ipcRenderer 是 Electron 中负责通信的两个次要模块。它们继承自 NodeJS 的 EventEmitter 模块。在 EventEmitter 中容许咱们向指定 channel 发送音讯。channel 是一个字符串,在 Electron 中 ipcMain 和 ipcRenderer 应用它来收回和接管事件 / 数据。

// 承受音讯
// EventEmitter: ipcMain / ipcRenderer
EventEmitter.on("string", function callback(event, messsage) {});


// 发送音讯
// EventEmitter: win.webContents / ipcRenderer
EventEmitter.send("string", "mydata");

二、渲染过程 -> 主过程

大多数状况下的通信都是从渲染过程到主过程,渲染过程依赖 ipcRenderer 模块给主过程发送音讯,官网提供了三个办法:

  • ipcRenderer.send(channel, …args)
  • ipcRenderer.invoke(channel, …args)
  • ipcRenderer.sendSync(channel, …args)

channel 示意的就是事件名 (音讯名称),args 是参数。须要留神的是参数将应用结构化克隆算法进行序列化,就像浏览器的 window.postMessage 一样,因而不会蕴含原型链。发送函数、Promise、Symbol、WeakMap 或 WeakSet 将会抛出异样。

2.1 ipcRenderer.send

渲染过程通过 ipcRenderer.send 发送音讯:

// render.js
import {ipcRenderer} from 'electron';


function sendMessageToMain() {ipcRenderer.send('my_channel', 'my_data');
}

主过程通过 ipcMain.on 来接管音讯:

// main.js
import {ipcMain} from 'electron';


ipcMain.on('my_channel', (event, message) => {console.log(`receive message from render: ${message}`) 
})

请留神,如果应用 send 来发送数据,如果你的主过程须要回复音讯,那么须要应用 event.replay 来进行回复:

// main.js
import {ipcMain} from 'electron';


ipcMain.on('my_channel', (event, message) => {console.log(`receive message from render: ${message}`)
  event.reply('reply', 'main_data')
})

同时,渲染过程须要进行额定的监听:

// renderer.js
ipcRenderer.on('reply', (event, message) => {console.log('replyMessage', message);
})

2.2  ipcRenderer.invoke

渲染过程通过 ipcRenderer.invoke 发送音讯:

// render.js
import {ipcRenderer} from 'electron';


async function invokeMessageToMain() {const replyMessage = await ipcRenderer.invoke('my_channel', 'my_data');
  console.log('replyMessage', replyMessage);
}

主过程通过 ipcMain.handle 来接管音讯:

// main.js
import {ipcMain} from 'electron';
ipcMain.handle('my_channel', async (event, message) => {console.log(`receive message from render: ${message}`);
  return 'replay';
});

留神,渲染过程通过 ipcRenderer.invoke 发送音讯后,invoke 的返回值是一个 Promise<pending>。主过程回复音讯须要通过 return 的形式进行回复,而 ipcRenderer 只须要等到 Promise resolve 即可获取到返回的值。

 

2.3 ipcRender.sendSync

渲染过程通过 ipcRender.sendSync 来发送音讯:

// render.js
import {ipcRenderer} from 'electron';


async function sendSyncMessageToMain() {const replyMessage = await ipcRenderer.sendSync('my_channel', 'my_data');
  console.log('replyMessage', replyMessage);
}

主过程通过 ipcMain.on 来接管音讯:

// main.js
import {ipcMain} from 'electron';
ipcMain.on('my_channel', async (event, message) => {console.log(`receive message from render: ${message}`);
  event.returnValue = 'replay';
});

留神,渲染过程通过 ipcRenderer.sendSync 发送音讯后,主过程回复音讯须要通过 e.returnValue 的形式进行回复,如果 event.returnValue 不为 undefined 的话,渲染过程会期待 sendSync 的返回值才执行前面的代码。

2.4 小结

下面咱们介绍了从渲染过程到主过程的几个通信办法,总结如下。

  • ipcRenderer.send:这个办法是异步的,用于从渲染过程向主过程发送音讯。它发送音讯后不会期待主过程的响应,而是立刻返回,适宜在不须要期待主过程响应的状况下发送音讯。
  • ipcRenderer.sendSync:与 ipcRenderer.send 不同,这个办法是同步的,也是用于从渲染过程向主过程发送音讯,然而它会期待主过程返回响应。它会阻塞以后过程,直到收到主过程的返回值或者超时。
  • ipcRenderer.invoke:这个办法也是用于从渲染过程向主过程发送音讯,然而它是一个异步的办法,能够不便地在渲染过程中期待主过程返回 Promise 后果。绝对于 send 和 sendSync,它更适宜解决异步操作,例如主过程返回 Promise 的状况。

三、主过程 -> 渲染过程

主过程向渲染过程发送音讯一种形式是当渲染过程通过 ipcRenderer.send、ipcRenderer.sendSync、ipcRenderer.invoke 向主过程发送音讯时,主过程通过 event.replay、event.returnValue、return … 的形式进行发送。这种形式是被动的,须要期待渲染过程先建设音讯推送机制,主过程能力进行回复。

其实除了下面说的几种被动接管音讯的模式进行推送外,还能够通过 webContents 模块进行音讯通信。

3.1 ipcMain 和 webContents

主过程应用 ipcMain 模块来监听来自渲染过程的事件,通过 event.sender.send() 办法向渲染过程发送音讯。

// 主过程
import {ipcMain, BrowserWindow} from 'electron';


ipcMain.on('messageFromMain', (event, arg) => {event.sender.send('messageToRenderer', 'Hello from Main!');
});

3.2 BrowserWindow.webContents.send

BrowserWindow.webContents.send 能够在主过程中间接应用 BrowserWindow 对象的 webContents.send() 办法向渲染过程发送音讯。

// 主过程
import {BrowserWindow} from 'electron';


const mainWindow = new BrowserWindow();
mainWindow.loadFile('index.html');


// 在某个事件或条件下发送音讯
mainWindow.webContents.send('messageToRenderer', 'Hello from Main!');

3.3 小结

不论是通过 event.sender.send() 还是 BrowserWindow.webContents.send 的形式,如果你只是单窗口的数据通信,那么实质上是没什么差别的。然而如果你想要发送一些数据到特定的窗口,那么你能够间接应用 BrowserWindow.webContents.send 这种形式。

四、渲染过程 -> 渲染过程

默认状况下,渲染过程和渲染过程之间是无奈间接进行通信的。

尽管说无奈间接通信,然而还是有一些“曲线救国”的形式。

4.1 利用主过程作为中间人

首先,须要在主过程注册一个事件监听程序,监听来自渲染过程的事件:

// main.js


// window 1
function createWindow1 () {window1 = new BrowserWindow({width: 800,height: 600})
  window1.loadURL('window1.html')
  window1.on('closed', function () {window1 = null})
  return window1
}


// window 2
function createWindow2 () {window2 = new BrowserWindow({width: 800, height: 600})
  window2.loadURL('window2.html')
  window2.on('closed', function () {window2 = null})
  return window2
}


app.on('ready', () => {createWindow1();
  createWindow2();
  ipcMain.on('win1-msg', (event, arg) => {
    // 这条音讯来自 window 1
    console.log("name inside main process is:", arg); 
    // 发送给 window 2 的音讯.
    window2.webContents.send('forWin2', arg);
  });
})

而后,在 window2 窗口建设一个监听事件:

ipcRenderer.on('forWin2', function (event, arg){console.log(arg);
});

这样,window1 发送的 win1-msg 事件,就能够传输到 window2:

ipcRenderer.send('win1-msg', 'msg from win1');

4.2 应用 MessagePort

下面的传输方式尽管能够实现渲染过程之间的通信,然而十分依赖主过程,写起来也比拟麻烦,那有什么不依赖于主过程的形式嘛?那当然也是有的,那就是 MessagePort。

MessagePort 并不是 Electron 提供的能力,而是基于 MDN 的 Web 规范 API,这意味着它能够在渲染过程间接创立。同时 Electron 提供了 node.js 侧的实现,所以它也能在主过程创立。

接下来,咱们将通过一个示例来形容如何通过 MessagePort 来实现渲染过程之间的通信。

4.2.1 主过程中创立 MessagePort

import {BrowserWindow, app, MessageChannelMain} from 'electron';


app.whenReady().then(async () => {
  // 创立窗口
  const mainWindow = new BrowserWindow({
    show: false,
    webPreferences: {
      contextIsolation: false,
      preload: 'preloadMain.js'
    }
  })


  const secondaryWindow = new BrowserWindow({
    show: false,
    webPreferences: {
      contextIsolation: false,
      preload: 'preloadSecondary.js'
    }
  })


  // 建设通道
  const {port1, port2} = new MessageChannelMain()


  // webContents 准备就绪后,应用 postMessage 向每个 webContents 发送一个端口。mainWindow.once('ready-to-show', () => {mainWindow.webContents.postMessage('port', null, [port1])
  })


  secondaryWindow.once('ready-to-show', () => {secondaryWindow.webContents.postMessage('port', null, [port2])
  })
})

实例化 MessageChannel 类之后,就产生了两个 port:port1 和 port2。接下来只有让 渲染过程 1 拿到 port1、渲染过程 2 拿到 port2,那么当初这两个过程就能够通过 port.onmessage 和 port.postMessage 来收发彼此间的音讯了。如下:

// mainWindow
port1.onmessage = (event) => {console.log('received result:', event.data)
};
port1.postMessage('我是渲染过程一发送的音讯');


// secondaryWindow
port2.onmessage = (event) => {console.log('received result:', event.data)
};
port2.postMessage('我是渲染过程二发送的音讯');

4.2.2 渲染过程中获取 port

有了下面的常识,咱们最重要的工作就是须要获取主过程中创立的 port 对象,要做的是在你的预加载脚本(preload.js)中通过 IPC 接管 port,并设置相应的监听器。

// preloadMain.js
// preloadSecondary.js
const {ipcRenderer} = require('electron')


ipcRenderer.on('port', e => {
  // 接管到端口,使其全局可用。window.electronMessagePort = e.ports[0]


  window.electronMessagePort.onmessage = messageEvent => {// 解决音讯}
})

4.3 音讯通信

通过下面的一些操作后,就能够在应用程序的任何中央调用 postMessage 办法向另一个渲染过程发送音讯。

// mainWindow renderer.js
// 在 renderer 的任何中央都能够调用 postMessage 向另一个过程发送音讯
window.electronMessagePort.postMessage('ping')
正文完
 0