早期桌面應用的開發主要藉助原生 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,讀者可以基於此來運行本文涉及的示例代碼。
由於知乎文章存在字數限制,完整版文章已經同步發布至個人Github Pages,結合博客的書籤進行閱讀,將會更加方便與直觀,需要的同學請點擊下面直達鏈接。
首先,讓我們通過npm init和git init新建一個項目,然後通過如下npm語句安裝最新的 Electron 穩定版。
npm init
git init
npm
? npm i -D electron@latest
然後向項目目錄下的package.json文件添加一條scripts語句,便於後面通過npm start命令啟動 Electron 應用。
package.json
scripts
npm start
{ // ... ... "author": "Hank", "main": "resource/main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^3.0.7" } }
然後在項目根目錄下新建resource文件夾,裡面分別再建立index.html和main.js兩個文件,最終形成如下的項目結構:
resource
index.html
main.js
electron-demo ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── resource ├── index.html └── main.js
main.js是 Electron 應用程序的主入口點,當在命令行運行這段程序的時候,就會啟動一個 Electron 的主進程,主進程當中可以通過代碼打開指定的 Web 頁面去展示 UI。
/** 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 能夠跨平臺執行的原因所在。
process.versions
<!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打開瀏覽器控制檯。
CTRL+R
CTRL+SHIFT+I
一個 Electron 應用的主進程只會有一個,渲染進程則會有多個。
BrowserWindow
Electron 分別在主進程和渲染進程提供了大量 API,可以通過require語句方便的將這些 API 包含在當前模塊使用。但是 Electron 提供的 API 只能用於指定進程類型,即某些 API 只能用於渲染進程,而某些只能用於主進程,例如上面提到的BrowserWindow就只能用於主進程。
require
const { BrowserWindow } = require("electron");
ccc = new BrowserWindow();
Electron 通過remote模塊暴露一些主進程的 API,如果需要在渲染進程中創建一個BrowserWindow實例,那麼就可以藉助這個remote模塊:
remote
const { remote } = require("electron"); // 獲取remote模塊 const { BrowserWindow } = remote; // 從remote當中獲取BrowserWindow const browserWindow = new BrowserWindow(); // 實例化獲取的BrowserWindow
Electron 可以使用所有 NodeJS 上提供的 API,同樣只需要簡單的require一下。
const fs = require("fs");
const root = fs.readdirSync("/");
當然,NodeJS 上數以萬計的 npm 包也同樣在 Electron 可用,當然,如果是涉及到底層 C/C++的模塊還需要單獨進行編譯,雖然這樣的模塊在 npm 倉庫裏並不多。
const S3 = require("aws-sdk/clients/s3");
既然 Electron 本質是一個瀏覽器 + 跨平臺中間件的組合,因此常用的前端調試技術也適用於 Electron,這裡可以通過CTRL+SHIFT+I手動開啟 Chromium 的調試控制檯,或者通過下面代碼在開發模式下自動打開:
瀏覽器 + 跨平臺中間件
mainWindow.webContents.openDevTools(); // 開啟調試模式
本節將對require("electron")所獲取的模塊進行概述,便於後期進行分類查找。
require("electron")
Electron 提供的app模塊即提供了可用於區分開發和生產環境的app.isPackaged屬性,也提供了關閉窗口的app.quit()和用於退出程序的app.exit()方法,以及window-all-closed和ready等 Electron 程序事件。
app.isPackaged
app.quit()
app.exit()
window-all-closed
ready
const { app } = require("electron"); app.on("window-all-closed", () => { app.quit(); // 當所有窗口關閉時退出應用程序 });
可以使用app.getLocale()獲取當前操作系統的國際化信息。
app.getLocale()
工作在主進程,用於創建和控制瀏覽器窗口。
// 主進程中使用如下方式獲取。 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即可:
frame
false
const { BrowserWindow } = require("electron"); let window = new BrowserWindow({ width: 800, height: 600, frame: false }); window.show();
例如載入頁面時,渲染進程第一次完成繪製時BrowserWindow會發出ready-to-show事件。
ready-to-show
const { BrowserWindow } = require("electron"); let win = new BrowserWindow({ show: false }); win.once("ready-to-show", () => { win.show(); });
對於較為複雜的應用程序,ready-to-show事件的發出可能較晚,會讓應用程序的打開顯得緩慢。 這種情況下,建議通過backgroundColor屬性設置接近應用程序背景色的方式顯示窗口,從而獲取更佳的用戶體驗。
backgroundColor
let window = new BrowserWindow({ backgroundColor: "#272822" }); window.loadURL("https://uinika.github.io/");
如果想要創建子窗口,那麼可以使用parent選項,此時子窗口將總是顯示在父窗口的頂部。
parent
let top = new BrowserWindow(); let child = new BrowserWindow({ parent: top });
child.show(); top.show();
創建子窗口時,如果需要禁用父窗口,那麼可以同時設置modal選項。
modal
let child = new BrowserWindow({ parent: top, modal: true, show: false });
child.loadURL("https://uinika.github.io/"); child.once("ready-to-show", () => { child.show(); });
使用globalShortcut模塊中的register()方法註冊快捷鍵。
globalShortcut
register()
const { app, globalShortcut } = require("electron");
app.on("ready", () => { // 註冊一個快捷鍵監聽器。 globalShortcut.register("CommandOrControl+Y", () => { // 當按下Control +Y鍵時觸發該回調函數。 }); });
Linux 和 Windows 上【Command】鍵會失效, 所以要使用 CommandOrControl(既 MacOS 上是【Command】鍵 ,Linux 和 Windows 上是【Control】鍵)。
用於在系統剪貼板上執行複製和粘貼操作,包含有readText()、writeText()、readHTML()、writeHTML()、readImage()、writeImage()等方法。
readText()
writeText()
readHTML()
writeHTML()
readImage()
writeImage()
const { clipboard } = require("electron"); clipboard.writeText("一些字元串內容");
用於在 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(); });
用於主進程到渲染進程的非同步通信,下面是一個主進程與渲染進程之間發送和處理消息的例子:
// 主進程 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"; });
//渲染器進程,即網頁 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對象。
ipcRenderer
用於主進程,用於創建原生應用菜單和上下文菜單。
const { app, BrowserWindow, Menu } = require("electron");
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 應用程序菜單和上下文菜單當中。
MenuItem
用於記錄網路日誌。
const { netLog } = require("electron");
netLog.startLogging("/user/log.info"); /** 一些網路事件發生之後 */ netLog.stopLogging(path => { console.log("網路日誌log.info保存在", path); });
通過 Electron 提供的powerMonitor模塊監視當前電腦電源狀態的改變,值得注意的是,在app模塊的ready事件被觸發之前, 不能引用或使用該模塊。
powerMonitor
app
const electron = require("electron"); const { app } = electron;
app.on("ready", () => { electron.powerMonitor.on("suspend", () => { console.log("系統將要休眠了!"); }); });
阻止操作系統進入低功耗 (休眠) 模式。
const { powerSaveBlocker } = require("electron");
const ID = powerSaveBlocker.start("prevent-display-sleep"); console.log(powerSaveBlocker.isStarted(ID));
powerSaveBlocker.stop(ID);
註冊自定義協議並攔截基於現有協議的請求,例如下面代碼實現了一個與[file://]協議等效的示例:
[file://]
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模塊是一個發送 HTTP(S) 請求的客戶端 API,類似於 NodeJS 的 HTTP 和 HTTPS 模塊 ,但底層使用的是 Chromium 原生網路庫。
net
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 請求。
ClientRequest
IncomingMessage
remote模塊返回的每個對象都表示主進程中的一個對象,調用這個對象實質是在發送同步進程消息。因為 Electron 當中 GUI 相關的模塊 (如 dialog、menu 等) 僅在主進程中可用, 在渲染進程中不可用,所以remote模塊提供了一種渲染進程(Web 頁面)與主進程(IPC)通信的簡單方法。remote 模塊包含了一個remote.require(module)
dialog
menu
remote.require(module)
remote.process
process
remote.getGlobal("process")
remote.getCurrentWindow()
remote.getCurrentWebContents()
WebContents
remote.getGlobal(name)
name
require(module)
module
project/ ├── main │ ├── helper.js │ └── index.js ├── package.json └── renderer └── index.js
remote模塊提供的主進程與渲染進程通信方法比ipcMain/ipcRenderer更加易於使用。
ipcMain
// 主進程: 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!
檢索有關屏幕大小、顯示器、遊標位置等信息,應用的ready事件觸發之前,不能使用該模塊。下面的示例代碼,創建了一個可以自動全屏窗口的應用:
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"); });
提供與桌面集成相關的功能,例如可以通過調用操作系統默認的應用程序管理文件或Url。
Url
const { shell } = require("electron");
shell.openExternal("https://github.com");
獲取操作系統特定的偏好信息,例如在 Mac 下可以通過下面代碼獲取當前是否開啟系統 Dark 模式的信息。
const { systemPreferences } = require("electron"); console.log(systemPreferences.isDarkMode()); // 返回一個布爾值。
用於主進程,添加圖標和上下文菜單至操作系統通知區域。
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); });
定義當前網頁渲染的一些屬性,比如縮放比例、縮放等級、設置拼寫檢查、執行 JavaScript 腳本等等。
const { webFrame } = require("electron"); webFrame.setZoomFactor(5); // 將頁面縮放至500%。
Electron 的session模塊可以創建新的session對象,主要用來管理瀏覽器會話、cookie、緩存、代理設置等等。
session
如果需要訪問現有頁面的session,那麼可以通過BrowserWindow對象的webContents的session屬性來獲取。
webContents
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 實例。
cookies
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協議頭:
Session
WebRequest
User-Agent
// 發送至下面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.getSources(options, callback)。
desktopCapturer.getSources(options, callback)
options
types
screen
window
thumbnailSize
150x150
callback
error
sources
如下代碼工作在渲染進程當中,作用是將桌面窗口捕獲為視頻:
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); }
調用操作系統原生的對話框,工作在主線程,下面示例展示了一個用於選擇多個文件和目錄的對話框:
const { dialog } = require("electron"); console.log(dialog.showOpenDialog({ properties: ["openFile", "openDirectory", "multiSelections"] }));
由於對話框工作在 Electron 的主線程上,如果需要在渲染器進程中使用, 那麼可以通過remote來獲得:
const { dialog } = require("electron").remote; console.log(dialog);
從 Chromium 收集跟蹤數據,從而查找性能瓶頸。使用後需要在瀏覽器打開chrome://tracing/頁面,然後載入生成的文件查看結果。
chrome://tracing/
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); }); });
Electron 的<webview>標籤基於 Chromium,由於開發變動較大官方並不建議使用,而應考慮<iframe>或者 Electron 的BrowserView等選擇,或者完全避免在頁面進行內容嵌入。
<webview>
<iframe>
BrowserView
<webview>與<iframe>最大不同是運行於不同的進程當中,Electron 應用程序與嵌入內容之間的所有交互都是非同步進行的,這樣可以保證應用程序與嵌入內容雙方的安全。
<webview id="uinika" src="http://localhost:5000/web/server/electron.html"></webview>
webContents是BrowserWindow對象的一個屬性,負責渲染和控制 Web 頁面。
let window = new BrowserWindow({ width: 600, height: 500 }); window.loadURL("https://uinika.github.io/");
let contents = window.webContents; console.log(contents);
該函數用於打開一個新窗口並載入指定url,調用後將會為該url創建一個BrowserWindow實例,並返回一個BrowserWindowProxy對象,但是該對象只能對打開的url頁面進行有限的控制。正常情況下,如果希望完全控制新窗口,可以直接創建一個新的BrowserWindow。
url
BrowserWindowProxy
// 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
win.focus()
win.print()
win.postMessage(message, targetOrigin)
Electron 的process對象繼承自 NodeJS 的process對象,但是新增了一些有用的事件、屬性、方法。
const { app, BrowserWindow } = require("electron");
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:Workspaceoctopus ode_moduleselectrondist esources });
Chromium 通過將 Web 前端代碼放置在一個與操作系統隔離的沙箱中運行,從而保證惡意代碼不會侵犯到操作系統本身。但是 Electron 中渲染進程可以調用 NodeJS,而 NodeJS 又需要涉及大量操作系統調用,因而沙箱機制默認是禁用的。
某些應用場景下,需要運行一些不確定安全性的外部前端代碼,為了保證操作系統安全,可能需要開啟沙箱機制。此時首先在創建BrowserWindow時傳入sandbox屬性,然後在命令行添加--enable-sandbox參數傳遞給 Electron 即可完成開啟。
sandbox
--enable-sandbox
let win; app.on("ready", () => { window = new BrowserWindow({ webPreferences: { sandbox: true } }); window.loadURL("http://google.com"); });
使用sandbox選項之後,將會阻止 Electron 在渲染器中創建一個 NodeJS 運行時環境,此時新窗口中的window.open() 將按照瀏覽器原生的方式工作。
window.open()
針對 Mac 筆記本電腦上配置的 TouchBar 硬體,Electron 提供了一系列相關的類與操作介面:TouchBar、TouchBarButton、TouchBarColorPicker、TouchBarGroup、TouchBarLabel、TouchBarPopover、TouchBarScrubber、TouchBarSegmentedControl、TouchBarSlider、TouchBarSpacer。
TouchBar
TouchBarButton
TouchBarColorPicker
TouchBarGroup
TouchBarLabel
TouchBarPopover
TouchBarScrubber
TouchBarSegmentedControl
TouchBarSlider
TouchBarSpacer
用於將 PNG 或 JPG 圖片設置為託盤、Dock 和應用程序的圖標。
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等。
HTTP
HTTPS
WS
WSS
FTP
FTPS
<!-- 錯誤 --> <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>
使用BrowserWindow、BrowserView、<webview>載入遠程內容時,都需要通過禁用 NodeJS 集成去限制遠程代碼的執行許可權,避免惡意代碼跨站攻擊。
<!-- 錯誤 --> <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 提供的試驗特性,通過為遠程載入的代碼創造一個全新上下文環境,避免與主進程中的代碼出現衝突或者相互污染。
// 主進程 const mainWindow = new BrowserWindow({ webPreferences: { contextIsolation: true, preload: "preload.js" } });
當頁面嘗試使用某個特性時,會彈出通知讓用戶手動進行確認;而默認情況下,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將導致許多重要的安全性功能被關閉,因此 Electron 默認開啟。
webSecurity
const mainWindow = new BrowserWindow({ webPreferences: { webSecurity: false // 錯誤的做法,預設該屬性使用默認值即可。 } });
內容安全策略 CSP 允許 Electron 通過webRequest對指定 URL 的訪問進行約束,例如允許載入https://uinika.github.io/這個源,那麼https://hack.attacker.com將不會被允許載入,CSP 是處理跨站腳本攻擊、數據注入攻擊的另外一層保護措施。
webRequest
https://uinika.github.io/
https://hack.attacker.com
session.defaultSession.webRequest.onHeadersReceived((details, callback) => { callback({ responseHeaders: { ...details.responseHeaders, "Content-Security-Policy": ["default-src none"] } }); });
使用file://協議打開本地文件時,可以通過元數據標籤<meta>的屬性來添加 CSP 約束。
file://
<meta>
<meta http-equiv="Content-Security-Policy" content="default-src none" />
Electron 默認不允許在 HTTPS 頁面中載入 HTTP 來源的代碼,如果將allowRunningInsecureContent屬性設置為true會禁用這種保護。
allowRunningInsecureContent
const mainWindow = new BrowserWindow({ webPreferences: { allowRunningInsecureContent: true // 錯誤的做法,預設該屬性使用默認值即可。 } });
開發人員可以通過experimentalFeatures屬性啟用未經嚴格測試的 Chromium 實驗性功能,不過 Electron 官方出於穩定性和安全性考慮並不建議這樣做。
experimentalFeatures
const mainWindow = new BrowserWindow({ webPreferences: { experimentalFeatures: true // 錯誤的做法,預設該屬性使用默認值即可。 } });
Blink 是 Chromium 內置的 HTML/CSS 渲染引擎,開發者可以通過enableBlinkFeatures啟用其某些默認是禁用的特性。
enableBlinkFeatures
const mainWindow = new BrowserWindow({ webPreferences: { enableBlinkFeatures: ["ExecCommandInJavaScript"] // 錯誤的做法,預設該屬性使用默認值即可。 } });
開啟allowpopups屬性將使window.open()創建一個新的窗口和BrowserWindows,若非必要狀況,盡量不要使用此屬性。
allowpopups
BrowserWindows
<!-- 錯誤 --> <webview allowpopups src="page.html"></webview>
通過渲染進程創建的<WebView>默認不集成 NodeJS,但是它可以通過webPreferences屬性創建出一個獨立的渲染進程。在<WebView>標籤開始渲染之前,Electron 將會觸發一個will-attach-webview事件,可以通過該事件防止創建具有潛在不安全選項的 Web 視圖。
<WebView>
webPreferences
will-attach-webview
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()並添加額外的判斷來實現這一點。
will- navigation
event.preventDefault()
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 以及相關選項,可以在這個事件中檢查窗口的創建,從而對其進行相應的限制。
web-contents-created
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手動開啟或關閉這些警告。
process.env
ELECTRON_ENABLE_SECURITY_WARNINGS
ELECTRON_DISABLE_SECURITY_WARNINGS
Electron 的發布有別於傳統桌面應用程序編譯打包的發部過程,需要首先下載已經預編譯完成的二進位包,Linux 下二進位包結構如下:
? 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 裏主要有如下兩種部署方式:
1、直接將代碼放置到resources下的子目錄,比如app目錄:
resources
├── app │ ├── package.json │ └── resource │ ├── index.html │ └── main.js ├── default_app.asar └── electron.asar
2、將應用打包加密為asar文件以後放置到resources目錄,比如app.asar文件。
asar
app.asar
├── app.asar ├── default_app.asar └── electron.asar
asar 是一種簡單的文件擴展格式,可以將文件如同tar格式一樣前後連接在一起,支持隨機讀寫,並使用 JSON 來保存文件信息,可以方便的讀取與解析。Electron 通過它可以解決 Windows 文件路徑長度的限制,提高require語句的載入速度,並且避免源代碼泄漏。
tar
首先,需要安裝asar這個 npm 包,然後可以選擇全局安裝然通過命令行使用。
? npm install asar ? asar pack electron-demo app.asar
當然,更加工程化的方式是通過代碼來執行打包操作,就像下面這樣:
let asar = require("asar");
src = "../octopus"; dest = "build/app.asar";
/** 打包完成後的回調函數 */ callback = () => { console.info("asar打包完成!"); };
asar.createPackage(src, dest, callback);
Electron 在 Web 頁面可以通過file:協議讀取 asar 包中的文件,即將 asar 文件視為一個虛擬的文件夾來進行操作。
file:
const { BrowserWindow } = require("electron"); const mainWindow = new BrowserWindow();
mainWindow.loadURL("file:///path/to/example.asar/static/index.html");
如果需要對 asar 文件進行 MD5 或者 SHA 完整性校驗,可以對 asar 檔案文件本身進行操作。
asar list app.asar
RcEdit是一款通過編輯窗口管理器的 rc 文件來對其進行配置的工具,Nodejs 社區提供了node-rcedit工具對 Windows 操作系統的.exe文件進行配置,首先通過npm i rcedit --save-dev為項目安裝該依賴項。
.exe
npm i rcedit --save-dev
var rcedit = require("rcedit");
rcedit(exePath, options, callback);
rcedit()函數包含有如下屬性:
rcedit()
exePath
version-string
file-version
product-version
icon
.ico
requested-execution-level
asInvoker
highestAvailable
requireAdministrator
application-manifest
callback:函數執行完畢之後回調,完整的函數簽名為function(error)。
function(error)