跳至主要內容

使用預加載腳本

學習目標

在本教學的這一部分,您將學習什麼是預加載腳本,以及如何使用預加載腳本安全地將特權 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 集的 polyfilled require 函式。

可用的 API詳細資訊
Electron 模組渲染器進程模組
Node.js 模組eventstimersurl
Polyfilled 全域變數BufferprocessclearImmediatesetImmediate

如需更多資訊,請查看進程沙箱化指南。

預加載腳本會在網頁載入渲染器之前注入,類似於 Chrome 擴充功能的內容腳本。若要將需要特權存取權的功能新增至您的渲染器,您可以使用 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
// 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 概念

  • __dirname 字串指向目前執行腳本的路徑(在此案例中,為您的專案根目錄)。
  • path.join API 會將多個路徑片段連結在一起,建立一個適用於所有平台的組合路徑字串。

此時,渲染器可以存取 versions 全域變數,因此讓我們將該資訊顯示在視窗中。此變數可以透過 window.versions 或直接使用 versions 存取。建立一個 renderer.js 腳本,使用 document.getElementById DOM API 來取代 HTML 元素的顯示文字,並將 info 作為其 id 屬性。

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,新增一個以 info 作為其 id 屬性的新元素,並附加您的 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 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 文件物件模型 (DOM)。

這個問題的解決方案是使用 Electron 的 ipcMainipcRenderer 模組進行跨進程通訊 (IPC)。若要從您的網頁將訊息傳送至主進程,您可以使用 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.invoke('ping') 呼叫包裝在輔助函式中,而不是透過 context bridge 直接公開 ipcRenderer 模組。您絕對不希望透過預加載直接公開整個 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()
資訊

如需更多關於使用 ipcRendereripcMain 模組的深入說明,請查看完整的跨進程通訊指南。

總結

預加載腳本包含在您的網頁載入瀏覽器視窗之前執行的程式碼。它可以存取 DOM API 和 Node.js 環境,並且通常用於透過 contextBridge API 將特權 API 公開到渲染器。

由於主進程和渲染器進程具有非常不同的責任,因此 Electron 應用程式通常會使用預加載腳本來設定跨進程通訊 (IPC) 介面,以便在兩種進程之間傳遞任意訊息。

在本教學的下一部分中,我們將向您展示有關為您的應用程式新增更多功能的資源,然後教您如何將應用程式發佈給使用者。