美文网首页
使用Typescript构架Electron的IPC响应与请求

使用Typescript构架Electron的IPC响应与请求

作者: Ellite | 来源:发表于2020-03-22 03:50 被阅读0次

Electron的工作方式非常简单。有两种不同的进程-主进程(Main Process)渲染进程(Renderer Process)。始终只保持有一个主进程,这是您Electron应用程序的入口。可以有任意数量的渲染器进程,这些进程负责渲染您的应用程序。

这些进程间的通信是通过IPC(进程间通信)完成的。听起来可能很复杂,但这只是异步请求-响应模式的一个好听的名字。

渲染器与主进程之间的通信在后台发生的事情基本上只是事件调度。例如,假设您的应用程序应显示有关其运行系统的信息。这可以通过一个简单的命令uname -a来完成,该命令显示您的内核版本。但是您的应用程序本身无法此执行命令,因此需要主进程。在Electron应用程序中,您的应用程序可以访问渲染器进程(ipcRenderer)。

以下是将要发生的事情:

  1. 您的应用程序将利用ipcRenderer向主进程发送事件。这些事件称为Electron内部的通道。

  2. 如果主进程正确的注册了事件侦听器(用于侦听刚刚调度的事件),则可以为该事件正确的运行代码。

  3. 完成所有操作后,主进程可以为结果发出另一个事件(在我们的示例中为内核版本)。

4. 现在整个工作流程都以相反的方式进行,渲染器流程需要为主流程中分派的事件实现一个侦听器。

  1. 当渲染器进程收到包含我们所需信息的适当事件时,在界面上显示该信息。

最终,整个过程可以看作是一个简单的请求-响应模式,有点像HTTP – 只不过是异步的。我们将通过某个指定的频道发起请求,并在某个指定的频道上收到对此的回复。

多亏了TypeScript,我们可以将整个逻辑抽象成一个干净分离且正确封装的应用程序中,在这个应用程序中,我们为主进程中的单个通道定义了单独的类,并利用Promise简化异步请求。再说一遍,这听起来比实际情况复杂得多!

用TypeScript引导电子应用程序


我们需要做的第一件事是用TypeScript引导我们的Electron应用程序。我们的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目录中,所有文件都将构建到dist目录中。我们将把src目录分成两个单独的目录,一个用于Electron,一个用于我们的应用程序。整个目录结构如下所示:

src/
  app/
  electron/
  shared/
index.html
package.json
tsconfig.json

我们的index.html将被Electron加载,非常简单(目前):

<!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>

我们要实施的第一个文件是Electron的主文件。 该文件将实现Main类,该类负责初始化我们的Electron应用程序:

// 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();

运行npm start现在应该启动Electron应用程序并显示index.html

接下来我们要实现的是如何处理IPC通道。

通道处理


基于SoC概念,我们将为每个通道实现一个类。这些类将负责传入的请求。在上面的例子中,我们有一个SystemInfoChannel负责收集系统数据。如果你想使用某些工具,例如使用Vagrant控制虚拟机,就创建一个VagrantChannel等。

每个通道都将有一个名称和一个处理传入请求的方法,因此我们为此创建一个接口:

// src/electron/IPC/IpcChannelInterface.ts

import {IpcMainEvent} from 'electron';

export interface IpcChannelInterface {
  getName(): string;

  handle(event: IpcMainEvent, request: any): void;
}

有个棘手的事情,在很多情况下,any类型意味着设计上的存在缺陷,我们不想拥有这种缺陷。因此,让我们花点时间考虑一下request的类型。

请求是从渲染进程中发出的。发送请求时可能需要知道两件事:

  • 我们的频道可以接受参数
  • 该使用哪个通道来响应

两者都是可选的,我们可以创建一个发送请求的接口。此接口将在Electron和我们的应用程序之间共享

export interface IpcRequest {
  responseChannel?: string;

  params?: string[];
}

现在我们可以回到IpcChannelInterface,为我们的request添加适当的类型:

handle(event: IpcMainEvent, request: IpcRequest): void;

