プリロードスクリプトの利用
これは Electron チュートリアルの 3 章 です。
学習目標
このチュートリアルでは、プリロードスクリプトとは何か、そしてプリロードスクリプトを使用して特権 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
グローバル変数へと公開します。
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// 関数だけでなく変数も公開できます
})
このスクリプトをレンダラープロセスへアタッチするには、BrowserWindow のコンストラクタの webPreferences.preload
オプションにそのパスを渡します。
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()
})
これによりレンダラーは versions
グローバルにアクセスできますので、その情報をウインドウに表示させましょう。 この変数は window.versions
または単に versions
でアクセスできます。 renderer.js
スクリプトを作成し、これで document.getElementById
DOM API を使用して id
プロパティが info
である HTML 要素の表示テキストを置換します。
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
スクリプトをアタッチするように変更します。
<!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>
上記のステップを踏むと、アプリは以下のような外観になるでしょう。
そしてコードは以下のようになります。
- main.js
- preload.js
- index.html
- renderer.js
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()
}
})
const { contextBridge } = require('electron/renderer')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
})
<!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>
const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${window.versions.chrome()}), Node.js (v${window.versions.node()}), and Electron (v${window.versions.electron()})`
プロセス間通信
前述したように、Electron のメインプロセスとレンダラープロセスはそれぞれ別の責務があり、互換性はありません。 つまり、レンダラープロセスから Node.js の API に直接アクセスすることも、メインプロセスから HTML の Document Object Model (DOM) にアクセスすることもできません。
この問題を解決するには、プロセス間通信 (IPC) を行うための Electron の ipcMain
と ipcRenderer
モジュールを利用します。 ウェブページからメインプロセスへメッセージを送信するには、ipcMain.handle
でメインプロセスのハンドラをセットアップし、ipcRenderer.invoke
を呼び出す関数をプリロードスクリプトで公開することでそのハンドラをトリガします。
説明のために、メインプロセスから文字列を返す ping()
というグローバル関数をレンダラーに追加します。
まず、プリロードスクリプトで invoke
を呼び出すものを設定します。
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping')
// 関数だけでなく、変数も公開できます
})
注意として、ここでは ipcRenderer
モジュールを直接コンテキストブリッジで公開するのではなく、ipcRenderer.invoke('ping')
の呼び出しをラップするヘルパー関数を用意しています。 プリロードを介して ipcRenderer
モジュール全体を直接公開しようという考えは取りやめてください。 これによりレンダラーがメインプロセスへ任意の IPC メッセージを送信できるようになり、悪意あるコードの強力な攻撃ベクトルとなります。
次に、メインプロセスに handle
リスナーをセットアップします。 HTML ファイルを読み込む 前 にこれを行うことで、レンダラーが invoke
呼び出しを送信する前にハンドラーの準備完了が保証されます。
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'
チャンネルを通じて、レンダラーからメインプロセスへメッセージを送信できるようになります。
const func = async () => {
const response = await window.versions.ping()
console.log(response) // 'pong' と出力
}
func()
ipcRenderer
と ipcMain
モジュールの使用に関するより詳細な説明については、プロセス間通信 の完全なガイドをご覧ください。
概要
プリロードスクリプトに書かれたコードは、ブラウザウインドウでウェブページが読み込まれるよりも前に実行されます。 これは DOM API と Node.js 環境の両方にアクセスでき、contextBridge
API を介して特権 API をレンダラーへ公開するためによく使われ ます。
メインプロセスとレンダラープロセスの責務は大きく異なります。そのため Electron アプリでは、プリロードスクリプトでプロセス間通信 (IPC) のインターフェースを設定し、2 種類のプロセス間で任意のメッセージを受け渡すようにします。
次章のチュートリアルでは、アプリに機能を追加するためのリソースを紹介して、アプリをユーザーに頒布する方法を学びます。