Electron的工作方式非常簡(jiǎn)單。有兩種不同的進(jìn)程-主進(jìn)程(Main Process)和渲染進(jìn)程(Renderer Process)。始終只保持有一個(gè)主進(jìn)程,這是您Electron應(yīng)用程序的入口。可以有任意數(shù)量的渲染器進(jìn)程,這些進(jìn)程負(fù)責(zé)渲染您的應(yīng)用程序。
這些進(jìn)程間的通信是通過IPC(進(jìn)程間通信)完成的。聽起來可能很復(fù)雜,但這只是異步請(qǐng)求-響應(yīng)模式的一個(gè)好聽的名字。
渲染器與主進(jìn)程之間的通信在后臺(tái)發(fā)生的事情基本上只是事件調(diào)度。例如,假設(shè)您的應(yīng)用程序應(yīng)顯示有關(guān)其運(yùn)行系統(tǒng)的信息。這可以通過一個(gè)簡(jiǎn)單的命令uname -a
來完成,該命令顯示您的內(nèi)核版本。但是您的應(yīng)用程序本身無法此執(zhí)行命令,因此需要主進(jìn)程。在Electron應(yīng)用程序中,您的應(yīng)用程序可以訪問渲染器進(jìn)程(ipcRenderer)。
以下是將要發(fā)生的事情:
您的應(yīng)用程序?qū)⒗?code>ipcRenderer向主進(jìn)程發(fā)送事件。這些事件稱為Electron內(nèi)部的通道。
如果主進(jìn)程正確的注冊(cè)了事件偵聽器(用于偵聽剛剛調(diào)度的事件),則可以為該事件正確的運(yùn)行代碼。
完成所有操作后,主進(jìn)程可以為結(jié)果發(fā)出另一個(gè)事件(在我們的示例中為內(nèi)核版本)。
4. 現(xiàn)在整個(gè)工作流程都以相反的方式進(jìn)行,渲染器流程需要為主流程中分派的事件實(shí)現(xiàn)一個(gè)偵聽器。
- 當(dāng)渲染器進(jìn)程收到包含我們所需信息的適當(dāng)事件時(shí),在界面上顯示該信息。
最終,整個(gè)過程可以看作是一個(gè)簡(jiǎn)單的請(qǐng)求-響應(yīng)模式,有點(diǎn)像HTTP – 只不過是異步的。我們將通過某個(gè)指定的頻道發(fā)起請(qǐng)求,并在某個(gè)指定的頻道上收到對(duì)此的回復(fù)。
多虧了TypeScript,我們可以將整個(gè)邏輯抽象成一個(gè)干凈分離且正確封裝的應(yīng)用程序中,在這個(gè)應(yīng)用程序中,我們?yōu)橹鬟M(jìn)程中的單個(gè)通道定義了單獨(dú)的類,并利用Promise簡(jiǎn)化異步請(qǐng)求。再說一遍,這聽起來比實(shí)際情況復(fù)雜得多!
用TypeScript引導(dǎo)電子應(yīng)用程序
我們需要做的第一件事是用TypeScript引導(dǎo)我們的Electron應(yīng)用程序。我們的package.json
是:
{
"name": "electron-ts",
"version": "1.0.0",
"description": "Yet another Electron application",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"start": "npm run build && electron ./dist/electron/main.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Kevin Hirczy <https://nehalist.io>",
"license": "MIT",
"devDependencies": {
"electron": "^7.1.5",
"typescript": "^3.7.3"
}
}
接下來我們要添加的是Typescript配置,tsconfig.json
:
{
"compilerOptions": {
"target": "es5",
"noImplicitAny": true,
"sourceMap": true,
"moduleResolution": "node",
"outDir": "dist",
"baseUrl": "."
},
"include": [
"src/**/*"
]
}
我們的源文件將位于src
目錄中,所有文件都將構(gòu)建到dist
目錄中。我們將把src
目錄分成兩個(gè)單獨(dú)的目錄,一個(gè)用于Electron,一個(gè)用于我們的應(yīng)用程序。整個(gè)目錄結(jié)構(gòu)如下所示:
src/
app/
electron/
shared/
index.html
package.json
tsconfig.json
我們的index.html將被Electron加載,非常簡(jiǎn)單(目前):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
Hello there!
</body>
</html>
我們要實(shí)施的第一個(gè)文件是Electron的主文件。 該文件將實(shí)現(xiàn)Main類,該類負(fù)責(zé)初始化我們的Electron應(yīng)用程序:
// src/electron/main.ts
import {app, BrowserWindow, ipcMain} from 'electron';
class Main {
private mainWindow: BrowserWindow;
public init() {
app.on('ready', this.createWindow);
app.on('window-all-closed', this.onWindowAllClosed);
app.on('activate', this.onActivate);
}
private onWindowAllClosed() {
if (process.platform !== 'darwin') {
app.quit();
}
}
private onActivate() {
if (!this.mainWindow) {
this.createWindow();
}
}
private createWindow() {
this.mainWindow = new BrowserWindow({
height: 600,
width: 800,
title: `Yet another Electron Application`,
webPreferences: {
nodeIntegration: true // 使在index.html中可以使用`require`
}
});
this.mainWindow.webContents.openDevTools();
this.mainWindow.loadFile('../../index.html');
}
}
// 走起!
(new Main()).init();
運(yùn)行npm start
現(xiàn)在應(yīng)該啟動(dòng)Electron應(yīng)用程序并顯示index.html
:
接下來我們要實(shí)現(xiàn)的是如何處理IPC通道。
通道處理
基于SoC概念,我們將為每個(gè)通道實(shí)現(xiàn)一個(gè)類。這些類將負(fù)責(zé)傳入的請(qǐng)求。在上面的例子中,我們有一個(gè)SystemInfoChannel
負(fù)責(zé)收集系統(tǒng)數(shù)據(jù)。如果你想使用某些工具,例如使用Vagrant控制虛擬機(jī),就創(chuàng)建一個(gè)VagrantChannel
等。
每個(gè)通道都將有一個(gè)名稱和一個(gè)處理傳入請(qǐng)求的方法,因此我們?yōu)榇藙?chuàng)建一個(gè)接口:
// src/electron/IPC/IpcChannelInterface.ts
import {IpcMainEvent} from 'electron';
export interface IpcChannelInterface {
getName(): string;
handle(event: IpcMainEvent, request: any): void;
}
有個(gè)棘手的事情,在很多情況下,any
類型意味著設(shè)計(jì)上的存在缺陷,我們不想擁有這種缺陷。因此,讓我們花點(diǎn)時(shí)間考慮一下request
的類型。
請(qǐng)求是從渲染進(jìn)程中發(fā)出的。發(fā)送請(qǐng)求時(shí)可能需要知道兩件事:
- 我們的頻道可以接受參數(shù)
- 該使用哪個(gè)通道來響應(yīng)
兩者都是可選的,我們可以創(chuàng)建一個(gè)發(fā)送請(qǐng)求的接口。此接口將在Electron和我們的應(yīng)用程序之間共享:
export interface IpcRequest {
responseChannel?: string;
params?: string[];
}
現(xiàn)在我們可以回到IpcChannelInterface
,為我們的request
添加適當(dāng)?shù)念愋停?/p>
handle(event: IpcMainEvent, request: IpcRequest): void;
接下來我們需要注意的是如何將頻道添加到我們的主進(jìn)程中。最簡(jiǎn)單的方法是將通道數(shù)組添加到Main類的init方法中。這些頻道將由我們的ipcMain進(jìn)程注冊(cè):
public init(ipcChannels: IpcChannelInterface[]) {
app.on('ready', this.createWindow);
app.on('window-all-closed', this.onWindowAllClosed);
app.on('activate', this.onActivate);
this.registerIpcChannels(ipcChannels);
}
registerIpcChannels方法只有一行:
private registerIpcChannels(ipcChannels: IpcChannelInterface[]) {
ipcChannels.forEach(channel => ipcMain.on(channel.getName(), (event, request) => channel.handle(event, request)));
}
這里發(fā)生的事情是,傳遞到init
方法的通道將注冊(cè)到主進(jìn)程,并由它們的響應(yīng)通道類處理。
為了更容易理解,讓我們從上面的示例中快速實(shí)現(xiàn)系統(tǒng)信息的類:
import {IpcChannelInterface} from "./IpcChannelInterface";
import {IpcMainEvent} from 'electron';
import {IpcRequest} from "../../shared/IpcRequest";
import {execSync} from "child_process";
export class SystemInfoChannel implements IpcChannelInterface {
getName(): string {
return 'system-info';
}
handle(event: IpcMainEvent, request: IpcRequest): void {
if (!request.responseChannel) {
request.responseChannel = `${this.getName()}_response`;
}
event.sender.send(request.responseChannel, { kernel: execSync('uname -a').toString() });
}
}
通過將此類的實(shí)例添加到我們的Main
類的init
調(diào)用中,我們現(xiàn)在注冊(cè)了我們的第一個(gè)通道處理程序:
(new Main()).init([
new SystemInfoChannel()
]);
現(xiàn)在,每次在system-info
通道上發(fā)生請(qǐng)求時(shí),SystemInfoChannel
都會(huì)處理該請(qǐng)求,并通過在內(nèi)核上響應(yīng)(在responseChannel
上)來正確處理該請(qǐng)求。
到目前為止,我們已經(jīng)完成了以下工作:
到目前為止看起來不錯(cuò),但我們?nèi)匀蝗鄙賾?yīng)用程序?qū)嶋H完成工作的部分,例如發(fā)送收集內(nèi)核版本的請(qǐng)求。
從我們的應(yīng)用程序發(fā)送請(qǐng)求
為了利用干凈的主流程的IPC架構(gòu),我們需要在應(yīng)用程序中實(shí)現(xiàn)一些邏輯。 為了簡(jiǎn)單起見,我們的用戶界面將僅具有一個(gè)用于向主進(jìn)程發(fā)送請(qǐng)求的按鈕,該按鈕將返回我們的內(nèi)核版本。
我們所有與IPC相關(guān)的邏輯都將放在一個(gè)簡(jiǎn)單的服務(wù)– IpcService
類中:
// src/app/IpcService.ts
export class IpcService {
}
使用此類時(shí),我們要做的第一件事是確保我們可以訪問ipcRenderer
。
如果您想知道為什么我們需要這樣做,那是因?yàn)椴贿@樣做,直接打開index.html
文件時(shí),沒有可用的ipcRenderer
。
讓我們添加一個(gè)可以正確初始化ipcRenderer
的方法:
private ipcRenderer?: IpcRenderer;
private initializeIpcRenderer() {
if (!window || !window.process || !window.require) {
throw new Error(`Unable to require renderer process`);
}
this.ipcRenderer = window.require('electron').ipcRenderer;
}
當(dāng)我們?cè)噲D從主流程請(qǐng)求某些內(nèi)容時(shí),將調(diào)用此方法-這是我們需要實(shí)現(xiàn)的下一個(gè)方法:
// 如果ipcRenderer不可用,請(qǐng)嘗試將其初始化
if (!this.ipcRenderer) {
this.initializeIpcRenderer();
}
// 如果沒有responseChannel讓我們自動(dòng)生成它
if (!request.responseChannel) {
request.responseChannel = `${channel}_response_${new Date().getTime()}`
}
const ipcRenderer = this.ipcRenderer;
ipcRenderer.send(channel, request);
// 該方法返回一個(gè)`promise`,當(dāng)響應(yīng)到達(dá)時(shí)將調(diào)用`resolve`。
return new Promise(resolve => {
ipcRenderer.once(request.responseChannel, (event, response) => resolve(response));
});
}
使用泛型使我們有可能獲得有關(guān)我們將從請(qǐng)求中得到的信息 - 否則,如果它是未知的,我們將不得不成再做一個(gè)轉(zhuǎn)換方法,以獲取有關(guān)我們的類型的正確信息。
在響應(yīng)到達(dá)時(shí)從send
方法解析promise
,使得使用async/await
語法成為可能。通過使用一次而不是在我們的ipcRenderer
上使用,我們確保不會(huì)監(jiān)聽到此指定通道上的其他事件。
現(xiàn)在,我們整個(gè)IpcService
應(yīng)該看起來像這樣:
// src/app/IpcService.ts
import {IpcRenderer} from 'electron';
import {IpcRequest} from "../shared/IpcRequest";
export class IpcService {
private ipcRenderer?: IpcRenderer;
public send<T>(channel: string, request: IpcRequest): Promise<T> {
// 如果ipcRenderer不可用,請(qǐng)嘗試將其初始化
if (!this.ipcRenderer) {
this.initializeIpcRenderer();
}
// 如果沒有responseChannel讓我們自動(dòng)生成它
if (!request.responseChannel) {
request.responseChannel = `${channel}_response_${new Date().getTime()}`
}
放在一起
現(xiàn)在,我們已經(jīng)在主進(jìn)程中創(chuàng)建了一個(gè)用于處理傳入請(qǐng)求的體系結(jié)構(gòu),并實(shí)現(xiàn)了一種發(fā)送此類服務(wù)的服務(wù),現(xiàn)在我們可以將所有內(nèi)容放在一起!
我們要做的第一件事是擴(kuò)展我們的index.html
以包括一個(gè)用于請(qǐng)求我們的信息的按鈕和一個(gè)顯示它的位置:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<button id="request-os-info">Request OS Info</button>
<div id="os-info"></div>
<script>
require('./dist/app/app.js');
</script>
</body>
</html>
所需的app.js
尚不存在-我們來創(chuàng)建它。 請(qǐng)記住,引用的路徑是內(nèi)置文件-我們將實(shí)現(xiàn)TypeScript文件(位于src/app/
中)!
// src/app/app.ts
import {IpcService} from "./IpcService";
const ipc = new IpcService();
document.getElementById('request-os-info').addEventListener('click', async () => {
const t = await ipc.send<{ kernel: string }>('system-info');
document.getElementById('os-info').innerHTML = t.kernel;
});
我們完成了! 乍一看似乎并不令人印象深刻,但是現(xiàn)在單擊按鈕,就將請(qǐng)求從渲染進(jìn)程發(fā)送到我們的主進(jìn)程,該主進(jìn)程將請(qǐng)求委托給負(fù)責(zé)的通道類,并最終以我們的內(nèi)核版本進(jìn)行響應(yīng)。
當(dāng)然,諸如錯(cuò)誤處理之類的事情需要在這里完成,但是這個(gè)概念允許為Electron應(yīng)用程序提供一種非常干凈且易于遵循的通信策略。
可以在GitHub上找到此方法的完整源代碼。
本文章原文地址: https://blog.logrocket.com/electron-ipc-response-request-architecture-with-typescript/