使用預加載腳本
學習目標
在本教學的這一部分,您將學習什麼是預加載腳本,以及如何使用預加載腳本安全地將特權 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 模組 | events 、timers 、url |
Polyfilled 全域變數 | Buffer 、process 、clearImmediate 、setImmediate |
如需更多資訊,請查看進程沙箱化指南。
預加載腳本會在網頁載入渲染器之前注入,類似於 Chrome 擴充功能的內容腳本。若要將需要特權存取權的功能新增至您的渲染器,您可以使用 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
// we can also expose variables, not just functions
})
若要將此腳本附加到您的渲染器進程,請將其路徑傳遞給 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 來取代 HTML 元素的顯示文字,並將 info
作為其 id
屬性。
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
腳本
<!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 文件物件模型 (DOM)。
這個問題的解決方案是使用 Electron 的 ipcMain
和 ipcRenderer
模組進行跨進程通訊 (IPC)。若要從您的網頁將訊息傳送至主進程,您可以使用 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')
// we can also expose variables, not just functions
})
請注意,我們如何將 ipcRenderer.invoke('ping')
呼叫包裝在輔助函式中,而不是透過 context bridge 直接公開 ipcRenderer
模組。您絕對不希望透過預加載直接公開整個 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) // prints out 'pong'
}
func()
如需更多關於使用 ipcRenderer
和 ipcMain
模組的深入說明,請查看完整的跨進程通訊指南。
總結
預加載腳本包含在您的網頁載入瀏覽器視窗之前執行的程式碼。它可以存取 DOM API 和 Node.js 環境,並且通常用於透過 contextBridge
API 將特權 API 公開到渲染器。
由於主進程和渲染器進程具有非常不同的責任,因此 Electron 應用程式通常會使用預加載腳本來設定跨進程通訊 (IPC) 介面,以便在兩種進程之間傳遞任意訊息。
在本教學的下一部分中,我們將向您展示有關為您的應用程式新增更多功能的資源,然後教您如何將應用程式發佈給使用者。