Code for final

ふぁいなる向けのコード置き場です。すでにコードじゃないこともいっぱい。

最低限抑えておくレベルでElectronを使ったアプリ開発のプロセス間通信を簡単に理解する

Electronを使ってアプリ開発をしていましたが、Electronの仕組みを理解できていなかったので開発に必要なレベルで簡単にまとめました。
コードの説明はvue.js + typescriptを使った例になりますが、ほかの場合も考え方はほぼ同じだと思います。

メインプロセスとレンダラープロセスはサーバーとクライアントのような関係

Electronはメインプロセス(1つ)とレンダラープロセス(複数)で動作します。
メインから画面を立ち上げるごとにレンダラープロセスが生成されます。
ここでいうプロセスはexeファイルのことだと思ってください。
メインプロセスでOSとのやり取りを行い、レンダラープロセスではユーザーとやり取りを行います。
メインプロセスをサーバーと考えれば、レンダラープロセスはクライアント(ブラウザ)というイメージですかね。

クライアント・サーバー間の通信はHTTP、メイン・レンダラープロセス間の通信はプロセス間通信

クライアントとサーバー間はHTTPを使って通信するようにメインとレンダラー間はプロセス間通信(IPC)で通信します。
プロセス間通信というと難しく聞こえるかもですが、HTTPと同じでリクエストを送って、レスポンスを受けるのと変わりません。

プロセス間通信のイメージ

私が理解したプロセス間通信をイメージを絵にしてみました。個人的なイメージなので間違ってたらすみません。

f:id:finalstream:20200609144100j:plain

OSとやり取り(ファイル操作とか)するのはメインプロセスで、レンダラープロセスからは依頼(リクエスト)して結果(レスポンス)を受け取るイメージです。

レンダラープロセスの実装(リクエスト送信)

Node.jsにプロセス間通信を行うためのapiが用意されておりそれを使うことで簡単に実装できます。
レンダラープロセス内(vueファイル)でリクエストしたいタイミングでipcRenderer.invoke()を実行します。
以下の例はディレクトリパスを渡して配下のディレクトリを返却する検索処理だと思ってください。
"searchDirectory"が呼び出すAPIの名前で、this.directoryPathはAPIに渡すパラメタです。

import electron from "electron";

export default class App extends Vue {

  private ipcRenderer = electron.ipcRenderer;

  async search() {
    const response: IpcResponse<string[]> = await this.ipcRenderer.invoke(
      "searchDirectory",
      this.directoryPath
    );
    if (response.error) {
      // エラーのときの処理
      this.showMessage(MessageLevel.Warning, "ディレクトリーが存在しません");
      return;
    }
    // 正常のときの処理
    this.updateDirectories(response.data);
  }

ipcRendererにはsend()というメソッドがありますが、async/awaitで書けるinvoke()を使ったほうがシンプルに書けるのでおすすめです。
TypeScriptで書いているのでIpcResponseというレスポンスオブジェクトを定義しています。これは自作クラスです。標準で用意されているかと思い、探してなかったので作りました。

メインプロセスの実装(レスポンス送信)

レンダラープロセス同様にこちらもAPIが用意されています。
メインプロセス(background.ts)にipcMain.handle()の処理を実装しておきます。
以下の例は指定パスの配下のディレクトリを取得して返却する処理になります。
"searchDirectory"は待ち受けるAPI名で、directoryPathは受信したパラメタです。

import { ipcMain } from "electron";

ipcMain.handle("searchDirectory", async (event, directoryPath) => {
  try {
    const dirents = await fs.promises.readdir(dirpath, { withFileTypes: true });
    return new IpcResponse(directoryPath);
  } catch (e) {
    return new IpcResponse(e);
  }
});

ipcMainにもon()というメソッドがありますが、async/awaitで書けるhandle()を使ったほうがシンプルに書けるのでおすすめです。

プロセス間通信を行うための設定

デフォルトの設定でプロセス間通信を行おうとすると、アプリを起動しただけで以下のようなエラーに苛まれます。

index.js:4 Uncaught ReferenceError: __dirname is not defined
    at Object../node_modules/electron/index.js (index.js:4)
    at __webpack_require__ (bootstrap:853)
    at fn (bootstrap:150)
    at Module../node_modules/cache-loader/dist/cjs.js?!./node_modules/babel-loader/lib/index.js!./node_modules/ts-loader/index.js?!./node_modules/cache-loader/dist/cjs.js?!./node_modules/vue-loader/lib/index.js?!./src/views/AppMain.vue?vue&type=script&lang=ts& (app.js:1056)
    at __webpack_require__ (bootstrap:853)
    at fn (bootstrap:150)
    at Module../src/views/AppMain.vue?vue&type=script&lang=ts& (AppMain.vue?679e:1)
    at __webpack_require__ (bootstrap:853)
    at fn (bootstrap:150)
    at Module../src/views/AppMain.vue (AppMain.vue?8622:1)

これはだいぶハマったのですが、どうやらデフォルトではセキュリティを高めるため、プロセス間通信できないようになっています。
background.tsの以下の箇所にある"nodeIntegration"をtrueにすればOKです。

// Create the browser window.
  win = new BrowserWindow({
    x: AppStore.instance.get("window.x"),
    y: AppStore.instance.get("window.y"),
    width: 800,
    height: 600,
    webPreferences: {
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      nodeIntegration: true,  //  ★ここをtrueにする
    },
    title: "Empty Directory Cleaner",
  });

もしくはvue.config.jsでも指定できます。どちらかお好きな方で指定してください。

module.exports = {
  pluginOptions: {
    electronBuilder: {
      nodeIntegration: true,
    },
  },
};

ただ、このnodeIntegrationをtrueにするのは推奨されない行為らしいです。
これをfalseのままプロセス間通信できるか調べて見ました。
以下でも議論されていますが、やり方はあるっぽいですが、Node.jsの便利なAPIは使えないっぽいです。

github.com

まとめ

Electronのプロセス間通信はwebと同じで、サーバー(メインプロセス)とクライアント(レンダラープロセス)間で行われるHTTP通信のようなもの。