メインコンテンツへ飛ぶ

プロセス間通信

Electron で機能豊かなデスクトップアプリケーションを構築するには、プロセス間通信 (IPC) が重要な要素です。 なぜなら、Electron のプロセスモデルではメインプロセスとレンダラープロセスが異なる責務を担っており、UI からネイティブ API を呼び出したり、ネイティブメニューからウェブコンテンツの変更をトリガーしたりといった多くの共同タスクの実行には、IPC が唯一の方法となるからです。

IPC チャンネル

Electron では、 ipcMainipcRenderer モジュールで開発者が定義した「チャンネル」を介してメッセージを渡すことによって、プロセスが通信します。 これらのチャンネルは 任意 (好きな名称を指定可能) かつ 双方向的 (両方のモジュールで同じチャンネル名を使用可能)です。

このガイドでは、アプリのコードの参考になる基本的な IPC のパターンを具体的な例で説明します。

コンテキスト分離されたプロセスを理解する

実装の詳細に進む前に、コンテキスト分離されたレンダラープロセスにて プリロードスクリプト を使って Node.js と Electron モジュールをインポートするアイデアを知っておきましょう。

パターン 1: レンダラーからメインへ (片方向)

レンダラープロセスからメインプロセスへ片方向の IPC メッセージを送信するには、ipcRenderer.send API を使用してメッセージを送信し、それを ipcMain.on APIで受信します。

通常このパターンは、ウェブコンテンツからメインプロセスの API を呼び出すために使用します。 ここでは、プログラムによってウインドウのタイトルを変更できる簡単なアプリを作成することで、このパターンを実証しようと思います。

このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。

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()
})

1. ipcMain.on でイベントをリッスンする

メインプロセスで、ipcMain.on API を使って set-title チャンネルに IPC リスナーを設定します。

main.js (Main Process)
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 を使用します。

info

次のステップで index.htmlpreload.js のエントリポイントをロードしていることを確認してください。

2. プリロード経由で ipcRenderer.send を公開する

先ほど作成したリスナーにメッセージを送るには、ipcRenderer.send API を使用することで可能です。 デフォルトでは、レンダラープロセスは Node.js や Electron のモジュールへアクセスできません。 アプリ開発者として、contextBridge API を使用し、プリロードスクリプトから API を限定して公開する必要があります。

プリロードスクリプトに、以下のコードを追加します。これは window.electronAPI グローバル変数をレンダラープロセスに公開します。

preload.js (Preload Script)
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 ファイルに、テキスト入力とボタンからなる基本的なユーザーインターフェイスを追加します。

index.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 機能を利用します。

renderer.js (Renderer Process)
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.invokeipcMain.handle を対にして使うことで実現できます。

以下の例では、レンダラープロセスからネイティブのファイルダイアログを開き、選択されたファイルのパスを返すことにします。

このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。

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()
})

1. ipcMain.handle でイベントをリッスンする

メインプロセスでは、dialog.showOpenDialog を呼び出してユーザーが選択したファイルパスの値を返す、handleFileOpen() 関数を作成することになります。 この関数は、レンダラープロセスから dialog:openFile チャンネルを通して ipcRender.invoke メッセージが送信されるたびにコールバックとして使用されます。 そして、その戻り値は元の invoke 呼び出しに対する Promise として返されます。

エラーハンドリングの小話

メインプロセスの handle から送出されたエラーはシリアライズされ、元のエラーのうち message プロパティのみがレンダラープロセスに提供されるため、不透過です。 詳細は #24427 をご参照ください。

main.js (Main Process)
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: という接頭辞は、コードに効果をもたらすものではありません。 これはコードの可読性を向上する名前空間として機能するだけです。

info

次のステップで index.htmlpreload.js のエントリポイントをロードしていることを確認してください。

2. プリロード経由で ipcRenderer.invoke を公開する

プリロードスクリプトでは、ipcRenderer.invoke('dialog:openFile') を呼び出してその値を返す、1 行の関数 openFile を公開しています。 次のステップでは、この API を使用することでレンダラーのユーザーインターフェースからネイティブのダイアログを呼び出します。

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile')
})
セキュリティ警告

セキュリティ上の理由 から、ipcRenderer.invoke API 全体は直接公開していません。 レンダラーの Electron API へのアクセスをできるだけ制限するようにしてください。

3. レンダラープロセスの UI を構築する

最後に、BrowserWindow に読み込む HTML ファイルを構築しましょう。

index.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 要素で構成されます。 これらの部品を動作させるには、レンダラープロセスのスクリプトに以下の数行のコードを記述する必要があります。

