前陣子將排課系統的一些功能,提供給 solar 編輯器使用,solar 是基於互動課件編輯器 Cocos ICE 進行二次定製和個性化開發的課件製作系統,其底層是 Cocos Creator。而 Cocos Creator 是基於 Electron 進行開發的,所以學習了一些關於 Electron IPC 通信的相關知識,在這裡做一個總結。
文章的開始,先讓我們來了解下 Electron 是什麼。
1. 什麼是 Electron?Electron 官網只有一句簡單的話: 使用 JavaScript,HTML 和 CSS 構建跨平臺的桌面應用程式。簡單點講,就是有了 Electron,我們就可以用前端技術來寫 web 頁面,它可以轉化為一個桌面應用。
除此之前,Electron 還有其他的一些特性:
Electron 基於 Chromium 和 Node.js,類似一個小型的 Chrome 的瀏覽器,Electron 可以將你寫的 web 頁面(html 文件)本地化,然後打包成一個桌面應用程式。它同時還是跨平臺的,提供了許多功能與原生系統進行交互。
由於是基於 Chromium 的,所以寫 Electron,從此與前端兼容性無緣(真香)。Node.js版本也是固定的,無需考慮版本兼容問題(除非升級大版本)。
所以作為前端開發人員來說,想開發一款桌面端應該,Electron 是再適合不過了。
Electron 官網還舉了一些使用 Electron 進行開發的應用,大名鼎鼎的 VSCode 就是基於Electron。
3. Electron 快速上手
學啥不得先來個 hello world 呢?
3.1. 初始化工程創建 Electron 工程方式與前端項目別無二致,創建一個目錄,然後用 npm 初始化:
mkdir hello-electron && cd hello-electron
npm init -y
生成之後的 package.json 應該長這樣。
{
"name": "hello-electron",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
npm install --save-dev electron
安裝過程中,electron 模塊會去 Github 下載 預編譯二進位文件,然而下載速度大家都懂的,可能會出現下載失敗的情況。這裡可以使用 taobao 的鏡像源來下載。
npm config set electron_mirror http://npm.taobao.org/mirrors/electron/
npm config set electron_custom_dir "8.1.1"
為了更方便的啟動我們的程序,可以新增一條命令。
{
"scripts": {
"start": "electron ."
}
}
接下來,就讓我們愉快地編碼吧。
3.3. 創建 HTML在 Electron 中,每個窗口都可以加載本地或者遠程 URL,這裡我們先創建一個本地的 HTML 文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Electron <span id="electron-version"></span>
</body>
</html>
這裡你可能會注意到, span 標籤裡面是空文本,後面我們會動態插入 Electron 的版本。
3.4. 創建入口文件類似於 Node.js 啟動服務,Electron 啟動也需要一個入口文件,這裡我們創建 index.js 文件。在這個入口文件裡,需要去加載上面創建的 HTML 文件,那麼如何加載呢? Electron 提供了兩個模塊:
入口文件是 Node.js 環境,所以可以通過 CommonJS 模塊規範來導入 Electron 的模塊。同時添加一個 createWindow() 方法來將 index.html 加載進一個新的 BrowserWindow 實例。
// index.js
const { app, BrowserWindow } = require('electron');
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile('index.html')
}
那麼在什麼時候調用 createWindow 方法來打開窗口呢?在 Electron 中,只有在 app 模塊的 ready 事件被激發後才能創建瀏覽器窗口。可以通過使用 app.whenReady() API 來監聽此事件。
// index.js
app.whenReady().then(() => {
createWindow()
})
這樣一來就可以通過以下命令打開 Electron 應用程式了!
# 這裡會自動去找package.json的main欄位對應的文件運行
# 當然 你也可以將命令放進 script 裡面
npx electron .
運行完打開的應用程式如下圖所示。
3.5. 管理窗口的聲明周期雖然現在可以打開一個瀏覽器窗口,但還需要一些額外的模板代碼使其看起來更像是各平臺原生的。應用程式窗口在每個 OS 下有不同的行為,Electron 將在 app 中實現這些約定的責任交給開發者們。
可以使用 process.platform 屬性來為不同的作業系統做處理。
3.5.1. 關閉所有窗口時退出應用(Windows & Linux)在 Windows 和 Linux 上,關閉所有窗口通常會完全退出一個應用程式。 app 模塊可以監聽所有窗口關閉的事件 window-all-closed,在事件回調裡可以調用 app.quit() 退出應用。
// index.js
app.on('window-all-closed', function () {
// darwin 為 macOS
if (process.platform !== 'darwin') app.quit()
})
用過 macOS 的人應該都知道,一個應用沒有窗口打開的時候,也是可以繼續運行的,這時如果打開應用程式,就會打開新的窗口。 app 模塊可以監聽應用激活事件 activate,在事件回調裡可以判斷當前窗口數量來確定需不需要打開一個新的窗口。因為窗口無法在 ready 事件前創建,你應當在你的應用初始化後僅監聽 activate 事件。通過在您現有的 whenReady() 回調中附上您的事件監聽器來完成這個操作。
// index.js
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
前面講到我們會在 HTML 文件中插入 Electron 的版本號。然而,在 index.js 主進程中,是不能編輯 DOM 的,因為它無法訪問到渲染進程 document 上下文,它們存在於完全不同的進程中。
這時候,預加載腳本就可以派上用場了。預加載腳本在渲染進程加載之前加載,並有權訪問兩個渲染進程全局 (例如 window 和 document) 和 Node.js 環境。
3.6.1. 創建預加載腳本創建一個名為 preload.js 的新腳本如下:
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
}
replaceText('electron-version', process.versions.electron);
})
我們需要在初始化 BrowserWindow 實例的時候,傳入該預加載腳本。
// 在文件頭部引入 Node.js 中的 path 模塊
const path = require('path')
// 修改現有的 createWindow() 函數
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
// ...
然後重新啟動程序,就可以看到 Electron 的版本了。
4. Electron 的流程模型前面講到了主進程、渲染進程等概念性知識,初學者可能會對此比較迷惑,不過,進行 Electron,對這一塊內容的掌握是至關重要的,後面的 IPC 進程通信,也與此有關。實際上,Electron 繼承了來自 Chromium 的多進程架構,作為前端工程師,對於瀏覽器進程架構有所了解,也是非常有必要的。
4.1. 主進程每個 Electron 應用都有一個單一的主進程,作為應用程式的入口點,比如上面的 index.js。主進程在 Node.js 環境中運行,這意味著它具有 require 模塊和使用所有 Node.js API 的能力。主進程一般包括以下三大塊:
窗口管理:使用 BrowserWindow 模塊創建和管理應用窗口。類的每個實例創建一個應用程式窗口,且在單獨的渲染器進程中加載一個網頁。
應用生命周期:主進程可以使用 Electron 提供的 app 模塊來控制應用程式的生命周期。
原生 API:Electron 有著多種控制原生桌面功能的模塊,例如菜單、對話框以及託盤圖標。
4.2. 渲染進程每個打開的 BrowserWindow 都會生成一個單獨的渲染進程。渲染進程負責渲染網頁實際的內容。因此,渲染進程中運行的代碼,幾乎跟我們編寫的 Web 代碼別無二致。除此之外,渲染進程也無法直接訪問 require 或其他 Node.js API。
注意:實際上渲染進程可以生成一個完整的 Node.js 環境以便於開發。在過去這是默認的,但如今此功能考慮到安全問題已經被禁用。
4.3. 預加載腳本前面上手的時候已經講過預加載腳本了,預加載(preload)腳本會在渲染進程網頁內容開始加載之前執行,並且可以訪問 Node.js API。由於預加載腳本與渲染器共享同一個全局 Window 接口,因此它通過在 window 全局中暴露任意您的網絡內容可以隨後使用的 API 來增強渲染器。
不過我們不能在預加載腳本中直接給 window 掛載變量,因為 contextIsolation 是默認的。
window.myAPI = { desktop: true }
console.log(window.myAPI) // => undefined
Electron 這樣做是為了將預加載腳本與渲染進程的主要運行環境隔離開來的,以避免洩漏任何具特權的 API 到網頁內容代碼中。(比如有些人會把 ipcRenderer.send 的方法暴露給 web 端,這將允許網站發送任意的 IPC 消息)
我們也可以關閉 contextIsolation,不過不建議這麼做。
new BrowserWindow({
// ...
webPreferences: {
// ...
contextIsolation: false
}
})
最好使用 contextBridge 模塊來安全地實現交互:
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
console.log(window.myAPI)// => { desktop: true }
5. Electron IPC 通信Electron 有主進程和渲染進程,之間會有許多通信,這樣就涉及到了進程間通信(IPC,InterProcess Communication)。
在 Electron 中,主線程和渲染進程之間進行通信,只要是用到以下兩個模塊:
5.1. 渲染進程給主線程發送消息,主線程回復5.1.1. 普通腳本監聽普通腳本引入 electron 的 ipcRenderer 模塊,實現發送消息。
在 HTML 文件添加 renderer.js 腳本
const { ipcRenderer } = require('electron')
ipcRenderer.on('main-message-reply', (event, arg) => {
console.log(arg);
});
ipcRenderer.send('message-from-renderer', '渲染進程發送消息過來了');
在 index.js 入口文件引入 ipcMain 模塊,並修改 BrowserWindow 的實例化參數,開啟渲染進程的 Node.js 環境。
const { ipcMain } = require('electron')
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// 這裡開啟後 渲染進程就可以用 NodeJS 環境
// 可以引如 Electron 相關模塊
nodeIntegration: true,
contextIsolation: false,
},
});
mainWindow.loadFile('index.html');
}
ipcMain.on('message-from-renderer', (event, arg) => {
console.log(arg);
// 接收到消息後可以回復
event.reply('main-message-reply', '主進程回復了')
})
啟動應用,可以在命令行看到渲染進程發過來的消息了。
然後渲染進程收到主線程的回覆。
5.1.2. 預加載腳本暴露接口在預加載腳本中,可以暴露一些全局的接口給到渲染進程,然後渲染進程調用,從而達到通信的目的。這種方式類似於微信 SDK,不用侵入到前端腳本去監聽事件,較為安全。
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 這裡暴露一個全局myAPI變量
contextBridge.exposeInMainWorld('myAPI', {
getMessage(args) {
ipcRenderer.send('message-from-proload', args);
consoloe.log('前端調用了:', args)
}
})
renderer.js 直接調用暴露出來的接口。
// renderer.js
window.myAPI.getMessage('postMessage');
index.js 主進程監聽預加載腳本發送過來的信息。
ipcMain.on('message-from-proload', (event, arg) => {
console.log(arg);
// 接收到消息後可以回復
event.reply('main-message-reply', '主進程回復了')
})
將 renderer.js 改為如下代碼,監聽主線程發送過來的消息。
const { ipcRenderer } = require("electron");
ipcRenderer.on("message", (event, arg) => {
console.log("主進程主動推消息了:", arg);
});
主線程往渲染進程發送消息,需要用到 webContents。 webContents 是一個 EventEmitter,負責渲染和控制網頁,是 BrowserWindow 對象的一個屬性。修改一下 index.js 文件。
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false,
},
});
const contents = mainWindow.webContents;
mainWindow.loadFile('index.html');
contents.openDevTools(); //打開調試工具
contents.on("did-finish-load", () => {
//頁面加載完成觸發的回調函數
contents.send("main-message-reply", "我看到你加載完了,給你發個信息");
});
}
運行應用,就可以在渲染進程中打開看到消息了。
以上的通信方式均為異步,不過 Electron 也提供了同步的通信方式,但是同步的方式會阻塞代碼的執行,最好都使用異步通信。同步用法在這裡不多作介紹。
ipcMain 和 ipcRenderer 模塊還有一些其他的通信 API,不過大抵都是類似的通信方式,需要了解的同學可以自行去查閱文檔。
6. 最後到這裡文章的介紹就差不多了,不過在實際寫代碼的時候,感覺 Electron 的原生 IPC 通信機制,寫起來還是有點繁瑣。VSCode 的事件通信機制,聽聞封裝得比較好,後面有時間再去讀讀它的源碼,寫一篇文章看看。