ElectronのIPC
ElectronのIPCの歴史はセキュリティの歴史でもあります。ゆえに、ちまたにあふれる情報のうち多くは、古いやり方を解説しているものも多いため、注意が必要です。
僕も Electron を触るのは大分久しぶりなため間違っているかもしれないので、なにか間違いがあればツッコミなどをいただけると幸いです。
最初に「今」の正しいIPCのやりかた
まず、メインプロセスでウィンドウを作成する時に、
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: false, // デフォルトで false
contextIsolation: true, // v12以後はデフォルトで true になる予定
preload: join(__dirname, 'preload.js'),
},
})
このように、webPreferences
オプションを指定します。nodeIntegration
はもう既にデフォルトが false
なので指定する必要はないのですが、指定しておくとなんとなくセキュアな気持ちになれます。
contextIsolation
も今後でる v12 以後ではデフォルト true
になるようですが、まだそうではないので true
を指定しておきましょう。セキュアになります。
これらオプションを設定しているときには、プリロードスクリプトが必要です。
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("rpc", {
test: (message) => {
ipcRenderer.invoke('test', message);
},
});
プリロードスクリプトでは、contextBridge
という仕組みを使って、レンダラープロセスに、機能を提供します。
contextBridge.exposeInMainWorld
と ipcRenderer.invoke
を覚えておけば大丈夫です。
ここから先は歴史的経緯を交えた解説です。
ElectronでなぜIPCが必要なのか
Electronは、Node.js と Chromium を組み合わせたソフトウェアです。またメインプロセスとレンダラープロセスという二種類のプロセスで成り立っています。
大まかにいうと起動などを司るNode.jsのメインプロセスから、ウィンドウを描画するための Chromium ベースのレンダラープロセスと認識すればいいでしょう。
初期の Electron は、レンダラープロセスは Chromium だけではなくて Node.js も統合されていました。そのため、レンダラープロセスでは、DOM操作だけではなくて Node.js の全機能を使えたのですが、当然のことながらセキュリティホールにしかならないので、Electron 5.0.0 で統合機能はデフォルトでオフになりました。
そのため、Node.js を使わないとできないことは、メインプロセス上で実行するしかなくなったのです。このとき、レンダラープロセスとメインプロセスで通信を行うプロセス間通信が IPC です。
const { ipcRenderer } = require('electron')
ipcRenderer.send('hoge', 'fuga')
レンダラープロセスでは electron
パッケージの ipcRenderer
オブジェクトを取得し send
メソッドでメッセージを送信します。この仕組み自体は Electron の最初期から用意されていたため、古い記事などで見かけることもあるでしょう。
ところが、くせ者は const { ipcRenderer } = require('electron')
です。このコードは Node.js の require
[1]命令を前提にしているのです。
レンダラープロセスから Node.js を排除したはずなのに require
しないと IPC できないのは詰んでるようにみえます。
そこで導入されたのがプリロードという特権的なモードです。Node.js統合が無効化されている状態でもプリロードスクリプトは、レンダラープロセスでありながら Node.js 機能が使える特別な初期化用のスクリプトとして動きます。
const { ipcRenderer } = require('electron')
process.once('loaded', () => {
global.ipcRenderer = ipcRenderer
})
このプリロードスクリプトでは、
-
ipcRenderer
をrequire
する - global 変数経由で
ipcRenderer
をレンダラープロセスで使えるようにする
ということをやっています。
今でも結構見かけることが多いはずですが、実はこのコードも危ないコードであるために、非推奨となっています。
コンテキストの分離
プリロードスクリプトは前述のとおり、特権的な仕組みです。レンダラープロセスでありながら Node.js の機能が使えるため、セキュリティ上あまりよろしくありません。
そこで、次に Electron のセキュリティ向上のために、V8 コンテキストが分離(contextIsolation
)されました。プリロードスクリプトとレンダラープロセスで、それぞれグローバル変数を共有しないというものです。
こうなってくると、プリロード時にどうやって初期化すればレンダラープロセスでIPCが可能になるかが問題となります。そこで用意されたのが contextBridge
です。contextBridge
はコンテキストが分離されていても問題なく使える、Electron側が用意した安全な仕組みです。
そこで、冒頭に登場したコードです。
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("rpc", {
test: (message) => {
ipcRenderer.invoke('test', message);
},
});
考え方としては、Electronのみで使える RPC(リモートプロシージャコール)だと考えた方がいいでしょう。rpc.test
というメソッドで、特定の機能が実行されるだけのものです。
-
ipcRenderer
そのものをレンダラープロセス側に露出してはいけません - 可能なら
ipcRenderer.send
は、より機能が限定されているipcRendere.invoke
に置き換えましょう
-
ESModules で
import ....
を記述した場合も、結局 babel なりで処理されると、Node.js のrequire
に置き換えられます。 ↩︎
Discussion