プロセス間通信
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/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>
これらの要素を動作させるために、インポートされる 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/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 へのアクセスをできるだけ制限するようにしてください。