プロセス間通信
プロセス間通信(IPC)は、Electronで機能豊富なデスクトップアプリケーションを構築する上で重要な部分です。Electronのプロセスモデルではメインプロセスとレンダラプロセスはそれぞれ異なる役割を持つため、IPCはUIからネイティブAPIを呼び出す、またはネイティブメニューからWebコンテンツの変更をトリガーするなど、多くの一般的なタスクを実行する唯一の方法です。
IPCチャネル
Electronでは、プロセスはipcMain
とipcRenderer
モジュールを使用して、開発者が定義した「チャネル」を介してメッセージをやり取りします。これらのチャネルは**任意**(自由に名前を付けることができます)であり、**双方向**(同じチャネル名を両方のモジュールで使用できます)。
このガイドでは、アプリコードの参照として使用できる具体的な例を用いて、いくつかの基本的なIPCパターンについて説明します。
コンテキスト分離プロセスの理解
実装の詳細に移る前に、コンテキスト分離されたレンダラプロセスでNode.jsとElectronモジュールをインポートするためにプリロードスクリプトを使用するという概念に精通している必要があります。
- Electronのプロセスモデルの完全な概要については、プロセスモデルのドキュメントを参照してください。
contextBridge
モジュールを使用してプリロードスクリプトからAPIを公開する方法の概要については、コンテキスト分離チュートリアルを参照してください。
パターン1:レンダラからメイン(一方向)
レンダラプロセスからメインプロセスに一方向のIPCメッセージを送信するには、ipcRenderer.send
APIを使用してメッセージを送信し、次にipcMain.on
APIで受信します。
通常、このパターンはWebコンテンツからメインプロセスの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()
})
// ...
上記の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>
これらの要素をインタラクティブにするために、プリロードスクリプトから公開されたwindow.electronAPI
機能を利用するインポートされた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.invoke
と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')
})
セキュリティ上の理由から、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は、Electron 7で、レンダラプロセスからの双方向IPCに対処するための開発者フレンドリーな方法として追加されました。ただし、このIPCパターンにはいくつかの代替アプローチが存在します。
可能な限りipcRenderer.invoke
の使用を推奨します。以下の双方向レンダラーからメインへのパターンは、歴史的経緯からドキュメントに残されています。
以下の例では、コードサンプルを小さく保つために、プリロードスクリプトから直接ipcRenderer
を呼び出しています。
ipcRenderer.send
の使用
単方向通信に使用していたipcRenderer.send
APIは、双方向通信にも利用できます。これは、Electron 7以前における非同期双方向IPC通信の推奨方法でした。
// 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')
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は、メインプロセスにメッセージを送信し、同期的に応答を待ちます。
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping" in the Node console
event.returnValue = 'pong'
})
// 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
メソッドが含まれています。
このパターンを示すために、ネイティブオペレーティングシステムのメニューによって制御される数値カウンターを作成します。
このデモでは、メインプロセス、レンダラプロセス、およびプリロードスクリプトにコードを追加する必要があります。完全なコードを以下に示しますが、各ファイルは次のセクションで個別に説明します。
- 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()
リスナー関数にアクセスできるようになります。
セキュリティ上の理由から、ipcRenderer.on
API全体を直接公開しません。Electron APIへのレンダラーのアクセスは、可能な限り制限してください。また、event.sender
を介してipcRenderer
がリークされるため、ipcRenderer.on
にコールバックを渡さないでください。必要な引数のみを使用して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の構築
すべてをまとめるために、値を表示するために使用する#counter
要素を含むロードされた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操作を追加します。
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を公開します。
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) // will print value to Node console
})
// ...
パターン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
)は、構造化クローンではシリアライズできません。