本文へスキップ

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

チュートリアルに従ってください

学習目標

このチュートリアルでは、プリロードスクリプトとは何か、そして特権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モジュールeventstimersurl
ポリフィルされたグローバルオブジェクトBufferprocessclearImmediatesetImmediate

詳細については、プロセスサンドボックス化ガイドをご覧ください。

プリロードスクリプトは、Chrome拡張機能のコンテンツスクリプトと同様に、レンダラーにウェブページが読み込まれる前に挿入されます。特権アクセスを必要とする機能をレンダラーに追加するには、グローバルオブジェクトをcontextBridgeAPIを通じて定義できます。

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

Electronの`process.versions`オブジェクトの選択されたプロパティをレンダラープロセスで`versions`グローバル変数に公開する新しい`preload.js`スクリプトを追加します。

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

contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// we can also expose variables, not just functions
})

このスクリプトをレンダラープロセスにアタッチするには、そのパスを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()
})
情報

ここで使用されているNode.jsの概念が2つあります。

  • __dirname文字列は、現在実行中のスクリプトのパス(この場合はプロジェクトのルートフォルダ)を指します。
  • path.joinAPIは複数のパスセグメントを結合し、すべてのプラットフォームで動作する組み合わせパス文字列を作成します。

この時点で、レンダラーは`versions`グローバルにアクセスできるため、ウィンドウにその情報を表示してみましょう。この変数には`window.versions`または単に`versions`を使用してアクセスできます。`id`プロパティとして`info`を持つHTML要素の表示テキストを置き換えるためにdocument.getElementByIdDOM APIを使用する`renderer.js`スクリプトを作成します。

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()})`

次に、`id`プロパティとして`info`を持つ新しい要素を追加し、`renderer.js`スクリプトをアタッチすることによって`index.html`を修正します。

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 app showing 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の`ipcMain`と`ipcRenderer`モジュールを使用することです。ウェブページからメインプロセスにメッセージを送信するには、`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')
// we can also expose variables, not just functions
})
IPCセキュリティ

`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) // prints out 'pong'
}

func()
情報

`ipcRenderer`と`ipcMain`モジュールの使用方法の詳細については、完全なプロセス間通信ガイドをご覧ください。

まとめ

プリロードスクリプトには、ウェブページがブラウザウィンドウに読み込まれる前に実行されるコードが含まれています。DOM APIとNode.js環境の両方にアクセスでき、多くの場合、contextBridge APIを介してレンダラーに特権APIを公開するために使用されます。

メインプロセスとレンダラープロセスは非常に異なる役割を担っているため、Electronアプリでは、プリロードスクリプトを使用してプロセス間通信(IPC)インターフェースを設定し、2種類のプロセス間で任意のメッセージをやり取りすることがよくあります。

チュートリアルの次のセクションでは、アプリの機能追加に関するリソースを紹介し、アプリのユーザーへの配布方法を説明します。