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

2023-02-16 17:14 更新

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

在這部分的教程中,你將會了解什么是預(yù)加載腳本,并且學(xué)會如何使用預(yù)加載腳本來安全地將特權(quán) API 暴露至渲染進(jìn)程中。 不僅如此,你還會學(xué)到如何使用 Electron 的進(jìn)程間通信 (IPC) 模組來讓主進(jìn)程與渲染進(jìn)程間進(jìn)行通信。

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

Electron 的主進(jìn)程是一個擁有著完全操作系統(tǒng)訪問權(quán)限的 Node.js 環(huán)境。 除了 Electron 模組 之外,你也可以使用 Node.js 內(nèi)置模塊 和所有通過 npm 安裝的軟件包。 另一方面,出于安全原因,渲染進(jìn)程默認(rèn)跑在網(wǎng)頁頁面上,而并非 Node.js里。

為了將 Electron 的不同類型的進(jìn)程橋接在一起,我們需要使用被稱為 預(yù)加載 的特殊腳本。

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

BrowserWindow 的預(yù)加載腳本運行在具有 HTML DOM 和 Node.js、Electron API 的有限子集訪問權(quán)限的環(huán)境中。

::: info 預(yù)加載腳本沙盒化

從 Electron 20 開始,預(yù)加載腳本默認(rèn) 沙盒化 ,不再擁有完整 Node.js 環(huán)境的訪問權(quán)。 實際上,這意味著你只擁有一個 polyfilled 的 require 函數(shù),這個函數(shù)只能訪問一組有限的 API。

可用的 API 詳細(xì)信息
Electron 模塊 渲染進(jìn)程模塊
Node.js 模塊 events、timers、url
Polyfilled 的全局模塊 Buffer、process、clearImmediatesetImmediate

有關(guān)詳細(xì)信息,請閱讀 進(jìn)程沙盒化 教程。

:::

預(yù)加載腳本像 Chrome 擴(kuò)展的 內(nèi)容腳本(Content Script)一樣,會在渲染器的網(wǎng)頁加載之前注入。 如果你想向渲染器加入需要特殊權(quán)限的功能,你可以通過 contextBridge 接口定義 全局對象

為了演示這一概念,你將會創(chuàng)建一個將應(yīng)用中的 Chrome、Node、Electron 版本號暴露至渲染器的預(yù)加載腳本

新建一個 preload.js 文件。該腳本通過 versions 這一全局變量,將 Electron 的 process.versions 對象暴露給渲染器。

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

這里使用了兩個Node.js概念:

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

現(xiàn)在渲染器能夠全局訪問 versions 了,讓我們快快將里邊的信息顯示在窗口中。 這個變量不僅可以通過 window.versions 訪問,也可以很簡單地使用 versions 來訪問。 新建一個 renderer.js 腳本, 這個腳本使用 document.getElementById DOM 接口來替換 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()})`

然后請修改你的 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>來自 Electron 渲染器的問好!</title>
  </head>
  <body>
    <h1>來自 Electron 渲染器的問好!</h1>
    <p></p>
    <p id="info"></p>
  </body>
  <script src="./renderer.js"></script>
</html>

做完這幾步之后,你的應(yīng)用應(yīng)該長這樣:


你的代碼應(yī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)程有著清楚的分工并且不可互換。 這代表著無論是從渲染進(jìn)程直接訪問 Node.js 接口,亦或者是從主進(jìn)程訪問 HTML 文檔對象模型 (DOM),都是不可能的。

解決這一問題的方法是使用進(jìn)程間通信 (IPC)??梢允褂?Electron 的 ipcMain 模塊和 ipcRenderer 模塊來進(jìn)行進(jìn)程間通信。 為了從你的網(wǎng)頁向主進(jìn)程發(fā)送消息,你可以使用 ipcMain.handle 設(shè)置一個主進(jìn)程處理程序(handler),然后在預(yù)處理腳本中暴露一個被稱為 ipcRenderer.invoke 的函數(shù)來觸發(fā)該處理程序(handler)。

我們將向渲染器添加一個叫做 ping() 的全局函數(shù)來演示這一點。這個函數(shù)將返回一個從主進(jìn)程翻山越嶺而來的字符串。

首先,在預(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 安全

可以注意到我們使用了一個輔助函數(shù)來包裹 ipcRenderer.invoke('ping') 調(diào)用,而并非直接通過 context bridge 暴露 ipcRenderer 模塊。 你永遠(yuǎn)都不會想要通過預(yù)加載直接暴露整個 ipcRenderer 模塊。 這將使得你的渲染器能夠直接向主進(jìn)程發(fā)送任意的 IPC 信息,會使得其成為惡意代碼最強(qiáng)有力的攻擊媒介。

然后,在主進(jìn)程中設(shè)置你的 handle 監(jiān)聽器。 我們在 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)在你可以將信息通過剛剛定義的 'ping' 通道從渲染器發(fā)送至主進(jìn)程當(dāng)中。

const func = async () => {
  const response = await window.versions.ping()
  console.log(response) // 打印 'pong'
}

func()

INFO

如欲了解使用 ipcRenderer 模塊和 ipcMain 模塊的詳細(xì)說明,請訪問完整的 進(jìn)程間通信 指南。

摘要

預(yù)加載腳本包含在瀏覽器窗口加載網(wǎng)頁之前運行的代碼。 其可訪問 DOM 接口和 Node.js 環(huán)境,并且經(jīng)常在其中使用 contextBridge 接口將特權(quán)接口暴露給渲染器。

由于主進(jìn)程和渲染進(jìn)程有著完全不同的分工,Electron 應(yīng)用通常使用預(yù)加載腳本來設(shè)置進(jìn)程間通信 (IPC) 接口以在兩種進(jìn)程之間傳輸任意信息。

在下一部分的教程中,我們將向你展示如何向你的應(yīng)用中添加更多的功能,之后將向你傳授如何向用戶分發(fā)你的應(yīng)用。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號