Electron 使用預(yù)加載腳本

2023-02-16 17:14 更新

學(xué)習(xí)目標(biāo)?

在這部分的教程中,你將會(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)行通信。

什么是預(yù)加載腳本?

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ù)加載 的特殊腳本。

使用預(yù)加載腳本來(lái)增強(qiáng)渲染器

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、clearImmediatesetImmediate

有關(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概念:

  • __dirname 字符串指向當(dāng)前正在執(zhí)行腳本的路徑 (在本例中,它指向你的項(xiàng)目的根文件夾)。
  • path.join API 將多個(gè)路徑聯(lián)結(jié)在一起,創(chuàng)建一個(gè)跨平臺(tái)的路徑字符串。

現(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 
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();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
const { contextBridge } = require('electron');

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${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`;

DOCS/FIDDLES/TUTORIAL-PRELOAD (22.0.2)

Open in Fiddle

在進(jìn)程之間通信

我們之前提到,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)用。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)