使用Typescript構(gòu)架Electron的IPC響應(yīng)與請(qǐng)求

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ā)生的事情:

  1. 您的應(yīng)用程序?qū)⒗?code>ipcRenderer向主進(jìn)程發(fā)送事件。這些事件稱為Electron內(nèi)部的通道。

  2. 如果主進(jìn)程正確的注冊(cè)了事件偵聽器(用于偵聽剛剛調(diào)度的事件),則可以為該事件正確的運(yùn)行代碼。

  3. 完成所有操作后,主進(jìn)程可以為結(jié)果發(fā)出另一個(gè)事件(在我們的示例中為內(nèi)核版本)。

4. 現(xiàn)在整個(gè)工作流程都以相反的方式進(jìn)行,渲染器流程需要為主流程中分派的事件實(shí)現(xiàn)一個(gè)偵聽器。

  1. 當(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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,208評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,477評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,960評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,200評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,726評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,617評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,807評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,049評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評(píng)論 1 281
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,432評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,769評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容