renderer.js (Renderer Process)
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 の使用を推奨します。 以下のレンダラーからメインへの双方向パターンは、歴史的な目的のために文書化されたものです。

info

以下の例では、コードサンプルを小さく保つために、プリロードスクリプトから直接 ipcRenderer を呼び出しています。

ipcRenderer.send を使用する

片方向通信で使用した ipcRenderer.send API は、双方向通信を行う際にも活用できます。 Electron 7 以前の IPC による非同期双方向通信では、この方法が推奨されていました。

preload.js (Preload Script)
// このコードを `contextBridge` API を用いて
// レンダラープロセスに公開することもできます。
const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // デベロッパー ツールのコンソールに「pong」と出力する
})
ipcRenderer.send('asynchronous-message', 'ping')
main.js (Main Process)
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 は、メインプロセスにメッセージを送信し、応答を 同期的に 待機します。

main.js (Main Process)
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // Node のコンソールに「ping」と出力する
event.returnValue = 'pong'
})
preload.js (Preload Script)
// このコードを `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 メソッドが含まれています。

このパターンを実証するために、オペレーティングシステムのネイティブメニューで制御される数値カウンターを構築することにします。

このデモでは、メインプロセス、レンダラープロセス、プリロードスクリプトにコードを追加する必要があります。 コード全体は以下のとおりですが、以降の節で各ファイルを個別に説明します。

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()
})

1. webContents モジュールでメッセージを送信する

このデモでは、まず Electron の Menu モジュールを使い、メインプロセスでカスタムメニューを作成します。このモジュールは webContents.send API を使ってメインプロセスからターゲットレンダラーに IPC メッセージを送信します。

main.js (Main Process)
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)
info

次のステップで index.htmlpreload.js のエントリポイントをロードしていることを確認してください。

2. プリロード経由で ipcRenderer.on を公開する

以前のレンダラーからメインへのサンプルのように、プリロードスクリプトで contextBridgeipcRendererモジュールを使用し、IPC 機能をレンダラープロセスに公開します。

preload.js (Preload Script)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})

プリロードスクリプトのロード後、レンダラープロセスは window.electronAPI.onUpdateCounter() リスナー関数にアクセスできるようになるでしょう。

セキュリティ警告

セキュリティ上の理由 から、ipcRenderer.on API 全体は直接公開していません。 レンダラーの Electron API へのアクセスをできるだけ制限するようにしてください。 また、コールバックを単に ipcRenderer.on へ渡さないでください。これは event.sender を介して ipcRenderer を漏洩してしまいます。 必要な引数のみを指定して callback を呼び出すような、カスタムハンドラーを使用してください。

info

この最小限の例の場合、コンテキストブリッジ上に公開するのではなくプリロードスクリプト内で ipcRenderer.on を直接呼び出せます。

preload.js (Preload Script)
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 要素を含んだインターフェイスを作成することにします。

index.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>Menu Counter</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src="./renderer.js"></script>
</body>
</html>

最後に、HTML ドキュメントの値を更新するために、update-counter イベントが発火されるたびに #counter 要素の値が更新されるように DOM 操作を数行追加します。

renderer.js (Renderer Process)
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 チャネルを介してメインプロセスへ応答を返信します。

preload.js (Preload Script)
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)
})
renderer.js (Renderer Process)
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 イベントをリッスンし適切にハンドリングします。

main.js (Main Process)
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // 値が Node のコンソールへ出力されます
})
// ...

パターン 4: レンダラーからレンダラーへ

ipcMainipcRenderer のモジュールを利用して、Electron のレンダラープロセス間でメッセージを直接送信する方法はありません。 これを達成するには、以下の 2 つの選択肢があります。

  • メインプロセスをレンダラー間のメッセージブローカとして使用する。 これは、一方のレンダラーからメインプロセスにメッセージを送り、メインプロセスがそのメッセージをもう一方のレンダラーに転送するというものです。
  • メインプロセスから両方のレンダラーに MessagePort を渡す。 これは、最初にセットアップした後からレンダラー間の直接通信ができるようになります。

オブジェクトのシリアライズ

Electron の IPC 実装では、HTML 標準の 構造化複製アルゴリズム を用いてプロセス間で渡されるオブジェクトをシリアライズしているため、特定の型のオブジェクトのみが IPC チャンネルを通して渡されることになります。

特に、DOM オブジェクト (ElementLocationDOMMatrix など)、内部に C++ のクラスがある Node.js オブジェクト (process.envStream のいくつかのメンバーなど)、内部に C++ のクラスがある Electron オブジェクト (WebContentsBrowserWindowWebFrame など) は、構造化複製ではシリアライズできません。