使用預載腳本
學習目標
在本教學的這部分,您將學習預載腳本是什麼,以及如何使用它來安全地將特權 API 暴露到渲染器程序中。您還將學習如何使用 Electron 的跨程序通訊 (IPC) 模組在主程序和渲染器程序之間進行通訊。
什麼是預載腳本?
Electron 的主程序是一個 Node.js 環境,具有完整的作業系統存取權限。除了 Electron 模組 之外,您還可以存取 Node.js 內建函式庫,以及透過 npm 安裝的任何套件。另一方面,渲染器程序執行網頁,並且基於安全考量,預設情況下不執行 Node.js。
為了橋接 Electron 的不同程序類型,我們需要使用一種稱為預載的特殊腳本。
使用預載腳本擴增渲染器
BrowserWindow 的預載腳本在一個可以存取 HTML DOM 和 Node.js 與 Electron API 的有限子集的上下文中執行。
從 Electron 20 開始,預載腳本預設為沙箱化,並且不再具有完整 Node.js 環境的存取權限。實際上,這表示您有一個 polyfilled 的 require
函式,該函式僅可存取一組有限的 API。
可用的 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 來替換 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 文件物件模型 (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) 介面,以便在兩種程序之間傳遞任意訊息。
在本教學的下一部分,我們將向您展示有關為應用程式新增更多功能的資源,然後教您如何將應用程式發布給使用者。