プロセス間通信
Electron で機能豊かなデスクトップアプリケーションを構築するには、プロセス間通信 (IPC) が重要な要素です。 なぜなら、Electron のプロセスモデルではメインプロセスとレンダラープロセスが異なる責務を担っており、UI からネイティブ API を呼び出したり、ネイティブメニューからウェブコンテンツの変更をトリガーしたりといった多くの共同タスクの実行には、IPC が唯一の方法となるからです。
IPC チャンネル
Electron では、 ipcMain
と ipcRenderer
モジュールで開発者が定義した「チャンネル」を介してメッセージを渡すことによって、プロセスが通信します。 これらのチャンネルは 任意 (好きな名称を指定可能) かつ 双方向的 (両方のモジュールで同じチャンネル名を使用可能)です。
このガイドでは、アプリのコードの参考になる基本的な IPC のパターンを具体的な例で説明します。
コンテキスト分離されたプロセスを理解する
実装の詳細に進む前に、コンテキスト分離されたレンダラープロセスにて プリロードスクリプト を使って Node.js と Electron モジュールをインポートするアイデアを知っておきましょう。
- Electron のプロセスモデルの概要については、プロセスモデルのドキュメント をご一読ください。
contextBridge
モジュールでプリロードスクリプトから API を公開する方法の手ほどきについては、コンテキスト分離のチュートリアル をご確認ください。
パターン 1: レンダラーからメインへ (片方向)
レンダラープロセスからメインプロセスへ片方向の IPC メッセージを送信するには、ipcRenderer.send
API を使用してメッセージを送信し、それを ipcMain.on
APIで受信します。
通常このパターンは、ウェブコンテンツからメインプロセスの API を呼び出すために使用します。 ここでは、プログラムによってウインドウのタイトルを変更できる簡単なアプリを作成することで、このパターンを実証しようと思います。
このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
ipcMain.on('set-title', (event, title) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
1. Listen for events with ipcMain.on
In the main process, set an IPC listener on the set-title
channel with the ipcMain.on
API:
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
// ...
function handleSetTitle (event, title) {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
win.setTitle(title)
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.on('set-title', handleSetTitle)
createWindow()
})
// ...
上記の handleSetTitle
コールバックには、ipcMainEvent 構造体とtitle
文字列の 2 つの引数があります。 メッセージが set-title
チャンネルからやってくる度に、この関数がメッセージ送信者として付属する BrowserWindow インスタンスを取り出し、その中の win.setTitle
API を使用します。
次のステップで index.html
と preload.js
のエントリポイントをロードしていることを確認してください。
2. プリロード経由で ipcRenderer.send
を公開する
先ほど作成したリスナーにメッセージを送るには、ipcRenderer.send
API を使用することで可能です。 デフォルトでは、レンダラープロセスは Node.js や Electron のモジュールへアクセスできません。 アプリ開発者として、contextBridge
API を使用し、プリロードスクリプトから API を限定して公開する必要があります。
プリロードスクリプトに、以下のコードを追加します。これは window.electronAPI
グローバル変数をレンダラープロセスに公開します。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})
こうすることで、レンダラープロセスで window.electronAPI.setTitle()
関数が使用できるようになります。
セキュリティ上の理由 から、ipcRenderer.send
API 全体は直接公開していません。 レンダラーの Electron API へのアクセスをできるだけ制限するようにしてください。
3. レンダラープロセスの UI を構築する
BrowserWindow に読み込まれる HTML ファイルに、テキスト入力とボタンからなる基本的なユーザーインターフェイスを追加します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./renderer.js"></script>
</body>
</html>
これらの要素を動作させるために、インポートされる renderer.js
ファイルに数行のコードを追加して、プリロードスクリプトで公開した window.electronAPI
機能を利用します。
const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
const title = titleInput.value
window.electronAPI.setTitle(title)
})
これにより、このデモは完全に機能しているはずです。 入力フィールドを使用すると BrowserWindow のタイトルに何が起こるのか、試してみてください!
パターン 2: レンダラーからメインへ (双方向)
双方向 IPC のよくある応用方法は、レンダラープロセスのコードからメインプロセスのモジュールを呼び出して、結果を待つことです。 これは、ipcRenderer.invoke
と ipcMain.handle
を対にして使うことで実現できます。
以下の例では、レンダラープロセスからネイティブのファイルダイアログを開き、選択されたファイルのパスを返すことにします。
このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('node:path')
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./renderer.js'></script>
</body>
</html>
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
1. ipcMain.handle
でイベントをリッスンする
メインプロセスでは、dialog.showOpenDialog
を呼び出してユーザーが選択したファイルパスの値を返す、handleFileOpen()
関数を作成することになります。 この関数は、レンダラープロセスから dialog:openFile
チャンネルを通して ipcRender.invoke
メッセージが送信されるたびにコールバックとして使用されます。 そして、その戻り値は元の invoke
呼び出しに対する Promise として返されます。
メインプロセスの handle
から送出されたエラーはシリアライズされ、元のエラーのうち message
プロパティのみがレンダラープロセスに提供されるため、不透過です。 詳細は #24427 をご参照ください。
const { app, BrowserWindow, dialog, ipcMain } = require('electron')
const path = require('node:path')
// ...
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
}
}
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('dialog:openFile', handleFileOpen)
createWindow()
})
// ...
IPC チャンネル名の dialog:
という接頭辞は、コードに効果をもたらすものではありません。 これはコードの可読性を向上する名前空間として機能するだけです。
次のステップで index.html
と preload.js
のエントリポイントをロードしていることを確認してください。
2. プリロード経由で ipcRenderer.invoke
を公開する
プリロードスクリプトでは、ipcRenderer.invoke('dialog:openFile')
を呼び出してその値を返す、1 行の関数 openFile
を公開しています。 次のステップでは、この API を使用することでレンダラーのユーザーインターフェースからネイティブのダイアログを呼び出します。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
セキュリティ上の理由 から、ipcRenderer.invoke
API 全体は直接公開していません。 レンダラーの Electron API へのアクセスをできるだけ制限するようにしてください。
3. レンダラープロセスの UI を構築する
最後に、BrowserWindow に読み込む HTML ファイルを構築しましょう。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Dialog</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./renderer.js'></script>
</body>
</html>
この UI は、プリロード API をトリガするために使う単一の #btn
ボタン要素と、選択したファイルのパスを表示するために使う #filePath
要素で構成されます。 これらの部品を動作させるには、レンダラープロセスのスクリプトに以下の数行のコードを記述する必要があります。
const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')
btn.addEventListener('click', async () => {
const filePath = await window.electronAPI.openFile()
filePathElement.innerText = filePath
})
上記スニペットでは、#btn
ボタンのクリックをリッスンし、window.electronAPI.openFile()
API を呼び出してネイティブのファイルを開くダイアログをアクティブにしています。 そして、選択されたファイルパスを#filePath
要素に表示します。
注意: レガシーなアプローチ
ipcRenderer.invoke
API は、レンダラープロセスから双方向 IPC に取りかかるための開発者向けの手段として Electron 7 で追加されました。 しかし、この IPC パターンにはいくつかの代替アプローチが存在します。
できる限り ipcRenderer.invoke
の使用を推奨します。 以下のレンダラーからメインへの双方向パターンは、歴史的な目的のために文書化されたものです。
以下の例では、コードサンプルを小さく保つために、プリロードスクリプトから直接 ipcRenderer
を呼び出しています。
ipcRenderer.send
を使用する
片方向通信で使用した ipcRenderer.send
API は、双方向通信を行う際にも活用できます。 Electron 7 以前の IPC による非同期双方向通信では、この方法が推奨されていました。
// このコードを `contextBridge` API を用いて
// レンダラープロセスに公開することもできます。
const { ipcRenderer } = require('electron')
ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // デベロッパー ツールのコンソールに「pong」と出力する
})
ipcRenderer.send('asynchronous-message', 'ping')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // Node のコンソール「ping」と出力する
// これは `send` のように動作しますが、メッセージの送信元の
// レンダラーにメッセージを返します
event.reply('asynchronous-reply', 'pong')
})
このアプローチには以下のようないくつかの欠点があります。
- レンダラープロセスでレスポンスを処理するために、2 つ目の
ipcRenderer.on
リスナーを用意する必要があります。invoke
ならば、元の API コールに対して Promise として返されるレスポンスの値を得られます。 asynchronous-reply
メッセージが元のasynchronous-message
メッセージとペアであると明示する方法がありません。 これらのチャンネルで非常に頻繁にメッセージが行き来する場合、各コールとレスポンスを個別に追跡することになり、さらなるアプリコードを追加する必要があります。
ipcRenderer.sendSync
を使用する
ipcRenderer.sendSync
API は、メインプロセスにメッセージを送信し、応答を 同期的に 待機します。
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // Node のコンソールに「ping」と出力する
event.returnValue = 'pong'
})
// このコードを `contextBridge` API を用いて
// レンダラープロセスに公開することもできます
const { ipcRenderer } = require('electron')
const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // デベロッパー ツールのコンソールに「pong」と出力する
このコードの構造は invoke
のモデルと非常に似ていますが、パフォーマンス上の理由から この API は避ける ことを推奨します。 これは同期的であるため、応答があるまでレンダラープロセスをブロックしてしまいます。
パターン 3: メインからレンダラーへ
メインプロセスからレンダラープロセスにメッセージを送信する場合、どのレンダラーがメッセージを受信するかを指定する必要があります。 メッセージは、WebContents
インスタンスを介してレンダラープロセスに送信する必要があります。 この WebContents インスタンスには、ipcRenderer.send
と同じ方法で使用できる send
メソッドが含まれています。
このパターンを実証するために、オペレーティングシステムのネイティブメニューで制御される数値カウンターを構築することにします。
このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment'
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement'
}
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index.html')
// Open the DevTools.
mainWindow.webContents.openDevTools()
}
app.whenReady().then(() => {
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>
const counter = document.getElementById('counter')
window.electronAPI.handleCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
event.sender.send('counter-value', newValue)
})
1. webContents
モジュールでメッセージを送信する
このデモでは、まず Electron の Menu
モジュールを使い、メインプロセスでカスタムメニューを作成します。このモジュールは webContents.send
API を使ってメインプロセスからターゲットレンダラーに IPC メッセージを送信します。
const { app, BrowserWindow, Menu, ipcMain } = require('electron')
const path = require('node:path')
function createWindow () {
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: [
{
click: () => mainWindow.webContents.send('update-counter', 1),
label: 'Increment'
},
{
click: () => mainWindow.webContents.send('update-counter', -1),
label: 'Decrement'
}
]
}
])
Menu.setApplicationMenu(menu)
mainWindow.loadFile('index.html')
}
// ...
このチュートリアルで重要なのは、click
ハンドラが update-counter
チャンネルを介してメッセージ (1
または -1
) をレンダラープロセスに送信することです。
click: () => mainWindow.webContents.send('update-counter', -1)
次のステップで index.html
と preload.js
のエントリポイントをロードしていることを確認してください。
2. プリロード経由で ipcRenderer.on
を公開する
以前のレンダラーからメインへのサンプルのように、プリロードスクリプトで contextBridge
と ipcRenderer
モジュールを使用し、IPC 機能をレンダラープロセスに公開します。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
プリロードスクリプトのロード後、レンダラープロセスは window.electronAPI.onUpdateCounter()
リスナー関数にアクセスできるようになるでしょう。
セキュリティ上の理由 から、ipcRenderer.on
API 全体は直接公開していません。 レンダラーの Electron API へのアクセスをできるだけ制限するようにしてください。
この最小限の例の場合、コンテキストブリッジ上に公開するのではなくプリロードスクリプト内で ipcRenderer.on
を直接呼び出せます。
const { ipcRenderer } = require('electron')
window.addEventListener('DOMContentLoaded', () => {
const counter = document.getElementById('counter')
ipcRenderer.on('update-counter', (_event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue
})
})
しかし、この方法ではリスナーがレンダラーのコードと直接対話できないため、コンテキストブリッジ上にプリロードの API を公開する場合と比較して、柔軟性が制限されます。
3. レンダラープロセスの UI を構築する
すべてを繋げるために、読み込んだ HTML ファイルには、値の表示に使う #counter
要素を含んだインターフェイスを作成することにします。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>
最後に、HTML ドキュメントの値を更新するために、update-counter
イベントが発火されるたびに #counter
要素の値が更新されるように DOM 操作を数行追加します。
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((_event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
})
上記のコードでは、プリロードスクリプトから公開された window.electronAPI.onUpdateCounter
関数へコールバックを渡しています。 2 番目の value
パラメータは、ネイティブメニューから webContents.send
呼び出しで渡された 1
または -1
に対応します。
任意: 返信を返す
メインからレンダラーへの IPC では ipcRenderer.invoke
に相当するものがありません。 その代わり、ipcRenderer.on
コールバック内からメインプロセスに返信を送ることができます。
前の例のコードを少し修正すれば、これを実証できます。 レンダラープロセスで event
引数を利用し、counter-value
チャンネルを介してメインプロセスに応答を返信します。
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((event, value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
event.sender.send('counter-value', newValue)
})
メインプロセスでは、counter-value
イベントをリッスンし適切にハンドリングします。
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // 値が Node のコンソールへ出力されます
})
// ...
パターン 4: レンダラーからレンダラーへ
ipcMain
と ipcRenderer
のモジュールを利用して、Electron のレンダラープロセス間でメッセージを直接送信する方法はありません。 これを達成するには、以下の 2 つの選択肢があります。
- メインプロセスをレンダラー間のメッセージブローカとして使用する。 これは、一方のレンダラーからメインプロセスにメッセージを送り、メインプロセスがそのメッセージをもう一方のレンダラーに転送するというものです。
- メインプロセスから両方のレンダラーに MessagePort を渡す。 これは、最初にセットアップした後からレンダラー間の直接通信ができるようになります。
オブジェクトのシリアライズ
Electron の IPC 実装では、HTML 標準の 構造化複製アルゴリズム を用いてプロセス間で渡されるオブジェクトをシリアライズしているため、特定の型のオブジェクトのみが IPC チャンネルを通して渡されることになります。
特に、DOM オブジェクト (Element
、Location
、DOMMatrix
など)、内部に C++ のクラスがある Node.js オブジェクト (process.env
、Stream
のいくつかのメンバーなど)、内部に C++ のクラスがある Electron オブジェクト (WebContents
、BrowserWindow
、WebFrame
など) は、構造化複製ではシリアライズできません。