メインコンテンツへスキップ

プロセス間通信

プロセス間通信(IPC)は、Electronで機能豊富なデスクトップアプリケーションを構築する上で重要な部分です。Electronのプロセスモデルではメインプロセスとレンダラプロセスはそれぞれ異なる役割を持つため、IPCはUIからネイティブAPIを呼び出す、またはネイティブメニューからWebコンテンツの変更をトリガーするなど、多くの一般的なタスクを実行する唯一の方法です。

IPCチャネル

Electronでは、プロセスはipcMainipcRendererモジュールを使用して、開発者が定義した「チャネル」を介してメッセージをやり取りします。これらのチャネルは**任意**(自由に名前を付けることができます)であり、**双方向**(同じチャネル名を両方のモジュールで使用できます)。

このガイドでは、アプリコードの参照として使用できる具体的な例を用いて、いくつかの基本的なIPCパターンについて説明します。

コンテキスト分離プロセスの理解

実装の詳細に移る前に、コンテキスト分離されたレンダラプロセスでNode.jsとElectronモジュールをインポートするためにプリロードスクリプトを使用するという概念に精通している必要があります。

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

レンダラプロセスからメインプロセスに一方向のIPCメッセージを送信するには、ipcRenderer.sendAPIを使用してメッセージを送信し、次にipcMain.onAPIで受信します。

通常、このパターンはWebコンテンツからメインプロセスの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(メインプロセス)
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.htmlpreload.jsのエントリポイントを読み込んでいることを確認してください!

2. プリロードによるipcRenderer.sendの公開

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

プリロードスクリプトに次のコードを追加すると、グローバルwindow.electronAPI変数がレンダラプロセスに公開されます。

preload.js(プリロードスクリプト)
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>

これらの要素をインタラクティブにするために、プリロードスクリプトから公開されたwindow.electronAPI機能を利用するインポートされたrenderer.jsファイルにいくつかのコード行を追加します。

renderer.js(レンダラプロセス)
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(メインプロセス)
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.htmlpreload.jsのエントリポイントを読み込んでいることを確認してください!

2. プリロードによるipcRenderer.invokeの公開

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

preload.js(プリロードスクリプト)
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(レンダラプロセス)
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は、Electron 7で、レンダラプロセスからの双方向IPCに対処するための開発者フレンドリーな方法として追加されました。ただし、このIPCパターンにはいくつかの代替アプローチが存在します。

可能であれば、レガシーなアプローチは避けてください。

可能な限りipcRenderer.invokeの使用を推奨します。以下の双方向レンダラーからメインへのパターンは、歴史的経緯からドキュメントに残されています。

情報

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

ipcRenderer.sendの使用

単方向通信に使用していたipcRenderer.send APIは、双方向通信にも利用できます。これは、Electron 7以前における非同期双方向IPC通信の推奨方法でした。

preload.js(プリロードスクリプト)
// You can also put expose this code to the renderer
// process with the `contextBridge` API
const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-reply', (_event, arg) => {
console.log(arg) // prints "pong" in the DevTools console
})
ipcRenderer.send('asynchronous-message', 'ping')
main.js(メインプロセス)
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
// works like `send`, but returning a message back
// to the renderer that sent the original message
event.reply('asynchronous-reply', 'pong')
})

このアプローチにはいくつかの欠点があります。

  • レンダラプロセスで応答を処理するために、2つ目のipcRenderer.onリスナーを設定する必要があります。invokeを使用すると、応答値は元のAPI呼び出しへのPromiseとして返されます。
  • 元のasynchronous-messageメッセージとasynchronous-replyメッセージを明らかにペアリングする方法がありません。これらのチャネルを介して頻繁にメッセージの送受信を行う場合、各呼び出しと応答を個別に追跡するための追加のアプリケーションコードを追加する必要があります。

ipcRenderer.sendSyncの使用

ipcRenderer.sendSync APIは、メインプロセスにメッセージを送信し、同期的に応答を待ちます。

main.js(メインプロセス)
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
preload.js(プリロードスクリプト)
// You can also put expose this code to the renderer
// process with the `contextBridge` API
const { ipcRenderer } = require('electron')

const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result) // prints "pong" in the DevTools console

このコードの構造は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(メインプロセス)
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.htmlpreload.jsのエントリポイントを読み込んでいることを確認してください!

2. プリロードによるipcRenderer.onの公開

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

preload.js(プリロードスクリプト)
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へのレンダラーのアクセスは、可能な限り制限してください。また、event.senderを介してipcRendererがリークされるため、ipcRenderer.onにコールバックを渡さないでください。必要な引数のみを使用してcallbackを呼び出すカスタムハンドラーを使用してください。

情報

この最小限の例では、コンテキストブリッジを介して公開する代わりに、プリロードスクリプトでipcRenderer.onを直接呼び出すことができます。

preload.js(プリロードスクリプト)
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の構築

すべてをまとめるために、値を表示するために使用する#counter要素を含むロードされた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>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(レンダラプロセス)
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コールバック内からメインプロセスに応答を送信できます。

前の例からのコードを少し変更して、これを示すことができます。レンダラプロセスでは、counter-valueチャネルを介してメインプロセスに応答を送信するための別のAPIを公開します。

preload.js(プリロードスクリプト)
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(レンダラプロセス)
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(メインプロセス)
// ...
ipcMain.on('counter-value', (_event, value) => {
console.log(value) // will print value to Node console
})
// ...

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

ipcMainipcRendererモジュールを使用して、Electronでレンダラプロセス間でメッセージを直接送信する方法はありません。これを実現するには、2つのオプションがあります。

  • レンダラー間のメッセージブローカーとしてメインプロセスを使用します。これには、あるレンダラーからメインプロセスにメッセージを送信し、メインプロセスがそのメッセージを別のレンダラーに転送することが含まれます。
  • メインプロセスから両方のレンダラーにMessagePortを渡します。これにより、初期設定後、レンダラー間で直接通信が可能になります。

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

ElectronのIPC実装では、HTML標準の構造化クローンアルゴリズムを使用してプロセス間で渡されるオブジェクトをシリアライズするため、IPCチャネルを介して渡すことができるオブジェクトの種類は限られています。

特に、DOMオブジェクト(例:ElementLocationDOMMatrix)、C++クラスによって裏付けられたNode.jsオブジェクト(例:process.envStreamの一部のメンバー)、およびC++クラスによって裏付けられたElectronオブジェクト(例:WebContentsBrowserWindowWebFrame)は、構造化クローンではシリアライズできません。