在這部分的教程中,你將會(huì)了解什么是預(yù)加載腳本,并且學(xué)會(huì)如何使用預(yù)加載腳本來(lái)安全地將特權(quán) API 暴露至渲染進(jìn)程中。 不僅如此,你還會(huì)學(xué)到如何使用 Electron 的進(jìn)程間通信 (IPC) 模組來(lái)讓主進(jìn)程與渲染進(jìn)程間進(jìn)行通信。
Electron 的主進(jìn)程是一個(gè)擁有著完全操作系統(tǒng)訪問(wèn)權(quán)限的 Node.js 環(huán)境。 除了 Electron 模組 之外,你也可以使用 Node.js 內(nèi)置模塊 和所有通過(guò) npm 安裝的軟件包。 另一方面,出于安全原因,渲染進(jìn)程默認(rèn)跑在網(wǎng)頁(yè)頁(yè)面上,而并非 Node.js里。
為了將 Electron 的不同類型的進(jìn)程橋接在一起,我們需要使用被稱為 預(yù)加載 的特殊腳本。
BrowserWindow 的預(yù)加載腳本運(yùn)行在具有 HTML DOM 和 Node.js、Electron API 的有限子集訪問(wèn)權(quán)限的環(huán)境中。
::: info 預(yù)加載腳本沙盒化
從 Electron 20 開始,預(yù)加載腳本默認(rèn) 沙盒化 ,不再擁有完整 Node.js 環(huán)境的訪問(wèn)權(quán)。 實(shí)際上,這意味著你只擁有一個(gè) polyfilled 的 require
函數(shù),這個(gè)函數(shù)只能訪問(wèn)一組有限的 API。
可用的 API | 詳細(xì)信息 |
---|---|
Electron 模塊 | 渲染進(jìn)程模塊 |
Node.js 模塊 | events 、timers 、url
|
Polyfilled 的全局模塊 | Buffer 、process 、clearImmediate 、setImmediate
|
有關(guān)詳細(xì)信息,請(qǐng)閱讀 進(jìn)程沙盒化 教程。
:::
預(yù)加載腳本像 Chrome 擴(kuò)展的 內(nèi)容腳本(Content Script)一樣,會(huì)在渲染器的網(wǎng)頁(yè)加載之前注入。 如果你想向渲染器加入需要特殊權(quán)限的功能,你可以通過(guò) contextBridge 接口定義 全局對(duì)象。
為了演示這一概念,你將會(huì)創(chuàng)建一個(gè)將應(yīng)用中的 Chrome、Node、Electron 版本號(hào)暴露至渲染器的預(yù)加載腳本
新建一個(gè) preload.js
文件。該腳本通過(guò) versions
這一全局變量,將 Electron 的 process.versions
對(duì)象暴露給渲染器。
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
// 能暴露的不僅僅是函數(shù),我們還可以暴露變量
})
為了將腳本附在渲染進(jìn)程上,在 BrowserWindow 構(gòu)造器中使用 webPreferences.preload
傳入腳本的路徑。
const { app, BrowserWindow } = require('electron')
const path = require('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()
})
INFO
這里使用了兩個(gè)Node.js概念:
現(xiàn)在渲染器能夠全局訪問(wèn) versions
了,讓我們快快將里邊的信息顯示在窗口中。 這個(gè)變量不僅可以通過(guò) window.versions
訪問(wèn),也可以很簡(jiǎn)單地使用 versions
來(lái)訪問(wèn)。 新建一個(gè) renderer.js
腳本, 這個(gè)腳本使用 document.getElementById
DOM 接口來(lái)替換 id
屬性為 info
的 HTML 元素顯示文本。
const information = document.getElementById('info')
information.innerText = `本應(yīng)用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})`
然后請(qǐng)修改你的 index.html
文件。加上一個(gè) 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>來(lái)自 Electron 渲染器的問(wèn)好!</title>
</head>
<body>
<h1>來(lái)自 Electron 渲染器的問(wèn)好!</h1>
<p></p>
<p id="info"></p>
</body>
<script src="./renderer.js"></script>
</html>
做完這幾步之后,你的應(yīng)用應(yīng)該長(zhǎng)這樣:
你的代碼應(yīng)該長(zhǎng)這樣:
main.js | preload.js | index.html | renderer.js |
|
|
|
|
DOCS/FIDDLES/TUTORIAL-PRELOAD (22.0.2)
我們之前提到,Electron 的主進(jìn)程和渲染進(jìn)程有著清楚的分工并且不可互換。 這代表著無(wú)論是從渲染進(jìn)程直接訪問(wèn) Node.js 接口,亦或者是從主進(jìn)程訪問(wèn) HTML 文檔對(duì)象模型 (DOM),都是不可能的。
解決這一問(wèn)題的方法是使用進(jìn)程間通信 (IPC)??梢允褂?Electron 的 ipcMain
模塊和 ipcRenderer
模塊來(lái)進(jìn)行進(jìn)程間通信。 為了從你的網(wǎng)頁(yè)向主進(jìn)程發(fā)送消息,你可以使用 ipcMain.handle
設(shè)置一個(gè)主進(jìn)程處理程序(handler),然后在預(yù)處理腳本中暴露一個(gè)被稱為 ipcRenderer.invoke
的函數(shù)來(lái)觸發(fā)該處理程序(handler)。
我們將向渲染器添加一個(gè)叫做 ping()
的全局函數(shù)來(lái)演示這一點(diǎn)。這個(gè)函數(shù)將返回一個(gè)從主進(jìn)程翻山越嶺而來(lái)的字符串。
首先,在預(yù)處理腳本中設(shè)置 invoke
調(diào)用:
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
ping: () => ipcRenderer.invoke('ping'),
// 能暴露的不僅僅是函數(shù),我們還可以暴露變量
})
IPC 安全
可以注意到我們使用了一個(gè)輔助函數(shù)來(lái)包裹
ipcRenderer.invoke('ping')
調(diào)用,而并非直接通過(guò) context bridge 暴露ipcRenderer
模塊。 你永遠(yuǎn)都不會(huì)想要通過(guò)預(yù)加載直接暴露整個(gè)ipcRenderer
模塊。 這將使得你的渲染器能夠直接向主進(jìn)程發(fā)送任意的 IPC 信息,會(huì)使得其成為惡意代碼最強(qiáng)有力的攻擊媒介。
然后,在主進(jìn)程中設(shè)置你的 handle
監(jiān)聽(tīng)器。 我們?cè)?HTML 文件加載之前完成了這些,所以才能保證在你從渲染器發(fā)送 invoke
調(diào)用之前處理程序能夠準(zhǔn)備就緒。
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
ipcMain.handle('ping', () => 'pong')
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
將發(fā)送器與接收器設(shè)置完成之后,現(xiàn)在你可以將信息通過(guò)剛剛定義的 'ping'
通道從渲染器發(fā)送至主進(jìn)程當(dāng)中。
const func = async () => {
const response = await window.versions.ping()
console.log(response) // 打印 'pong'
}
func()
INFO
如欲了解使用
ipcRenderer
模塊和ipcMain
模塊的詳細(xì)說(shuō)明,請(qǐng)?jiān)L問(wèn)完整的 進(jìn)程間通信 指南。
預(yù)加載腳本包含在瀏覽器窗口加載網(wǎng)頁(yè)之前運(yùn)行的代碼。 其可訪問(wèn) DOM 接口和 Node.js 環(huán)境,并且經(jīng)常在其中使用 contextBridge
接口將特權(quán)接口暴露給渲染器。
由于主進(jìn)程和渲染進(jìn)程有著完全不同的分工,Electron 應(yīng)用通常使用預(yù)加載腳本來(lái)設(shè)置進(jìn)程間通信 (IPC) 接口以在兩種進(jìn)程之間傳輸任意信息。
在下一部分的教程中,我們將向你展示如何向你的應(yīng)用中添加更多的功能,之后將向你傳授如何向用戶分發(fā)你的應(yīng)用。
更多建議: