人生有些事,錯過一時,就錯過一世。
?官網:簡介 | Electron
Electron-大多用來寫桌面端軟件
?
Electron介紹
Electront的核心組成是Chromium、Node.js以及內置的Native API,其中Chromium為Electron提供強大的UI能力,可以在不考慮兼容的情況下利用強大的Web生態來開發界面;Node.js讓Electron有了底層的操作能力,比如像文件的讀寫,然后集成C++等等,還可以使用大量的NPM包來幫助大家來完成項目需求;最后內置的Native API解決了跨平臺的問題,首先它提供了統一的原生界面,比如像窗口、托盤,其次是系統能力,比如像我們的Notification,最后是應用的基礎能力,比如像軟件更新,崩潰監控等等,而通過這三者的組合,我們開發桌面應用變得十分的高效。
基礎介紹
1、Electron是什么?
Electron 是 GitHub 開發的一個開源框架。它允許開發者使用Web技術構建跨平臺桌面應用。
高效
:通過Web技術寫UI 能力
:底層能力 能力&體驗
:跨平臺&原生能力
桌面技術選型
1、為什么開發桌面端?
① 提供更快捷的入口,讓自己的產品占據用戶的桌面;
② 軟件需要離線可用;
③ 需要調用到系統能力,比如像通知用戶,然后結合打印機去完成自己的業務等等
④ 安全需求,比如在金融或企業級應用領域下,我們會更偏向于做一個桌面端
2、技術
1)Native(C++/C#/Objective-C)
各平臺的原生語言寫的應用在高性能、原生體驗、包體積小、門檻高、迭代速度慢。
2)QT
基于C++的跨平臺開發框架(Mac、Windows、IOS、Android、Linux、嵌入式)應用十分廣泛,像大家熟知的DropBox或者WPS,它都是用QT來寫的;雖然是跨平臺,但實際(高)性能還挺好,它甚至可以媲美原生(體驗);QT它有著自己的QML,類似CSS,也有不錯的社區和生態,所以迭代速度會比Native會快一些,但是整體門檻還是比較高,而且人才在市場上比較稀缺。
3)Flutter
Flutter是移動端非常火的,它的整體的目標是跨端(iOS、Android、Mac、Windows、Linux、Web),PC端在發展中(Mac>Linux、Windows),在Linux和Windows上是基本不可用的狀態,基建也特別少,所以目前來說是不太適用用做業務的,但是可以保持關注。
4)NW.js
Web技術代表包括NW.js和Electron,NW.js它跟Electron特別像,它一樣是跨平臺(Mac、Windows、Linux),基于Web來做桌面端,V0.14.7支持XP(XP市場份額約為15%),NW.js做的比較好的它支持了一個源碼的加密,然后支持Chrome的擴展;它有不錯的社區,但是它也有這Electron一樣的缺陷,就是包體積會比較大、性能一般,代表作有微信開發工具。
5)Electron
它是一個跨平臺框架(Mac、Windows、Linux、不支持XP),有著非常活躍的社區,如下圖作為案例。在包體積中,因為Electron將整個Chromium都打進去了,所以哪怕你的代碼只有一行Hello World,包體積也會達到50M,同時性能相比Native和QT都會差一些。
more…
?技術架構與原理
1、Chromium架構:
了解Electron的架構和原理、Electron的多進程架構以及我們的Chromium和Node.js是怎么一起工作的。Electron是基于Chromium做的,如果想了解Electron得先了解Chromium架構,如下圖為Chromium架構:
?Chromium本質是Chrome的開源版,也是一個瀏覽器,瀏覽器也是一個桌面應用,它需要去創建窗口、右鍵菜單、管理瀏覽器Tab頁面還有擴展程序等等,而處理這些事項的進程,我們稱它為主進程,也就是如圖中的Browser,而對應每個具體頁面的進程,我們稱它為渲染進程,對應的就是Render。在一個瀏覽器里面,它會有一個Browser,多個頁面,而這兩個進程需要通信交互才能運轉的,如果大家對Linux或者進程有一定了解,兩個進程它就需要跨進程通信,也就是所謂的IPC。我們主進程的RendererProcessHost以及Render進程的RenderProcess就是專門用來處理IPC事件。接下來將一下渲染進程具體內容,首先是RenderView,我們最熟悉的頁面就是在RenderView中,基于Webkit排版展示出來的。最后只剩下一個ResourceDispatcher,是用來處理我們的資源請求,當我們的頁面需要請求資源的時候,會通過ResourceDispatcher,然后創建一個請求ID,然后轉發到我們的IPC,在我們的Browser進程里處理然后返回。本質上圖帶給我們有三點分別是:
① Chromium是多進程架構,包括Browser和多個Render;
② 進程間是需要IPC通信;
③ 我們Web關注到的只是很小一部分
2、Electron架構:
接下來我們來看一下Electron的大體架構,如下圖:
?
由于Electron使用了Chromium來展示Web頁面,所以Chromium的多進程架構也會被使用到Electron,在Electron中也分為主進程、渲染進程,但是跟Chromium不一樣的有兩點:① 我們在各個進程里暴露了一些Native API;② 我們引入了Node.js,于是我們在Electron中,可以使用Chromium和Node,比如我們可以通過Node去管理窗口,然后在頁面中我們可以使用Node庫。其實這很不容易的,因為在主線程中同一個時間下,只能運行一個事件循環,但是Node.js它的事件循環是基于libuv,但Chromium基于message bump,這就是Electron原理的重點就是如何整合事件循環:①Chromium的messagebump用libuv實現一次,比如像NW.js就是這么做的。②Node.js集成到Chromium。
可以簡單的理解為Electron為web項目套上了Node.js環境的殼,使得我們可以調用Node.js的豐富的API。這樣我們可以用JavaScript來寫桌面應用,拓展很多我們在web端不能做的事情。
?
Electron 快速上手
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" }
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、創建 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 的版本。
4、創建入口文件
類似于 Node.js 啟動服務,Electron 啟動也需要一個入口文件,這里我們創建 index.js 文件。在這個入口文件里,需要去加載上面創建的 HTML 文件,那么如何加載呢? Electron 提供了兩個模塊:
-- app 模塊,它控制應用程序的事件生命周期。
-- BrowserWindow 模塊,它創建和管理應用程序 窗口。
入口文件是 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 .
?
運行完打開的應用程序如下圖所示。
5、管理窗口的聲明周期
雖然現在可以打開一個瀏覽器窗口,但還需要一些額外的模板代碼使其看起來更像是各平臺原生的。應用程序窗口在每個 OS 下有不同的行為,Electron 將在 app 中實現這些約定的責任交給開發者們。可以使用 process.platform屬性來為不同的操作系統做處理。
(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()
})
?
(2)沒有窗口打開則打開一個新窗口(macOS)
用過 macOS 的人應該都知道,一個應用沒有窗口打開的時候,也是可以繼續運行的,這時如果打開應用程序,就會打開新的窗口。 app 模塊可以監聽應用激活事件 activate,在事件回調里可以判斷當前窗口數量來確定需不需要打開一個新的窗口。因為窗口無法在 ready 事件前創建,你應當在你的應用初始化后僅監聽 activate 事件。通過在您現有的 whenReady() 回調中附上您的事件監聽器來完成這個操作。
?
// index.js
app.whenReady().then(() => {
? createWindow()? app.on('activate', function () {
? ? if (BrowserWindow.getAllWindows().length === 0) createWindow()
? })
})
?
6、預加載腳本
前面講到我們會在?HTML?文件中插入?Electron?的版本號。然而,在?index.js?主進程中,是不能編輯?DOM?的,因為它無法訪問到渲染進程?document?上下文,它們存在于完全不同的進程中。
? ? ?這時候,預加載腳本就可以派上用場了。預加載腳本在渲染進程加載之前加載,并有權訪問兩個渲染進程全局 (例如?window?和?document) 和?Node.js?環境。
(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 的版本了。
?
?
Electron 的流程模型
前面講到了主進程、渲染進程等概念性知識,初學者可能會對此比較迷惑,不過,進行 Electron,對這一塊內容的掌握是至關重要的,后面的 IPC 進程通信,也與此有關。實際上,Electron繼承了來自 Chromium 的多進程架構,作為工程師,對于瀏覽器進程架構有所了解,也是非常有必要的。
1、主進程
每個 Electron 應用都有一個單一的主進程,作為應用程序的入口點,比如上面的 index.js。主進程在 Node.js 環境中運行,這意味著它具有 require 模塊和使用所有 Node.js API 的能力。主進程一般包括以下三大塊:
-- 窗口管理:使用 BrowserWindow 模塊創建和管理應用窗口。類的每個實例創建一個應用程序窗口,且在單獨的渲染器進程中加載一個網頁。
-- 應用生命周期:主進程可以使用 Electron 提供的 app 模塊來控制應用程序的生命周期。
-- 原生 API: Electron 有著多種控制原生桌面功能的模塊,例如菜單、對話框以及托盤圖標。
?
2、渲染進程
每個打開的BrowserWindow都會生成一個單獨的渲染進程。渲染進程負責渲染網頁實際的內容。因此,渲染進程中運行的代碼,幾乎跟我們編寫的 Web 代碼別無二致。除此之外,渲染進程也無法直接訪問 require或其他Node.js API。
注意:實際上渲染進程可以生成一個完整的 Node.js 環境以便于開發。在過去這是默認的,但如今此功能考慮到安全問題已經被禁用。
?
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 }
?
Electron IPC 通信
Electron?有主進程和渲染進程,之間會有許多通信,這樣就涉及到了進程間通信(IPC,InterProcess Communication)。在Electron中,主線程和渲染進程之間進行通信,只要是用到以下兩個模塊:
-- ipcMain : ipcMain是一個 EventEmitter的實例。當在主進程中使用時,它處理從渲染器進程(網頁)發送出來的異步和同步信息。從渲染器進程發送的消息將被發送到該模塊。
-- ipcRenderer: ipcRenderer是一個 EventEmitter的實例。你可以使用它提供的一些方法從渲染進程 (web 頁面) 發送同步或異步的消息到主進程。也可以接收主進程回復的消息。
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', '主進程回復了') })
啟動應用,可以在命令行看到渲染進程發過來的消息了。
?
?(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', '主進程回復了')
})
?
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,不過大抵都是類似的通信方式,需要了解的同學可以自行去查閱文檔。
?