プロセス間通信
Electron で機能豊かなデスクトップアプリケーションを構築するには、プロセス間通信 (IPC) が重要な要素です。 なぜなら、Electron のプロセスモデルではメインプロセスとレンダラープロセスが異なる責務を担っており、UI からネイティブ API を呼び出したり、ネイティブメニューからウェブコンテンツの変更をトリガーしたりといった多くの共同タスクの実行には、IPC が唯一の方法となるからです。
IPC チャンネル
In Electron, processes communicate by passing messages through developer-defined "channels" with the ipcMain
and ipcRenderer
modules. これらのチャンネルは 任意 (好きな名称を指定可能) かつ 双方向的 (両方のモジュールで同じチャンネル名を使用可能)です。
このガイドでは、アプリのコードの参考になる基本的な IPC のパターンを具体的な例で説明します。
コンテキスト分離されたプロセスを理解する
Before proceeding to implementation details, you should be familiar with the idea of using a preload script to import Node.js and Electron modules in a context-isolated renderer process.
- For a full overview of Electron's process model, you can read the process model docs.
- For a primer into exposing APIs from your preload script using the
contextBridge
module, check out the context isolation tutorial.
パターン 1: レンダラーからメインへ (片方向)
To fire a one-way IPC message from a renderer process to the main process, you can use the ipcRenderer.send
API to send a message that is then received by the ipcMain.on
API.
通常このパターンは、ウェブコンテンツからメインプロセスの API を呼び出すために使用します。 ここでは、プログラムによってウインドウのタイトルを変更できる簡単なアプリを作成することで、このパターンを実証しようと思います。
このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
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/renderer')
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. ipcMain.on
でイベントをリッスンする
メインプロセスで、ipcMain.on
API を使って set-title
チャンネルに IPC リスナーを設定します。
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()
})
// ...
The above handleSetTitle
callback has two parameters: an IpcMainEvent structure and a title
string. メッセージが 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()
関数が使用できるようになります。
We don't directly expose the whole ipcRenderer.send
API for security reasons. レンダラーの 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 のよくある応用方法は、レンダラープロセスのコードからメインプロセスのモジュールを呼び出して、結果を待つことです。 This can be done by using ipcRenderer.invoke
paired with ipcMain.handle
.
以下の例では、レンダラープロセスからネイティブのファイルダイアログを開き、選択されたファイルのパスを返すことにします。
このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron/main')
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/renderer')
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')
})
We don't directly expose the whole ipcRenderer.invoke
API for security reasons. レンダラーの 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: メインからレンダラーへ
メインプロセスからレンダラープロセスにメッセージを送信する場合、どのレンダラーがメッセージを受信するかを指定する必要があります。 Messages need to be sent to a renderer process via its WebContents
instance. This WebContents instance contains a send
method that can be used in the same way as ipcRenderer.send
.
このパターンを実証するために、オペレーティングシステムのネイティブメニューで制御される数値カウンターを構築することにします。
このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。
- main.js
- preload.js
- index.html
- renderer.js
const { app, BrowserWindow, Menu, ipcMain } = require('electron/main')
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/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
<!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.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(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', (_event, value) => callback(value))
})
プリロードスクリプトのロード後、レンダラープロセスは window.electronAPI.onUpdateCounter()
リスナー関数にアクセスできるようになるでしょう。
We don't directly expose the whole ipcRenderer.on
API for security reasons. レンダラーの Electron API へのアクセスをできるだけ制限するようにしてください。 また、コールバックを単に ipcRenderer.on
へ渡さないでください。これは event.sender
を介して ipcRenderer
を漏洩してしまいます。 必要な引数のみを指定して callback
を呼び出すような、カスタムハンドラーを使用してください。
この最小限の例の場合、コンテキストブリッジ上に公開するのではなくプリロードスクリプト内で 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((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
コールバック内からメインプロセスに返信を送ることができます。
前の例のコードを少し修正すれば、これを実証できます。 レンダラープロセスでは、別の API を公開しておくことで、counter-value
チャネルを介してメインプロセスへ応答を返信します。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value)),
counterValue: (value) => ipcRenderer.send('counter-value', value)
})
const counter = document.getElementById('counter')
window.electronAPI.onUpdateCounter((value) => {
const oldValue = Number(counter.innerText)
const newValue = oldValue + value
counter.innerText = newValue.toString()
window.electronAPI.counterValue(newValue)
})
メインプロセスでは、counter-value
イベントをリッスンし適切にハンドリングします。
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // 値が Node のコンソールへ出力されます
})
// ...
パターン 4: レンダラーからレンダラーへ
ipcMain
と ipcRenderer
のモジュールを利用して、Electron のレンダラープロセス間でメッセージを直接送信する方法はありません。 これを達成するには、以下の 2 つの選択肢があります。
- メインプロセスをレンダラー間のメッセージブローカとして使用する。 これは、一方のレンダラーからメインプロセスにメッセージを送り、メインプロセスがそのメッセージをもう一方のレンダラーに転送するというものです。
- Pass a MessagePort from the main process to both renderers. これは、最初にセットアップした後からレンダラー間の直接通信ができるようになります。
オブジェクトのシリアライズ
Electron の IPC 実装では、HTML 標準の 構造化複製アルゴリズム を用いてプロセス間で渡されるオブジェクトをシリアライズしているため、特定の型のオブジェクトのみが IPC チャンネルを通して渡されることになります。
特に、DOM オブジェクト (Element
、Location
、DOMMatrix
など)、内部に C++ のクラスがある Node.js オブジェクト (process.env
、Stream
のいくつかのメンバーなど)、内部に C++ のクラスがある Electron オブジェクト (WebContents
、BrowserWindow
、WebFrame
など) は、構造化複製ではシリアライズできません。