Electron 快速入門,順便聊聊 IPC 通信

前陣子將排課系統的一些功能,提供給 solar 編輯器使用,solar 是基於互動課件編輯器 Cocos ICE 進行二次定製和個性化開發的課件製作系統,其底層是 Cocos Creator。而 Cocos Creator 是基於 Electron 進行開發的,所以學習了一些關於 Electron IPC 通信的相關知識,在這裏做一個總結。

文章的開始,先讓我們來了解下 Electron 是什麼。

1. 什麼是 Electron?

Electron 官網只有一句簡單的話: 使用 JavaScript,HTML 和 CSS 構建跨平臺的桌面應用程序。簡單點講,就是有了 Electron,我們就可以用前端技術來寫 web 頁面,它可以轉化爲一個桌面應用。
除此之前,Electron 還有其他的一些特性:

2. 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"
}

3.2. 安裝依賴

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()
})

3.5.2. 沒有窗口打開則打開一個新窗口(macOS)

用過 macOS 的人應該都知道,一個應用沒有窗口打開的時候,也是可以繼續運行的,這時如果打開應用程序,就會打開新的窗口。 app 模塊可以監聽應用激活事件 activate,在事件回調裏可以判斷當前窗口數量來確定需不需要打開一個新的窗口。因爲窗口無法在 ready 事件前創建,你應當在你的應用初始化後僅監聽 activate 事件。通過在您現有的 whenReady() 回調中附上您的事件監聽器來完成這個操作。

// index.js
app.whenReady().then(() => {
  createWindow()

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

3.6. 預加載腳本

前面講到我們會在 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 的能力。主進程一般包括以下三大塊:

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', '主進程回覆了')
})

5.2. 主線程給渲染進程發送消息


將 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 的事件通信機制,聽聞封裝得比較好,後面有時間再去讀讀它的源碼,寫一篇文章看看。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hMtAeNl5iSbq6MOeluEzPg