接下来我们需要注意的是如何将频道添加到我们的主进程中。最简单的方法是将通道数组添加到Main类的init方法中。这些频道将由我们的ipcMain进程注册:

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)));
}

这里发生的事情是,传递到init方法的通道将注册到主进程,并由它们的响应通道类处理。
为了更容易理解,让我们从上面的示例中快速实现系统信息的类:

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() });
  }
}

通过将此类的实例添加到我们的Main类的init调用中,我们现在注册了我们的第一个通道处理程序:

(new Main()).init([
  new SystemInfoChannel()
]);

现在,每次在system-info通道上发生请求时,SystemInfoChannel都会处理该请求,并通过在内核上响应(在responseChannel上)来正确处理该请求。

到目前为止,我们已经完成了以下工作:


到目前为止看起来不错,但我们仍然缺少应用程序实际完成工作的部分,例如发送收集内核版本的请求。

从我们的应用程序发送请求


为了利用干净的主流程的IPC架构,我们需要在应用程序中实现一些逻辑。 为了简单起见,我们的用户界面将仅具有一个用于向主进程发送请求的按钮,该按钮将返回我们的内核版本。

我们所有与IPC相关的逻辑都将放在一个简单的服务– IpcService类中:

// src/app/IpcService.ts

export class IpcService {
}

使用此类时,我们要做的第一件事是确保我们可以访问ipcRenderer

如果您想知道为什么我们需要这样做,那是因为不这样做,直接打开index.html文件时,没有可用的ipcRenderer

让我们添加一个可以正确初始化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;
}

当我们试图从主流程请求某些内容时,将调用此方法-这是我们需要实现的下一个方法:

 // 如果ipcRenderer不可用,请尝试将其初始化
  if (!this.ipcRenderer) {
    this.initializeIpcRenderer();
  }
  // 如果没有responseChannel让我们自动生成它
  if (!request.responseChannel) {
    request.responseChannel = `${channel}_response_${new Date().getTime()}`
  }

  const ipcRenderer = this.ipcRenderer;
  ipcRenderer.send(channel, request);

  // 该方法返回一个`promise`,当响应到达时将调用`resolve`。
  return new Promise(resolve => {
    ipcRenderer.once(request.responseChannel, (event, response) => resolve(response));
  });
}

使用泛型使我们有可能获得有关我们将从请求中得到的信息 - 否则,如果它是未知的,我们将不得不成再做一个转换方法,以获取有关我们的类型的正确信息。

在响应到达时从send方法解析promise,使得使用async/await语法成为可能。通过使用一次而不是在我们的ipcRenderer上使用,我们确保不会监听到此指定通道上的其他事件。

现在,我们整个IpcService应该看起来像这样:

// 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不可用,请尝试将其初始化
    if (!this.ipcRenderer) {
      this.initializeIpcRenderer();
    }
    // 如果没有responseChannel让我们自动生成它
    if (!request.responseChannel) {
      request.responseChannel = `${channel}_response_${new Date().getTime()}`
    }

放在一起


现在,我们已经在主进程中创建了一个用于处理传入请求的体系结构,并实现了一种发送此类服务的服务,现在我们可以将所有内容放在一起!

我们要做的第一件事是扩展我们的index.html以包括一个用于请求我们的信息的按钮和一个显示它的位置:

<!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尚不存在-我们来创建它。 请记住,引用的路径是内置文件-我们将实现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;
});

我们完成了! 乍一看似乎并不令人印象深刻,但是现在单击按钮,就将请求从渲染进程发送到我们的主进程,该主进程将请求委托给负责的通道类,并最终以我们的内核版本进行响应。


当然,诸如错误处理之类的事情需要在这里完成,但是这个概念允许为Electron应用程序提供一种非常干净且易于遵循的通信策略。

可以在GitHub上找到此方法的完整源代码。

本文章原文地址: https://blog.logrocket.com/electron-ipc-response-request-architecture-with-typescript/

相关文章

网友评论

      本文标题:使用Typescript构架Electron的IPC响应与请求

      本文链接:https://www.haomeiwen.com/subject/tutmyhtx.html