https://uinika.github.io/web/server/electron.html
早期桌面應用的開發主要藉助原生 C/C++ API 進行,由於需要反覆經歷編譯過程,且無法分離界面 UI 與業務代碼,開發調試極爲不便。後期出現的 QT 和 WPF 在一定程度上解決了界面代碼分離和跨平臺的問題,卻依然無法避免較長時間的編譯過程。近幾年伴隨互聯網行業的迅猛發展,尤其是 NodeJS、Chromium 這類基於 W3C 標準開源應用的不斷涌現,原生代碼與 Web 瀏覽器開發逐步走向融合,Electron 正是在這種背景下誕生的。
Electron 是由 Github 開發,通過將Chromium和NodeJS整合爲一個運行時環境,實現使用 HTML、CSS、JavaScript 構建跨平臺的桌面應用程序的目的。Electron 源於 2013 年 Github 社區提供的開源編輯器 Atom,後於 2014 年在社區開源,並在 2016 年的 5 月和 8 月,通過了 Mac App Store 和 Windows Store 的上架許可,VSCode、Skype 等著名開源或商業應用程序,都是基於 Electron 打造。爲了方便編寫測試用例,筆者在 Github 搭建了一個簡單的 Electron 種子項目Octopus,讀者可以基於此來運行本文涉及的示例代碼。
Getting Start
首先,讓我們通過npm init
和git init
新建一個項目,然後通過如下npm
語句安裝最新的 Electron 穩定版。
1 |
➜ npm i -D electron@latest |
然後向項目目錄下的package.json
文件添加一條scripts
語句,便於後面通過npm start
命令啓動 Electron 應用。
1 2 3 4 5 6 7 8 9 10 11 |
{ // ... ... "author": "Hank", "main": "resource/main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^3.0.7" } } |
然後在項目根目錄下新建resource
文件夾,裏面分別再建立index.html
和main.js
兩個文件,最終形成如下的項目結構:
1 2 3 4 5 6 7 8 |
electron-demo ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── resource ├── index.html └── main.js |
main.js
是 Electron 應用程序的主入口點,當在命令行運行這段程序的時候,就會啓動一個 Electron 的主進程,主進程當中可以通過代碼打開指定的 Web 頁面去展示 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** main.js */ const { app, BrowserWindow } = require("electron"); let mainWindow; app.on("ready", () => { mainWindow = new BrowserWindow({ width: 800, height: 500 }); mainWindow.setMenu(null); // mainWindow.loadFile("index.html"); // 隱藏Chromium菜單 // mainWindow.webContents.openDevTools() // 開啓調試模式 mainWindow.on("closed", () => { mainWindow = null; }); }); app.on("window-all-closed", () => { /* 在Mac系統用戶通過Cmd+Q顯式退出之前,保持應用程序和菜單欄處於激活狀態。*/ if (process.platform !== "darwin") { app.quit(); } }); app.on("activate", () => { /* 當dock圖標被點擊並且不會有其它窗口被打開的時候,在Mac系統上重新建立一個應用內的window。*/ if (mainWindow === null) { createWindow(); } }); |
Web 頁面index.html
運行在自己的渲染進程當中,但是能夠通過 NodeJS 提供的 API 去訪問操作系統的原生資源(例如下面代碼中的process.versions
語句),這正是 Electron 能夠跨平臺執行的原因所在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello Electron</title> </head> <body> <h1>你好,Electron!</h1> <!-- 所有NodeJS可用的API都可以通過renderer.js的process屬性訪問 --> <h2> 當前Electron版本: <script> document.write(process.versions.electron); </script> </h2> <h2> 當前NodeJS版本:<script> document.write(process.versions.node); </script> </h2> <h2> 當前Chromium版本: <script> document.write(process.versions.chrome); </script> </h2> <script> // 這裏也可以包含運行在當前進程裏的其它文件 require("./renderer.js"); </script> </body> </html> |
使用命令行工具執行npm start
命令之後,上述 HTML 代碼在筆者 Linux 操作系統內被渲染爲如下界面。應用當中,可以通過CTRL+R
重新加載頁面,或者使用CTRL+SHIFT+I
打開瀏覽器控制檯。
一個 Electron 應用的主進程只會有一個,渲染進程則會有多個。
主進程與渲染進程
- 主進程(main process)管理所有的 web 頁面以及相應的渲染進程,它通過
BrowserWindow
來創建視圖頁面。 - 渲染進程(renderer processes)用來運行頁面,每個渲染進程都對應自己的
BrowserWindow
實例,如果實例被銷燬那麼渲染進程就會被終止。
Electron 分別在主進程和渲染進程提供了大量 API,可以通過require
語句方便的將這些 API 包含在當前模塊使用。但是 Electron 提供的 API 只能用於指定進程類型,即某些 API 只能用於渲染進程,而某些只能用於主進程,例如上面提到的BrowserWindow
就只能用於主進程。
1 2 3 |
const { BrowserWindow } = require("electron"); ccc = new BrowserWindow(); |
Electron 通過remote
模塊暴露一些主進程的 API,如果需要在渲染進程中創建一個BrowserWindow
實例,那麼就可以藉助這個 remote
模塊:
1 2 3 4 |
const { remote } = require("electron"); // 獲取remote模塊 const { BrowserWindow } = remote; // 從remote當中獲取BrowserWindow const browserWindow = new BrowserWindow(); // 實例化獲取的BrowserWindow |
Electron 可以使用所有 NodeJS 上提供的 API,同樣只需要簡單的require
一下。
1 2 3 |
const fs = require("fs"); const root = fs.readdirSync("/"); |
當然,NodeJS 上數以萬計的 npm 包也同樣在 Electron 可用,當然,如果是涉及到底層 C/C++的模塊還需要單獨進行編譯,雖然這樣的模塊在 npm 倉庫裏並不多。
1 |
const S3 = require("aws-sdk/clients/s3"); |
既然 Electron 本質是一個瀏覽器 + 跨平臺中間件
的組合,因此常用的前端調試技術也適用於 Electron,這裏可以通過CTRL+SHIFT+I
手動開啓 Chromium 的調試控制檯,或者通過下面代碼在開發模式下自動打開:
1 |
mainWindow.webContents.openDevTools(); // 開啓調試模式 |
核心模塊
本節將對require("electron")
所獲取的模塊進行概述,便於後期進行分類查找。
app 模塊
Electron 提供的app模塊即提供了可用於區分開發和生產環境的app.isPackaged
屬性,也提供了關閉窗口的app.quit()
和用於退出程序的app.exit()
方法,以及window-all-closed
和ready
等 Electron 程序事件。
1 2 3 4 |
const { app } = require("electron"); app.on("window-all-closed", () => { app.quit(); // 當所有窗口關閉時退出應用程序 }); |
可以使用
app.getLocale()
獲取當前操作系統的國際化信息。
BrowserWindow 模塊
工作在主進程,用於創建和控制瀏覽器窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 主進程中使用如下方式獲取。 const { BrowserWindow } = require("electron"); // 渲染進程中可以使用remote屬性獲取。 const { BrowserWindow } = require("electron").remote; let window = new BrowserWindow({ width: 800, height: 600 }); window.on("closed", () => { win = null; }); // 加載遠程URL window.loadURL("https://uinika.github.io/"); // 加載本地HTML window.loadURL(`file://${__dirname}/app/index.html`); |
例如需要創建一個無邊框窗口的 Electron 應用程序,只需將BrowserWindow
配置對象中的frame
屬性設置爲false
即可:
1 2 3 |
const { BrowserWindow } = require("electron"); let window = new BrowserWindow({ width: 800, height: 600, frame: false }); window.show(); |
例如加載頁面時,渲染進程第一次完成繪製時BrowserWindow
會發出ready-to-show
事件。
1 2 3 4 5 |
const { BrowserWindow } = require("electron"); let win = new BrowserWindow({ show: false }); win.once("ready-to-show", () => { win.show(); }); |
對於較爲複雜的應用程序,ready-to-show
事件的發出可能較晚,會讓應用程序的打開顯得緩慢。 這種情況下,建議通過backgroundColor
屬性設置接近應用程序背景色的方式顯示窗口,從而獲取更佳的用戶體驗。
1 2 3 4 |
const { BrowserWindow } = require("electron"); let window = new BrowserWindow({ backgroundColor: "#272822" }); window.loadURL("https://uinika.github.io/"); |
如果想要創建子窗口,那麼可以使用parent
選項,此時子窗口將總是顯示在父窗口的頂部。
1 2 3 4 5 6 7 |
const { BrowserWindow } = require("electron"); let top = new BrowserWindow(); let child = new BrowserWindow({ parent: top }); child.show(); top.show(); |
創建子窗口時,如果需要禁用父窗口,那麼可以同時設置modal
選項。
1 2 3 4 5 6 7 8 |
const { BrowserWindow } = require("electron"); let child = new BrowserWindow({ parent: top, modal: true, show: false }); child.loadURL("https://uinika.github.io/"); child.once("ready-to-show", () => { child.show(); }); |
globalShortcut 模塊
使用globalShortcut
模塊中的register()
方法註冊快捷鍵。
1 2 3 4 5 6 7 8 |
const { app, globalShortcut } = require("electron"); app.on("ready", () => { // 註冊一個快捷鍵監聽器。 globalShortcut.register("CommandOrControl+Y", () => { // 當按下Control +Y鍵時觸發該回調函數。 }); }); |
Linux 和 Windows 上【Command】鍵會失效, 所以要使用 CommandOrControl(既 MacOS 上是【Command】鍵 ,Linux 和 Windows 上是【Control】鍵)。
clipboard 模塊
用於在系統剪貼板上執行復制和粘貼操作,包含有readText()
、writeText()
、readHTML()
、writeHTML()
、readImage()
、writeImage()
等方法。
1 2 |
const { clipboard } = require("electron"); clipboard.writeText("一些字符串內容"); |
globalShortcut 模塊
用於在 Electron 應用程序失去鍵盤焦點時監聽全局鍵盤事件,即在操作系統中註冊或註銷全局快捷鍵。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const { app, globalShortcut } = require("electron"); app.on("ready", () => { // 註冊全局快捷鍵 const regist = globalShortcut.register("CommandOrControl+A", () => { console.log("快捷鍵被摁下!"); }); if (!regist) { console.log("註冊失敗!"); } // 檢查快捷鍵是否註冊成功 console.log(globalShortcut.isRegistered("CommandOrControl+A")); }); app.on("will-quit", () => { // 註銷快捷鍵 globalShortcut.unregister("CommandOrControl+A"); // 清空所有快捷鍵 globalShortcut.unregisterAll(); }); |
ipcMain 與 ipcRenderer 模塊
用於主進程到渲染進程的異步通信,下面是一個主進程與渲染進程之間發送和處理消息的例子:
1 2 3 4 5 6 7 8 9 10 11 |
// 主進程 const { ipcMain } = require("electron"); ipcMain.on("asynchronous-message", (event, arg) => { console.log(arg); // 打印 "ping" event.sender.send("asynchronous-reply", "pong"); }); ipcMain.on("synchronous-message", (event, arg) => { console.log(arg); // 打印 "ping" event.returnValue = "pong"; }); |
1 2 3 4 5 6 7 8 |
//渲染器進程,即網頁 const { ipcRenderer } = require("electron"); console.log(ipcRenderer.sendSync("synchronous-message", "ping")); // 打印 "pong" ipcRenderer.on("asynchronous-reply", (event, arg) => { console.log(arg); // 打印 "pong" }); ipcRenderer.send("asynchronous-message", "ping"); |
如果需要完成渲染器進程到主進程的異步通信,可以選擇使用
ipcRenderer
對象。
Menu 與 MenuItem 模塊
用於主進程,用於創建原生應用菜單和上下文菜單。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const { app, BrowserWindow, Menu } = require("electron"); let mainWindow; const template = [ { label: "自定義菜單", submenu: [{ label: "菜單項-1" }, { label: "菜單項-2" }] } ]; app.on("ready", () => { mainWindow = new BrowserWindow({ width: 800, height: 500 }); mainWindow.setMenu(Menu.buildFromTemplate(template)); mainWindow.loadFile("resource/index.html"); }); |
使用
MenuItem
類可以添加菜單項至 Electron 應用程序菜單和上下文菜單當中。
netLog 模塊
用於記錄網絡日誌。
1 2 3 4 5 6 7 |
const { netLog } = require("electron"); netLog.startLogging("/user/log.info"); /** 一些網絡事件發生之後 */ netLog.stopLogging(path => { console.log("網絡日誌log.info保存在", path); }); |
powerMonitor 模塊
通過 Electron 提供的powerMonitor
模塊監視當前電腦電源狀態的改變,值得注意的是,在app
模塊的ready
事件被觸發之前, 不能引用或使用該模塊。
1 2 3 4 5 6 7 8 |
const electron = require("electron"); const { app } = electron; app.on("ready", () => { electron.powerMonitor.on("suspend", () => { console.log("系統將要休眠了!"); }); }); |
powerSaveBlocker 模塊
阻止操作系統進入低功耗 (休眠) 模式。
1 2 3 4 5 6 |
const { powerSaveBlocker } = require("electron"); const ID = powerSaveBlocker.start("prevent-display-sleep"); console.log(powerSaveBlocker.isStarted(ID)); powerSaveBlocker.stop(ID); |
protocol 模塊
註冊自定義協議並攔截基於現有協議的請求,例如下面代碼實現了一個與[file://]
協議等效的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
const { app, protocol } = require("electron"); const path = require("path"); app.on("ready", () => { protocol.registerFileProtocol( "uinika", (request, callback) => { const url = request.url.substr(7); callback({ path: path.normalize(`${__dirname}/${url}`) }); }, error => { if (error) console.error("協議註冊失敗!"); } ); }); |
net 模塊
net
模塊是一個發送 HTTP(S) 請求的客戶端 API,類似於 NodeJS 的 HTTP 和 HTTPS 模塊 ,但底層使用的是 Chromium 原生網絡庫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const { app } = require("electron"); app.on("ready", () => { const { net } = require("electron"); const request = net.request("https://zhihu.com/people/uinika/activities"); request.on("response", response => { console.log(`STATUS: ${response.statusCode}`); console.log(`HEADERS: ${JSON.stringify(response.headers)}`); response.on("data", chunk => { console.log(`BODY: ${chunk}`); }); response.on("end", () => { console.log("沒有更多數據!"); }); }); request.end(); }); |
Electron 中提供的
ClientRequest
類用來發起 HTTP/HTTPS 請求,IncomingMessage
類則用於響應 HTTP/HTTPS 請求。
remote 模塊
remote
模塊返回的每個對象都表示主進程中的一個對象,調用這個對象實質是在發送同步進程消息。因爲 Electron 當中 GUI 相關的模塊 (如 dialog
、menu
等) 僅在主進程中可用, 在渲染進程中不可用,所以remote
模塊提供了一種渲染進程(Web 頁面)與主進程(IPC)通信的簡單方法。remote 模塊包含了一個remote.require(module)
remote.process
:主進程中的process
對象,與remote.getGlobal("process")
作用相同, 但結果已經被緩存。remote.getCurrentWindow()
:返回BrowserWindow
,即該網頁所屬的窗口。remote.getCurrentWebContents()
:返回WebContents
,即該網頁的 Web 內容remote.getGlobal(name)
:該方法返回主進程中名爲name
的全局變量。remote.require(module)
:返回主進程內執行require(module)
時返回的對象,參數module
指定的模塊相對路徑將會相對於主進程入口點進行解析。
1 2 3 4 5 6 7 |
project/ ├── main │ ├── helper.js │ └── index.js ├── package.json └── renderer └── index.js |
1 2 3 4 5 6 7 8 9 10 11 |
// 主進程: main/index.js const { app } = require("electron"); app.on("ready", () => { /* ... */ }); // 主進程關聯的模塊: main/test.js module.exports = "This is a test!"; // 渲染進程: renderer/index.js const helper = require("electron").remote.require("./helper"); // This is a test! |
remote
模塊提供的主進程與渲染進程通信方法比ipcMain
/ipcRenderer
更加易於使用。
screen 模塊
檢索有關屏幕大小、顯示器、光標位置等信息,應用的ready
事件觸發之前,不能使用該模塊。下面的示例代碼,創建了一個可以自動全屏窗口的應用:
1 2 3 4 5 6 7 8 9 10 |
const electron = require("electron"); const { app, BrowserWindow } = electron; let window; app.on("ready", () => { const { width, height } = electron.screen.getPrimaryDisplay().workAreaSize; window = new BrowserWindow({ width, height }); window.loadURL("https://github.com"); }); |
shell 模塊
提供與桌面集成相關的功能,例如可以通過調用操作系統默認的應用程序管理文件或Url
。
1 2 3 |
const { shell } = require("electron"); shell.openExternal("https://github.com"); |
systemPreferences 模塊
獲取操作系統特定的偏好信息,例如在 Mac 下可以通過下面代碼獲取當前是否開啓系統 Dark 模式的信息。
1 2 |
const { systemPreferences } = require("electron"); console.log(systemPreferences.isDarkMode()); // 返回一個布爾值。 |
Tray 模塊
用於主進程,添加圖標和上下文菜單至操作系統通知區域。
1 2 3 4 5 6 7 8 9 10 |
const { app, Menu, Tray } = require("electron"); let tray = null; app.on("ready", () => { tray = new Tray("/images/icon"); const contextMenu = Menu.buildFromTemplate([{ label: "Item1", type: "radio" }, { label: "Item2", type: "radio" }, { label: "Item3", type: "radio", checked: true }, { label: "Item4", type: "radio" }]); tray.setToolTip("This is my application."); tray.setContextMenu(contextMenu); }); |
webFrame 模塊
定義當前網頁渲染的一些屬性,比如縮放比例、縮放等級、設置拼寫檢查、執行 JavaScript 腳本等等。
1 2 |
const { webFrame } = require("electron"); webFrame.setZoomFactor(5); // 將頁面縮放至500%。 |
session 模塊
Electron 的session
模塊可以創建新的session
對象,主要用來管理瀏覽器會話、cookie、緩存、代理設置等等。
如果需要訪問現有頁面的session
,那麼可以通過BrowserWindow
對象的webContents
的session
屬性來獲取。
1 2 3 4 5 6 7 |
const { BrowserWindow } = require("electron"); let window = new BrowserWindow({ width: 600, height: 900 }); window.loadURL("https://uinika.github.io/web/server/electron.html"); const mySession = window.webContents.session; console.log(mySession.getUserAgent()); |
Electron 裏也可以通過session
模塊的cookies
屬性來訪問瀏覽器的 Cookie 實例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const { session } = require("electron"); // 查詢所有cookies。 session.defaultSession.cookies.get({}, (error, cookies) => { console.log(error, cookies); }); // 查詢當前URL下的所有cookies。 session.defaultSession.cookies.get({ url: "http://www.github.com" }, (error, cookies) => { console.log(error, cookies); }); // 設置cookie const cookie = { url: "https://www.zhihu.com/people/uinika/posts", name: "hank", value: "zhihu" }; session.defaultSession.cookies.set(cookie, error => { if (error) console.error(error); }); |
使用Session
的WebRequest
屬性可以訪問WebRequest
類的實例,WebRequest
類可以在 HTTP 請求生命週期的不同階段修改相關內容,例如下面代碼爲 HTTP 請求添加了一個User-Agent
協議頭:
1 2 3 4 5 6 7 8 9 10 11 |
const { session } = require("electron"); // 發送至下面URL地址的請求將會被添加User-Agent協議頭 const filter = { urls: ["https://*.github.com/*", "*://electron.github.io"] }; session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { details.requestHeaders["User-Agent"] = "MyAgent"; callback({ cancel: false, requestHeaders: details.requestHeaders }); }); |
desktopCapturer 模塊
用於捕獲桌面窗口裏的內容,該模塊只擁有一個方法:desktopCapturer.getSources(options, callback)
。
options
對象types
:字符串數組,列出需要捕獲的桌面類型是screen
還是window
。thumbnailSize
:媒體源縮略圖的大小,默認爲150x150
。
callback
回調函數,擁有如下 2 個參數:error
:錯誤信息。sources
:捕獲的資源數組。
如下代碼工作在渲染進程當中,作用是將桌面窗口捕獲爲視頻:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
const { desktopCapturer } = require("electron"); desktopCapturer.getSources({ types: ["window", "screen"] }, (error, sources) => { if (error) throw error; for (let i = 0; i < sources.length; ++i) { if (sources[i].name === "Electron") { navigator.mediaDevices .getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: sources[i].id, minWidth: 1280, maxWidth: 1280, minHeight: 800, maxHeight: 800 } } }) .then(stream => handleStream(stream)) .catch(error => handleError(error)); return; } } }); function handleStream(stream) { const video = document.querySelector("video"); video.srcObject = stream; video.onloadedmetadata = error => video.play(); } function handleError(error) { console.log(error); } |
dialog 模塊
調用操作系統原生的對話框,工作在主線程,下面示例展示了一個用於選擇多個文件和目錄的對話框:
1 2 |
const { dialog } = require("electron"); console.log(dialog.showOpenDialog({ properties: ["openFile", "openDirectory", "multiSelections"] })); |
由於對話框工作在 Electron 的主線程上,如果需要在渲染器進程中使用, 那麼可以通過remote
來獲得:
1 2 |
const { dialog } = require("electron").remote; console.log(dialog); |
contentTracing 模塊
從 Chromium 收集跟蹤數據,從而查找性能瓶頸。使用後需要在瀏覽器打開chrome://tracing/
頁面,然後加載生成的文件查看結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const { app, contentTracing } = require("electron"); app.on("ready", () => { const options = { categoryFilter: "*", traceOptions: "record-until-full,enable-sampling" }; contentTracing.startRecording(options, () => { console.log("開始跟蹤!"); setTimeout(() => { contentTracing.stopRecording("", path => { console.log("跟蹤數據已經記錄至" + path); }); }, 8000); }); }); |
webview 標籤
Electron 的<webview>
標籤基於 Chromium,由於開發變動較大官方並不建議使用,而應考慮<iframe>
或者 Electron 的BrowserView
等選擇,或者完全避免在頁面進行內容嵌入。
<webview>
與<iframe>
最大不同是運行於不同的進程當中,Electron 應用程序與嵌入內容之間的所有交互都是異步進行的,這樣可以保證應用程序與嵌入內容雙方的安全。
1 |
<webview id="uinika" src="http://localhost:5000/web/server/electron.html"></webview> |
webContents 屬性
webContents
是BrowserWindow
對象的一個屬性,負責渲染和控制 Web 頁面。
1 2 3 4 5 6 7 |
const { BrowserWindow } = require("electron"); let window = new BrowserWindow({ width: 600, height: 500 }); window.loadURL("https://uinika.github.io/"); let contents = window.webContents; console.log(contents); |
window.open() 函數
該函數用於打開一個新窗口並加載指定url
,調用後將會爲該url
創建一個BrowserWindow
實例,並返回一個BrowserWindowProxy
對象,但是該對象只能對打開的url
頁面進行有限的控制。正常情況下,如果希望完全控制新窗口,可以直接創建一個新的BrowserWindow
。
1 2 |
// window.open(url[, frameName][, features]) window.open("https://github.com", "_blank", "nodeIntegration=no"); |
BrowserWindowProxy
對象擁有如下屬性和方法:
win.closed
:子窗口關閉後設置爲true
的布爾屬性。win.blur()
:將焦點從子窗口中移除。win.close()
:強制關閉子窗口, 而不調用其卸載事件。win.eval(code)
:code
字符串,需要在子窗口 Eval 的代碼。win.focus()
:聚焦子窗口(即將子窗口置頂)。win.print()
:調用子窗口的打印對話框。win.postMessage(message, targetOrigin)
:向子窗口發送信息。
Electron 進程
Electron 的process
對象繼承自 NodeJS 的process
對象,但是新增了一些有用的事件、屬性、方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const { app, BrowserWindow } = require("electron"); let mainWindow; app.on("ready", () => { mainWindow = new BrowserWindow({ width: 800, height: 500, frame: false }); mainWindow.loadFile("resource/index.html"); console.log(process.type); // 當前進程類型是browser主進程還是renderer渲染進程,browser console.log(process.versions.node); // NodeJS版本,10.2.0 console.log(process.versions.chrome); // Chrome版本,66.0.3359.181 console.log(process.versions.electron); //Electron版本,3.0.13 console.log(process.resourcesPath); // 資源目錄路徑,D:\Workspace\octopus\node_modules\electron\dist\resources }); |
沙箱機制
Chromium 通過將 Web 前端代碼放置在一個與操作系統隔離的沙箱中運行,從而保證惡意代碼不會侵犯到操作系統本身。但是 Electron 中渲染進程可以調用 NodeJS,而 NodeJS 又需要涉及大量操作系統調用,因而沙箱機制默認是禁用的。
某些應用場景下,需要運行一些不確定安全性的外部前端代碼,爲了保證操作系統安全,可能需要開啓沙箱機制。此時首先在創建BrowserWindow
時傳入sandbox
屬性,然後在命令行添加--enable-sandbox
參數傳遞給 Electron 即可完成開啓。
1 2 3 4 5 6 7 8 9 |
let win; app.on("ready", () => { window = new BrowserWindow({ webPreferences: { sandbox: true } }); window.loadURL("http://google.com"); }); |
使用
sandbox
選項之後,將會阻止 Electron 在渲染器中創建一個 NodeJS 運行時環境,此時新窗口中的window.open()
將按照瀏覽器原生的方式工作。
MacBook TouchBar 支持
針對 Mac 筆記本電腦上配置的 TouchBar 硬件,Electron 提供了一系列相關的類與操作接口:TouchBar
、TouchBarButton
、TouchBarColorPicker
、TouchBarGroup
、TouchBarLabel
、TouchBarPopover
、TouchBarScrubber
、TouchBarSegmentedControl
、TouchBarSlider
、TouchBarSpacer
。
創建應用圖標
用於將 PNG 或 JPG 圖片設置爲托盤、Dock 和應用程序的圖標。
1 2 3 4 5 |
const { BrowserWindow, Tray } = require("electron"); const Icon = new Tray("/images/icon.png"); let window = new BrowserWindow({ icon: "/images/window.png" }); |
安全原則
由於 Electron 的發佈通常落後最新版本 Chromium 幾周甚至幾個月,因此特別需要注意如下這些安全性問題:
使用安全的協議加載外部內容
外部資源儘量使用更安全的協議加載,比如HTTP
換成HTTPS
、WS
換成WSS
、FTP
換成FTPS
等。
1 2 3 4 5 6 7 8 9 10 11 12 |
<!-- 錯誤 --> <script crossorigin src="http://cdn.com/react.js"></script> <link rel="stylesheet" href="http://cdn.com/scss.css" /> <!-- 正確 --> <script crossorigin src="https://cdn.com/react.js"></script> <link rel="stylesheet" href="https://cdn.com/scss.css" /> <script> browserWindow.loadURL("http://uinika.github.io/); // 錯誤 browserWindow.loadURL("https://uinika.github.io/"); // 正確 </script> |
加載外部內容時禁用 NodeJS 集成
使用BrowserWindow
、BrowserView
、<webview>
加載遠程內容時,都需要通過禁用 NodeJS 集成去限制遠程代碼的執行權限,避免惡意代碼跨站攻擊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<!-- 錯誤 --> <webview nodeIntegration src="page.html"></webview> <!-- 正確 --> <webview src="page.html"></webview> <script> /** 錯誤 */ const mainWindow = new BrowserWindow(); mainWindow.loadURL("https://my-website.com"); /** 正確 */ const mainWindow = new BrowserWindow({ webPreferences: { nodeIntegration: false, preload: "./preload.js" } }); </script> |
對於需要與遠程代碼共享的變量或函數,可以通過將其掛載至當前頁面的
window
全局對象來實現。
渲染進程中啓用上下文隔離
上下文隔離是 Electron 提供的試驗特性,通過爲遠程加載的代碼創造一個全新上下文環境,避免與主進程中的代碼出現衝突或者相互污染。
1 2 3 4 5 6 7 |
// 主進程 const mainWindow = new BrowserWindow({ webPreferences: { contextIsolation: true, preload: "preload.js" } }); |
處理遠程內容中的會話許可
當頁面嘗試使用某個特性時,會彈出通知讓用戶手動進行確認;而默認情況下,Electron 會自動批准所有的許可請求。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const { session } = require("electron"); session.fromPartition("some-partition").setPermissionRequestHandler((webContents, permission, callback) => { const url = webContents.getURL(); if (permission === "notifications") { callback(true); // 通過許可請求 } if (!url.startsWith("https://my-website.com")) { return callback(false); // 拒絕許可請求 } }); |
不要禁用 webSecurity
在渲染進程禁用webSecurity
將導致許多重要的安全性功能被關閉,因此 Electron 默認開啓。
1 2 3 4 5 |
const mainWindow = new BrowserWindow({ webPreferences: { webSecurity: false // 錯誤的做法,缺省該屬性使用默認值即可。 } }); |
定義 CSP 安全策略
內容安全策略 CSP 允許 Electron 通過webRequest
對指定 URL 的訪問進行約束,例如允許加載https://uinika.github.io/
這個源,那麼https://hack.attacker.com
將不會被允許加載,CSP 是處理跨站腳本攻擊、數據注入攻擊的另外一層保護措施。
1 2 3 4 5 6 7 8 9 10 |
const { session } = require("electron"); session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, "Content-Security-Policy": ["default-src 'none'"] } }); }); |
使用file://
協議打開本地文件時,可以通過元數據標籤<meta>
的屬性來添加 CSP 約束。
1 |
<meta http-equiv="Content-Security-Policy" content="default-src 'none'" /> |
別設置 allowRunningInsecureContent 爲 true
Electron 默認不允許在 HTTPS 頁面中加載 HTTP 來源的代碼,如果將allowRunningInsecureContent
屬性設置爲true
會禁用這種保護。
1 2 3 4 5 |
const mainWindow = new BrowserWindow({ webPreferences: { allowRunningInsecureContent: true // 錯誤的做法,缺省該屬性使用默認值即可。 } }); |
不要開啓實驗性功能
開發人員可以通過experimentalFeatures
屬性啓用未經嚴格測試的 Chromium 實驗性功能,不過 Electron 官方出於穩定性和安全性考慮並不建議這樣做。
1 2 3 4 5 |
const mainWindow = new BrowserWindow({ webPreferences: { experimentalFeatures: true // 錯誤的做法,缺省該屬性使用默認值即可。 } }); |
不要使用 enableBlinkFeatures
Blink 是 Chromium 內置的 HTML/CSS 渲染引擎,開發者可以通過enableBlinkFeatures
啓用其某些默認是禁用的特性。
1 2 3 4 5 |
const mainWindow = new BrowserWindow({ webPreferences: { enableBlinkFeatures: ["ExecCommandInJavaScript"] // 錯誤的做法,缺省該屬性使用默認值即可。 } }); |
禁用 webview 的 allowpopups
開啓allowpopups
屬性將使window.open()
創建一個新的窗口和BrowserWindows
,若非必要狀況,儘量不要使用此屬性。
1 2 3 4 5 |
<!-- 錯誤 --> <webview allowpopups src="page.html"></webview> <!-- 正確 --> <webview src="page.html"></webview> |
驗證 webview 選項與參數
通過渲染進程創建的<WebView>
默認不集成 NodeJS,但是它可以通過webPreferences
屬性創建出一個獨立的渲染進程。在<WebView>
標籤開始渲染之前,Electron 將會觸發一個will-attach-webview
事件,可以通過該事件防止創建具有潛在不安全選項的 Web 視圖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
app.on("web-contents-created", (event, contents) => { contents.on("will-attach-webview", (event, webPreferences, params) => { // 如果未使用或者驗證位置合法,那麼將會剝離預加載腳本。 delete webPreferences.preload; delete webPreferences.preloadURL; // 禁用NodeJS集成。 webPreferences.nodeIntegration = false; // 驗證正在加載的URL。 if (!params.src.startsWith("https://www.zhihu.com/people/uinika/columns")) { event.preventDefault(); } }); }); |
禁用或限制網頁跳轉
如果 Electron 應用程序不需要導航或只需導航至特定頁面,最佳實踐是將導航限制在已知範圍,並禁止其它類型的導航。可以通過在will- navigation
事件處理函數中調用event.preventDefault()
並添加額外的判斷來實現這一點。
1 2 3 4 5 6 7 8 9 10 11 |
const URL = require("url").URL; app.on("web-contents-created", (event, contents) => { contents.on("will-navigate", (event, navigationUrl) => { const parsedUrl = new URL(navigationUrl); if (parsedUrl.origin !== "https://www.zhihu.com/people/uinika/posts") { event.preventDefault(); } }); }); |
禁用或限制新窗口創建
限制在 Electron 應用程序中創建額外窗口,並避免因此帶來額外的安全隱患。webContents
創建新窗口時會觸發一個web-contents-created
事件,該事件包含了將要打開的 URL 以及相關選項,可以在這個事件中檢查窗口的創建,從而對其進行相應的限制。
1 2 3 4 5 6 7 8 9 |
const { shell } = require("electron"); app.on("web-contents-created", (event, contents) => { contents.on("new-window", (event, navigationUrl) => { // 通知操作系統在默認瀏覽器上打開URL event.preventDefault(); shell.openExternal(navigationUrl); }); }); |
Electron 2.0 版本開始,會在可執行文件名爲 Electron 時會爲開發者在控制檯顯示安全相關的警告和建議,開發人員也可以在
process.env
或window
對象上配置ELECTRON_ENABLE_SECURITY_WARNINGS
或ELECTRON_DISABLE_SECURITY_WARNINGS
手動開啓或關閉這些警告。
應用發佈
Electron 的發佈有別於傳統桌面應用程序編譯打包的發部過程,需要首先下載已經預編譯完成的二進制包,Linux 下二進制包結構如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
➜ electron-v3.0.7-linux-x64 tree -L 2 . ├── blink_image_resources_200_percent.pak ├── content_resources_200_percent.pak ├── content_shell.pak ├── electron ├── icudtl.dat ├── libffmpeg.so ├── libnode.so ├── LICENSE ├── LICENSES.chromium.html ├── locales ├── natives_blob.bin ├── ui_resources_200_percent.pak ├── v8_context_snapshot.bin ├── version └── views_resources_200_percent.pak └── resources ├── default_app.asar └── electron.asar |
接下來,就可以部署前面編寫的源代碼,Electron 裏主要有如下兩種部署方式:
- 直接將代碼放置到
resources
下的子目錄,比如app
目錄:
1 2 3 4 5 6 7 |
├── app │ ├── package.json │ └── resource │ ├── index.html │ └── main.js ├── default_app.asar └── electron.asar |
- 將應用打包加密爲
asar
文件以後放置到resources
目錄,比如app.asar
文件。
1 2 3 |
├── app.asar ├── default_app.asar └── electron.asar |
asar 打包源碼
asar 是一種簡單的文件擴展格式,可以將文件如同tar
格式一樣前後連接在一起,支持隨機讀寫,並使用 JSON 來保存文件信息,可以方便的讀取與解析。Electron 通過它可以解決 Windows 文件路徑長度的限制,提高require
語句的加載速度,並且避免源代碼泄漏。
首先,需要安裝asar
這個 npm 包,然後可以選擇全局安裝然通過命令行使用。
1 2 |
➜ npm install asar ➜ asar pack electron-demo app.asar |
當然,更加工程化的方式是通過代碼來執行打包操作,就像下面這樣:
1 2 3 4 5 6 7 8 9 10 11 |
let asar = require("asar"); src = "../octopus"; dest = "build/app.asar"; /** 打包完成後的回調函數 */ callback = () => { console.info("asar打包完成!"); }; asar.createPackage(src, dest, callback); |
Electron 在 Web 頁面可以通過file:
協議讀取 asar 包中的文件,即將 asar 文件視爲一個虛擬的文件夾來進行操作。
1 2 3 4 |
const { BrowserWindow } = require("electron"); const mainWindow = new BrowserWindow(); mainWindow.loadURL("file:///path/to/example.asar/static/index.html"); |
如果需要對 asar 文件進行 MD5 或者 SHA 完整性校驗,可以對 asar 檔案文件本身進行操作。
1 |
asar list app.asar |
rcedit 編輯可執行文件
RcEdit是一款通過編輯窗口管理器的 rc 文件來對其進行配置的工具,Nodejs 社區提供了node-rcedit工具對 Windows 操作系統的.exe
文件進行配置,首先通過npm i rcedit --save-dev
爲項目安裝該依賴項。
1 2 3 |
var rcedit = require("rcedit"); rcedit(exePath, options, callback); |
rcedit()
函數包含有如下屬性:
exePath
:需要進行修改的 Windows 可執行文件所在路徑。options
:一個擁有如下屬性的配置對象。version-string
:版本字符串;file-version
:文件版本;product-version
:產品版本;icon
:圖標文件.ico
的路徑;requested-execution-level
:需要修改的執行級別(asInvoker
、highestAvailable
、requireAdministrator
)。application-manifest
:本地清單文件的路徑。
callback
:函數執行完畢之後回調,完整的函數簽名爲function(error)
。
yarn 包管理器
Yarn 是一款由 Facebook 推出的 JavaScript 包管理器,與 NodeJS 提供的 Npm 一樣使用package.json
作爲包信息文件。
測試當前 Yarn 的安裝版本:
1 |
yarn --version |
初始化新項目:
1 |
yarn init |
添加依賴包:
1 2 3 |
yarn add [package] yarn add [package]@[version] yarn add [package]@[tag] |
將依賴項添加到不同的依賴項類別:devDependencies、peerDependencies 和 optionalDependencies。
1 2 3 |
yarn add [package] --dev yarn add [package] --peer yarn add [package] --optional |
升級依賴包:
1 2 3 |
yarn upgrade [package] yarn upgrade [package]@[version] yarn upgrade [package]@[tag] |
移除依賴包:
1 |
yarn remove [package] |
可以直接使用yarn
命令安裝項目的全部依賴:
1 |
yarn install |
windows-installer
windows-installer是一個用於爲 Electron 應用程序構建 Windows 安裝程序的 Npn 包,底層基於Squirrel(一組用於管理C#或C++開發的 Windows 應用程序的安裝、更新的工具庫)進行實現。
1 |
npm install --save-dev electron-winstaller |
1 2 3 4 5 6 7 8 9 10 |
var electronInstaller = require("electron-winstaller"); resultPromise = electronInstaller.createWindowsInstaller({ appDirectory: "/tmp/build/my-app-64", outputDirectory: "/tmp/build/installer64", authors: "My App Inc.", exe: "myapp.exe" }); resultPromise.then(() => console.log("It worked!"), e => console.log(`No dice: ${e.message}`)); |
electron-build
除了像上面這樣通過預編譯包手動打包應用程序,也可以採用electron-forge、electron-packager等第三方包來完成這項工作,在這裏筆者選擇electron-builder來進行自動化打包任務。
Electron Userland 是一個維護 Electron 模塊的第三方社區,electron-builder 是由其維護的一款能夠同時處理 Windows、MacOS、Linux 多平臺的打包編譯工具。由於 electron-builder 工具包的文件體積較大,其社區強烈推薦使用更快速的yarn
來代替npm
作爲包管理方案。
1 |
yarn add electron-builder --dev |
electron-builder 能夠以命令行或者JavaScript API的方式進行使用:
(1)如果安裝在項目目錄下的node_modules
目錄,可以直接通過 NodeJS 提供的npx
以命令行方式使用:
1 |
npx electron-builder |
(2)也可以像使用其它 Npm 包那樣直接調用 electron-builder 提供的 API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
"use strict"; const builder = require("electron-builder"); const Platform = builder.Platform; // Promise is returned builder .build({ targets: Platform.MAC.createTarget(), config: { "//": "build options, see https://goo.gl/QQXmcV" } }) .then(() => { // handle result }) .catch(error => { // handle error }); |
官方推薦使用electron-webpack-quick-start作爲 Electron 應用的項目模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ // 指定編譯配置 "build": { "appId": "your.id", "mac": { "category": "your.app.category.type" } }, // 添加編譯指令 "scripts": { "pack": "electron-builder --dir", "dist": "electron-builder" }, // 確保原生依賴總是匹配Electron版本 "postinstall": "electron-builder install-app-deps } |
如果項目中存在原生依賴,還需要設置nodeGypRebuild爲
true
。
如果需要調試electron-builder
的行爲,那麼需要設置DEBUG=electron-builder
環境變量。
1 2 |
set DEBUG=electron-builder // Cmder $env:DEBUG=electron-builder // PowerShell |
electron-forge
electron-forge同樣是由 Electron Userland 維護的一款命令行工具,用於快速建立、打包、發佈一個 Electron 應用程序。
1 2 3 4 |
λ npm install -g electron-forge λ electron-forge init my-new-project λ cd my-new-project λ electron-forge start |
目前 Github 上 electron-builder 的 Star 遠遠超過 electron-forge,由於筆者項目需要使用 React,因而也就選用帶有支持 React 項目模板的 electron-builder,需要嘗試 electron-forge 的同學可以移步官網查看更多信息。