メインコンテンツへ飛ぶ

プリロードスクリプトの利用

学習目標

このチュートリアルでは、プリロードスクリプトとは何か、そしてプリロードスクリプトを使用して特権 API を安全にレンダラープロセスへ公開する方法について学びます。 また Electron のプロセス間通信 (IPC) モジュールを用いた、メインプロセスとレンダラープロセス間の通信方法についても学びます。

プリロードスクリプトとは何でしょう?

Electron のメインプロセスは、オペレーティングシステムにフルアクセス可能な Node.js 環境です。 Electron のモジュール に加えて、Node.js 組み込み や npm 経由でインストールしたパッケージにもアクセス可能です。 一方、レンダラープロセスはウェブページを実行するもので、セキュリティ上の理由からデフォルトでは Node.js を実行しません。

Electron の異なる種類のプロセスをブリッジするために、プリロード と呼ばれる特別なスクリプトを使用する必要があります。

プリロードスクリプトを使ってレンダラーを拡張する

BrowserWindow のプリロードスクリプトは、HTML DOM に加えて Node.js と Electron API の制限付きサブセットの両方にアクセスできるコンテキストで実行されます。

プリロードスクリプトのサンドボックス化

Electron 20 以降、プリロードスクリプトはデフォルトで サンドボックス化 され、完全な Node.js 環境にはアクセスできなくなりました。 実際には、限られた API のあつまりのみにアクセスできる、ポリフィルされた require 関数があるということです。

利用可能な API詳細
Electron モジュールレンダラープロセスのモジュール
Node.js モジュールevents, timers, url
ポリフィルされたグローバルBuffer, process, clearImmediate, setImmediate

詳しい情報については、プロセスのサンドボックス化 のガイドをご確認ください。

プリロードスクリプトは、Chrome 拡張機能の コンテンツスクリプト と同様の、レンダラーがウェブページを読み込む前に注入されるスクリプトです。 特権アクセスを必要とする機能をレンダラーに追加するために、global オブジェクトを contextBridge API で定義できます。

このコンセプトを実証するために、アプリの Chrome、Node、Electron のバージョンをレンダラーへ公開するプリロードスクリプトを作成します。

新しく preload.js スクリプトを追加し、ここでは Electron の process.versions オブジェクトから選んだプロパティをレンダラープロセスでの versions グローバル変数へと公開します。

preload.js
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// 関数だけでなく変数も公開できます
})

このスクリプトをレンダラープロセスへアタッチするには、BrowserWindow のコンストラクタの webPreferences.preload オプションにそのパスを渡します。

main.js
const { app, BrowserWindow } = require('electron')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()
})
info

ここでは、以下 2 つの Node.js のコンセプトが使われています。

  • __dirname 文字列は、現在実行中のスクリプトのパス (ここではプロジェクトのルートフォルダ) を指しています。
  • path.join API は、複数のパス断片を結合し、すべてのプラットフォームで動作する結合パス文字列を作成します。

これによりレンダラーは versions グローバルにアクセスできますので、その情報をウインドウに表示させましょう。 この変数は window.versions または単に versions でアクセスできます。 renderer.js スクリプトを作成し、これで document.getElementById DOM API を使用して id プロパティが info である HTML 要素の表示テキストを置換します。

renderer.js
const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`

そして index.html は、id プロパティが info である要素を新規追加し、renderer.js スクリプトをアタッチするように変更します。

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
<p id="info"></p>
</body>
<script src="./renderer.js"></script>
</html>

上記のステップを踏むと、アプリは以下のような外観になるでしょう。

Electron アプリの表示: This app is using Chrome (v102.0.5005.63), Node.js (v16.14.2), and Electron (v19.0.3)

そしてコードは以下のようになります。

const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}

app.whenReady().then(() => {
createWindow()

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

プロセス間通信

前述したように、Electron のメインプロセスとレンダラープロセスはそれぞれ別の責務があり、互換性はありません。 つまり、レンダラープロセスから Node.js の API に直接アクセスすることも、メインプロセスから HTML の Document Object Model (DOM) にアクセスすることもできません。

この問題を解決するには、プロセス間通信 (IPC) を行うための Electron の ipcMainipcRenderer モジュールを利用します。 ウェブページからメインプロセスへメッセージを送信するには、ipcMain.handle でメインプロセスのハンドラをセットアップし、ipcRenderer.invoke を呼び出す関数をプリロードスクリプトで公開することでそのハンドラをトリガします。

説明のために、メインプロセスから文字列を返す ping() というグローバル関数をレンダラーに追加します。

まず、プリロードスクリプトで invoke を呼び出すものを設定します。

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

contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping')
// 関数だけでなく、変数も公開できます
})
IPC セキュリティ

注意として、ここでは ipcRenderer モジュールを直接コンテキストブリッジで公開するのではなく、ipcRenderer.invoke('ping') の呼び出しをラップするヘルパー関数を用意しています。 プリロードを介して ipcRenderer モジュール全体を直接公開しようという考えは取りやめてください。 これによりレンダラーがメインプロセスへ任意の IPC メッセージを送信できるようになり、悪意あるコードの強力な攻撃ベクトルとなります。

次に、メインプロセスに handle リスナーをセットアップします。 HTML ファイルを読み込む にこれを行うことで、レンダラーが invoke 呼び出しを送信する前にハンドラーの準備完了が保証されます。

main.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(() => {
ipcMain.handle('ping', () => 'pong')
createWindow()
})

送信者と受信者の設定ができれば、先ほど定義した 'ping' チャンネルを通じて、レンダラーからメインプロセスへメッセージを送信できるようになります。

renderer.js
const func = async () => {
const response = await window.versions.ping()
console.log(response) // 'pong' と出力
}

func()
info

ipcRendereripcMain モジュールの使用に関するより詳細な説明については、プロセス間通信 の完全なガイドをご覧ください。

概要

プリロードスクリプトに書かれたコードは、ブラウザウインドウでウェブページが読み込まれるよりも前に実行されます。 これは DOM API と Node.js 環境の両方にアクセスでき、contextBridge API を介して特権 API をレンダラーへ公開するためによく使われます。

メインプロセスとレンダラープロセスの責務は大きく異なります。そのため Electron アプリでは、プリロードスクリプトでプロセス間通信 (IPC) のインターフェースを設定し、2 種類のプロセス間で任意のメッセージを受け渡すようにします。

次章のチュートリアルでは、アプリに機能を追加するためのリソースを紹介して、アプリをユーザーに頒布する方法を学びます。