由于作者在实现 cec-client-server 的时候,并不知道 json-rpc 2.0 的这个协议。这个协议规范了 RPC 过程中的实施细则,所以,基于 json-rpc 2.0,重写了 cec-client-server,新的代码库见于:github.com/cross-end-c…
简介
cec-client-server 是一个用于实现“远程过程调用”的库,使用 TS 实现。
所谓的“远程过程调用”,简称 RPC,就是要像调用本地的函数一样去调“远程函数”。通俗的说:A、B 两个服务分别部署在不同的地方,A 可以像调用方法本地的函数一样去调用 B 声明好的函数。
cec-client-server 使用 调用/订阅 的方式来实现 electron 进程间的通讯,其有以下优点:
- 全面:可实现 electron:
主进程 <—> 渲染进程、渲染进程 <—> 渲染进程的通讯 - 实用:比
ipcRenderer.invoke/ipcMain.handle的方式更强大 - 安全:remote 模块虽然功能强大,但是安全、性能是令人担忧的,cec-client-server 是基于
ipcMain.on/ipcRenderer.send,安全有保证。要注意的是:这里仅比较两者在通讯方面的能力,cec-client-server 不具备代替 remote 的能力
实现
新建一个 electron 项目,目录结构:
.
├── src
│ ├── main.ts
│ ├── preload.ts
│ └── renderer.ts
├── index.html
└── package.json
electron 的通讯模式是一对多的一个模型,即:当存在多窗口时,一个主程序需要和多个窗口(渲染进程)通讯。但是使用 cec-client-server 时,每一对 CecServer 和 CecClient 都需要一个独立的信道。于是有了以下代码:
main.ts 主要逻辑为:
1、创建一个新的窗口实例;
2、初始化 CecServer 实例;
如下:
import { join } from 'path';
import { app, BrowserWindow, ipcMain } from 'electron';
import { CecServer, MsgHandler } from 'cec-client-server';
const createWindow = () => {
const mainWindow = new BrowserWindow({ webPreferences: { preload: join(__dirname, 'preload.js') }});
mainWindow.loadFile(join(__dirname, '../index.html'));
// 初始化 CecServer 实例,每一个 BrowserWindow 都需要一个自有的 CecServer 实例
initCecServer(mainWindow);
};
app.whenReady().then(() => {
createWindow();
});
function initCecServer({ webContents }: BrowserWindow) {
// 发送/接收的消息的 channel 名称,需要保持唯一性,所以加上了 webContents.id
// 每一对 CecServer 和 CecClient 都需要一个独立的信道,所以新创建一个 window 时,需要新建一个 CecServer, 并使用一个唯一的 channel name 来通讯
const sendMessageToken = 'cec-channel:send-message-' + webContents.id;
const receiveMessageToken = 'cec-channel:receive-messag-' + webContents.id;
// 将两个 channel 的名称的同步给 preload.ts
webContents.send('cec-channel:initial-message', { sendMessageToken, receiveMessageToken });
// 实例化 CecServer: 使用唯一 channel name 建立信道, 实现 cecServer 和 cecClient 的通讯
const msgSender = (value: any) => webContents.send(sendMessageToken, value);
const msgReceiver = (msgHandler: MsgHandler) => ipcMain.on(receiveMessageToken, (_, value) => msgHandler(value));
const cecServer = new CecServer(msgSender, msgReceiver);
// 注册"能力"
cecServer.onCall('xxx', () => { ... })
cecServer.onSubscribe(...)
...
}
preload.ts 主要逻辑:
import { MsgHandler } from 'cec-client-server';
import { contextBridge, ipcRenderer } from 'electron/renderer';
// 通过 contextBridge 暴露事件:electronMesssageAPI.onMessageReady 给 renderer.ts 暴露通讯能力:msgSender, msgReceiver
contextBridge.exposeInMainWorld('electronMesssageAPI', {
onMessageReady: (callback: (arg: any) => void) => {
ipcRenderer.on('cec-channel:initial-message', (_, value) => {
// 每一对 CecServer 和 CecClient 都需要一个独立的信道,所以新建一个 window 时,需要使用一个唯一的 channel name 来建立信道
const { sendMessageToken, receiveMessageToken } = value;
const msgSender = (val: any) => ipcRenderer.send(receiveMessageToken, val);
const msgReceiver = (msgHandler: MsgHandler) => ipcRenderer.on(sendMessageToken, (_, val) => msgHandler(val));
// 将初始化 CecClient 所需的 msgSender, msgReceiver 传递到 renderer.ts 中
callback({ msgSender, msgReceiver });
});
},
});
renderer.ts 主要逻辑:
import { CecClient } from 'cec-client-server';
const electronMesssageAPI = (window as any).electronMesssageAPI;
electronMesssageAPI.onMessageReady(({ msgSender, msgReceiver }: any) => {
// 实例化 CecClient,通过该实例,可以与对应的窗口的 CecServer 进行通信
const cecClient = new CecClient(msgSender, msgReceiver);
// 调用"能力"
cecClient.call('xxx').then((res) => {
...
})
...
});
总结
在 electron 中使用 cec-client-server,除了功能增强外,主进程和渲染进程组件之间的依赖关系也得以优化
cec-client-server 不依赖特定的信道,除了 ipc, scoket 等全双工的通信也是可选项