白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Electronでデスクトップアプリを作りたい(メインプロセスとレンダラープロセス)

こんばんは。

すっかり寒くなってきましたね。
毎日お布団から出たくないです。

前回はElectronでHello Worldを表示するだけのプログラムを作りましたが、
今回はもう少し仕組みについて掘り下げてみます。

2つのプロセス

Electronでアプリを作成しようとして最初に混乱するのが、メイン/レンダラーの2つのプロセスについてかもしれません。
かくいう私もしっかりと罠にハマりました。

ネットで検索するとサンプルはたくさん出てくるので、それをそのままコピペしたら動いたけど、
ちょっといじったら変なエラーが出るとかまぁありがちですよね。

electronjs.org

ちょっと公式のドキュメントを覗いてみましょう。

In Electron, the process that runs package.json's main script is called the main process. The script that runs in the main process can display a GUI by creating web pages. An Electron app always has one main process, but never more.

Since Electron uses Chromium for displaying web pages, Chromium's multi-process architecture is also used. Each web page in Electron runs in its own process, which is called the renderer process.

package.jsonの「main」で指定されている、アプリケーション自体を実行しているNode.jsのプロセスがメインプロセスで、これはElectronアプリには常に1つしかありません。
一方でレンダラープロセスはChromiumを利用した画面表示用のプロセスで、メインプロセスから複数呼び出すことができます。で、

In web pages, calling native GUI related APIs is not allowed because managing native GUI resources in web pages is very dangerous and it is easy to leak resources. If you want to perform GUI operations in a web page, the renderer process of the web page must communicate with the main process to request that the main process perform those operations.

とあるように、レンダラープロセス側であまり処理をするのは良くないようなので、
基本的にはレンダラープロセスで描画や操作をさせて、必要に応じてメインプロセスに処理を依頼することで、
ブラウザではできないような処理をさせるという感じでしょうか。
若干ニュアンスは違うかもしれませんが、Webアプリのサーバサイドとクライアントサイドみたいな。

ちなみにElectronのAPIは種類によってメインプロセスのみで使えるもの、レンダラープロセスのみで使えるもの、両方で使えるものが分かれています。

electronjs.org

プロセス間の通信

2つのプロセスをうまく使い分けながら処理をすることが必要なことはわかりましたが、
デスクトップアプリではWebアプリのように1リクエストごとにサーバサイドの処理→クライアントサイドの処理という流れがあるわけではありません。
そのため、プロセス間の通信が必要になってきます。

プロセス間の通信にはIPC(InterProcess Communication)を利用すると便利です。

レンダラープロセスで利用するのはipcRenderer。
ここではテキストボックスの中でEnterキーが押されたときにメインプロセスに「renderer2main」というチャネル(イベントみたいなもの?)で通信を行います。
また、メインプロセスから「main2renderer」というチャネルで受信したときには値を画面上に書き込みます。

少し気をつけたいのがレンダラープロセスは複数同時に起動することができますが、レンダラープロセス間の通信はできません。
ipcRendererが送信先のプロセスを指定していないことからもわかるように、常にメインプロセスに対して通信を行います。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>sample</title>
  </head>
  <body>
    <input type="text" id="req" value="">
    <p id="res"></p>
  </body>
  <script>
    const {ipcRenderer} = require('electron');
    document.getElementById("req").addEventListener("keyup", function(e) {
      if (e.keyCode === 13) {
        ipcRenderer.send('renderer2main', this.value);
        this.value = "";
      }
    });
    ipcRenderer.on('main2renderer', function(event, arg) {
      document.getElementById("res").innerHTML += arg + "<br>";
    });
  </script>
</html>

一方でメインプロセスで利用するのはipcMain。
この場合だと、「renderer2main」というチャネルを受け取ったときに、
「main2renderer」というチャネルでレンダラープロセスに送り返します。

メインプロセスからレンダープロセスに通信する場合には相手が決まっている必要があるので、
BrowserWindowのwebContentsに対して送信します。(「did-finish-load」はレンダラープロセスのロードが完了したら)

const { app, BrowserWindow, ipcMain } = require('electron');

app.on('ready', function() {

    let win = new BrowserWindow({
        width: 300,
        height: 300,
        webPreferences: {
            nodeIntegration: true
        }
    });

    win.loadFile('index.html');

    win.webContents.on('did-finish-load', function () {
        this.send('main2renderer', "init");
    })
});

app.on('window-all-closed', function() {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

ipcMain.on('renderer2main', (event, arg) => {
    event.reply('main2renderer', arg + arg);
    //event.sender.send('main2renderer', arg + arg); こっちでもいい
});

今回はメインプロセスの処理はただ2回繰り返して送り返しているだけなのでレンダラープロセスでも処理できますが、
たとえばファイルの読み書きやAPIをつつくといった処理を行うこともメインプロセスならできます。
(レンダラープロセスで絶対できないかと言うとまたややこしいのですが、基本はメインプロセスでやりますよね…)

f:id:Shiro-Neko:20191109234736p:plain

でまぁ、特に意味のないアプリが出来上がったわけです。

・メインプロセスとレンダラープロセスがある
・プロセス間の通信にはIPCを使うと便利

はい、ということです。