ElectronでのIPCの例(send, sendSync, invoke, etc.)

ElectronでIPCを同期的に行うときは、sendSyncでなく、invokeを使う。

ということを言いたくて書いていたらIPCの例みたいになったけどまあいいや。

sendSync

sendSyncは実装の見た目は簡潔になるが、レンダラープロセス全体をブロックしてしまう。他にどうしようもない時の最終手段として用いることが勧められている。

// in renderer process
const result = ipcRenderer.sendSync("some-process", args);
doSomething(result);

メインプロセスでは、sendと同じようにipcMain.onでイベントをリッスンできる。

// in main process
ipcMain.on("some-process", (event, args) => {
    const result = doSomethingInMain(args);
    event.returnvalue = result;
});

event.returnValueで返り値を指定できる。

見た目はとても分かりやすい。少しでも重い処理をするならやめた方がいい。

invoke

一方でinvokeはPromiseを返す。実装はthenなどを使うこともできるが、awaitasyncを使えばさっぱりとした(というかほぼ上と同じ)実装になる。

// async+await version
async function f() {
    const result = await ipcRenderer.invoke("some-process-2", args);
    // catchも使える
    // const result = await ipcRenderer.invoke("some-process-3", args).catch(() => "フェールセーフ");
    doSomething(result);
}

// promise version
ipcRenderer.invoke("some-process-2", args).then((result) => {
    doSomething(result);
}).catch(() => {
    doSomething("フェールセーフ");
});

sendSyncの例も関数内で行うのが大半だと思うので、そこまで変わらない。promiseバージョンは、関数内でなくても書けるのが強みだと思う。関数内でpromiseで書こうとすると処理によっては見た目が複雑になるので、asyncawaitで書いていくといいだろう。

関数内なのに、awaitが使えない!というとき(promiseで引っかかるなど)は実装を考え直した方がいい。

メインプロセスでは、ipcMain.handleを使う。返り値がinvokeしたレンダラープロセス側に返る。

// in main process
ipcMain.handle("some-process", async (event, args) => {
    const result = await doHeavyWork(args);
    return result;
});

sendSyncよりもこちらを使う方がいいだろう。メインプロセスの方のasyncawaitは別に使わなくてもいい。

番外編

せっかくなので、他のIPCについても書いておく。

ipcRenderer.send

非同期ならよく使うやつ。メイン側から返信はあってもなくてもいい。しかし、返信は同じ関数にはこない。なぜなら、非同期なので、あっという間にその命令行は通り過ぎるから。返信を受け取るには、あらかじめipcMainからのイベント(チャンネル)をリッスンしておく必要がある。

// in renderer process
// 購読してないと結果は受け取れない
ipcRenderer.on("kekka-otodoke", (event, result) => {
    doSomething(result);
});

ipcRenderer.send("main-de-yoro", arg1);

メインプロセスの方は、event.returnValueの代わりにevent.replyを使うという点以外はsendSyncとほぼ変わらない。

ipcMain.on("main-de-yoro", (event, arg1) => {
    const result = doHardWork(arg1);
    event.reply("kekka-otodoke", result);
});

メインプロセス内で非同期にdoHardWorkが行われる。asyncなどは必要ない。メインプロセスからの返答が不要なときはこれで十分。

メインプロセス側からレンダラープロセスに向けて自発的に送信

今まではメインプロセスは受け取りに徹してきた。しかし、メインプロセスからIPCしたいこともある。単純にipcMain.sendだろう、ということはない。というかsend関数はない。送り先が不明なためだろう。メインプロセスは1つだが、レンダラープロセスは複数になる。どのレンダラープロセス宛てかを指定しないといけないようだ。

どうするかというと、大体メインプロセスで保持しているはずのBrowserWindowのプロパティのwebContentsから送信できる。

// in main process
// 初期化などでBrowserWindowのインスタンスを代入する
let mainWindow;

function doSomething() {
    // 何かする。そしてmainWindowのレンダラープロセスにIPC
    if(mainWindow !== null) {
        mainWindow.webContents.send("main-kara-dayo", args);
    }
}

レンダラープロセスでは、今まで述べたsendの返信の受け取りのようにしておけばいい。

// in renderer process
ipcRenderer.on("main-kara-dayo", (event, result) => {
    updateEtc(result);
});

これは、1つのウィンドウだけに送る例だが、単純に全てのウィンドウに送りたいなら、

// in main process
for(const window of BrowserWindow.getAllWindows()) {
    window.webContents.send("to-all-windows", "hello windows!");
}

などでいいし、複数の特定ウィンドウならletで保持してそれらにsendすればいい。

使い所を考える

実際に使うときは、何をしたいかをよく考える。

レンダラープロセスからなのか、メインプロセスからなのか。返信が必要なのか、どのタイミングで実行するのか、レスポンスタイムはどのくらいになりそうなのか、他の実装で十分なのか、などたくさん考えることはある。

統一性がある実装は保守もしやすいので、そこは考えておくといい。

参考

以上。

タイトルとURLをコピーしました