早期桌面應用的開發主要藉助原生 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,結合博客的書籤進行閱讀,將會更加方便與直觀,需要的同學請點擊下面直達鏈接。

使用Electron打造跨平臺桌面應用?

uinika.github.io
圖標

Getting Start

首先,讓我們通過npm initgit init新建一個項目,然後通過如下npm語句安裝最新的 Electron 穩定版。

? npm i -D electron@latest

然後向項目目錄下的package.json文件添加一條scripts語句,便於後面通過npm start命令啟動 Electron 應用。

{
// ... ...
"author": "Hank",
"main": "resource/main.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^3.0.7"
}
}

然後在項目根目錄下新建resource文件夾,裡面分別再建立index.htmlmain.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 能夠跨平臺執行的原因所在。

<!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就只能用於主進程。

const { BrowserWindow } = require("electron");

ccc = new BrowserWindow();

Electron 通過remote模塊暴露一些主進程的 API,如果需要在渲染進程中創建一個BrowserWindow實例,那麼就可以藉助這個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")所獲取的模塊進行概述,便於後期進行分類查找。

app 模塊

Electron 提供的app模塊即提供了可用於區分開發和生產環境的app.isPackaged屬性,也提供了關閉窗口的app.quit()和用於退出程序的app.exit()方法,以及window-all-closedready等 Electron 程序事件。

const { app } = require("electron");
app.on("window-all-closed", () => {
app.quit(); // 當所有窗口關閉時退出應用程序
});

可以使用app.getLocale()獲取當前操作系統的國際化信息。

BrowserWindow 模塊

工作在主進程,用於創建和控制瀏覽器窗口。

// 主進程中使用如下方式獲取。
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即可:

const { BrowserWindow } = require("electron");
let window = new BrowserWindow({ width: 800, height: 600, frame: false });
window.show();

例如載入頁面時,渲染進程第一次完成繪製時BrowserWindow會發出ready-to-show事件。

const { BrowserWindow } = require("electron");
let win = new BrowserWindow({ show: false });
win.once("ready-to-show", () => {
win.show();
});

對於較為複雜的應用程序,ready-to-show事件的發出可能較晚,會讓應用程序的打開顯得緩慢。 這種情況下,建議通過backgroundColor屬性設置接近應用程序背景色的方式顯示窗口,從而獲取更佳的用戶體驗。

const { BrowserWindow } = require("electron");

let window = new BrowserWindow({ backgroundColor: "#272822" });
window.loadURL("https://uinika.github.io/");

如果想要創建子窗口,那麼可以使用parent選項,此時子窗口將總是顯示在父窗口的頂部。

const { BrowserWindow } = require("electron");

let top = new BrowserWindow();
let child = new BrowserWindow({ parent: top });

child.show();
top.show();

創建子窗口時,如果需要禁用父窗口,那麼可以同時設置modal選項。

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()方法註冊快捷鍵。

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()等方法。

const { clipboard } = require("electron");
clipboard.writeText("一些字元串內容");

globalShortcut 模塊

用於在 Electron 應用程序失去鍵盤焦點時監聽全局鍵盤事件,即在操作系統中註冊或註銷全局快捷鍵。

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 模塊

用於主進程到渲染進程的非同步通信,下面是一個主進程與渲染進程之間發送和處理消息的例子:

// 主進程
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對象。

Menu 與 MenuItem 模塊

用於主進程,用於創建原生應用菜單和上下文菜單。

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 模塊

用於記錄網路日誌。

const { netLog } = require("electron");

netLog.startLogging("/user/log.info");
/** 一些網路事件發生之後 */
netLog.stopLogging(path => {
console.log("網路日誌log.info保存在", path);
});

powerMonitor 模塊

通過 Electron 提供的powerMonitor模塊監視當前電腦電源狀態的改變,值得注意的是,在app模塊的ready事件被觸發之前, 不能引用或使用該模塊。

const electron = require("electron");
const { app } = electron;

app.on("ready", () => {
electron.powerMonitor.on("suspend", () => {
console.log("系統將要休眠了!");
});
});

powerSaveBlocker 模塊

阻止操作系統進入低功耗 (休眠) 模式。

const { powerSaveBlocker } = require("electron");

const ID = powerSaveBlocker.start("prevent-display-sleep");
console.log(powerSaveBlocker.isStarted(ID));

powerSaveBlocker.stop(ID);

protocol 模塊

註冊自定義協議並攔截基於現有協議的請求,例如下面代碼實現了一個與[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 模塊

net模塊是一個發送 HTTP(S) 請求的客戶端 API,類似於 NodeJS 的 HTTP 和 HTTPS 模塊 ,但底層使用的是 Chromium 原生網路庫。

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 相關的模塊 (如 dialogmenu 等) 僅在主進程中可用, 在渲染進程中不可用,所以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指定的模塊相對路徑將會相對於主進程入口點進行解析。

project/
├── main
│ ├── helper.js
│ └── index.js
├── package.json
└── renderer
└── index.js

remote模塊提供的主進程與渲染進程通信方法比ipcMain/ipcRenderer更加易於使用。

// 主進程: 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!

screen 模塊

檢索有關屏幕大小、顯示器、遊標位置等信息,應用的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");
});

shell 模塊

提供與桌面集成相關的功能,例如可以通過調用操作系統默認的應用程序管理文件或Url

const { shell } = require("electron");

shell.openExternal("https://github.com");

systemPreferences 模塊

獲取操作系統特定的偏好信息,例如在 Mac 下可以通過下面代碼獲取當前是否開啟系統 Dark 模式的信息。

const { systemPreferences } = require("electron");
console.log(systemPreferences.isDarkMode()); // 返回一個布爾值。

Tray 模塊

用於主進程,添加圖標和上下文菜單至操作系統通知區域。

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 腳本等等。

const { webFrame } = require("electron");
webFrame.setZoomFactor(5); // 將頁面縮放至500%。

session 模塊

Electron 的session模塊可以創建新的session對象,主要用來管理瀏覽器會話、cookie、緩存、代理設置等等。

如果需要訪問現有頁面的session,那麼可以通過BrowserWindow對象的webContentssession屬性來獲取。

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 實例。

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

使用SessionWebRequest屬性可以訪問WebRequest類的實例,WebRequest類可以在 HTTP 請求生命週期的不同階段修改相關內容,例如下面代碼為 HTTP 請求添加了一個User-Agent協議頭:

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)

  1. options 對象
  • types:字元串數組,列出需要捕獲的桌面類型是screen還是window
  • thumbnailSize:媒體源縮略圖的大小,默認為150x150
  • callback 回調函數,擁有如下 2 個參數:
  • 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);
}

dialog 模塊

調用操作系統原生的對話框,工作在主線程,下面示例展示了一個用於選擇多個文件和目錄的對話框:

const { dialog } = require("electron");
console.log(dialog.showOpenDialog({ properties: ["openFile", "openDirectory", "multiSelections"] }));

由於對話框工作在 Electron 的主線程上,如果需要在渲染器進程中使用, 那麼可以通過remote來獲得:

const { dialog } = require("electron").remote;
console.log(dialog);

contentTracing 模塊

從 Chromium 收集跟蹤數據,從而查找性能瓶頸。使用後需要在瀏覽器打開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);
});
});

webview 標籤

Electron 的<webview>標籤基於 Chromium,由於開發變動較大官方並不建議使用,而應考慮<iframe>或者 Electron 的BrowserView等選擇,或者完全避免在頁面進行內容嵌入。

<webview><iframe>最大不同是運行於不同的進程當中,Electron 應用程序與嵌入內容之間的所有交互都是非同步進行的,這樣可以保證應用程序與嵌入內容雙方的安全。

<webview id="uinika" src="http://localhost:5000/web/server/electron.html"></webview>

webContents 屬性

webContentsBrowserWindow對象的一個屬性,負責渲染和控制 Web 頁面。

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

// 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對象,但是新增了一些有用的事件、屬性、方法。

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:Workspaceoctopus
ode_moduleselectrondist
esources
});

沙箱機制

Chromium 通過將 Web 前端代碼放置在一個與操作系統隔離的沙箱中運行,從而保證惡意代碼不會侵犯到操作系統本身。但是 Electron 中渲染進程可以調用 NodeJS,而 NodeJS 又需要涉及大量操作系統調用,因而沙箱機制默認是禁用的。

某些應用場景下,需要運行一些不確定安全性的外部前端代碼,為了保證操作系統安全,可能需要開啟沙箱機制。此時首先在創建BrowserWindow時傳入sandbox屬性,然後在命令行添加--enable-sandbox參數傳遞給 Electron 即可完成開啟。

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 提供了一系列相關的類與操作介面:TouchBarTouchBarButtonTouchBarColorPickerTouchBarGroupTouchBarLabelTouchBarPopoverTouchBarScrubberTouchBarSegmentedControlTouchBarSliderTouchBarSpacer

創建應用圖標

用於將 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換成HTTPSWS換成WSSFTP換成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>

載入外部內容時禁用 NodeJS 集成

使用BrowserWindowBrowserView<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 會自動批准所有的許可請求。

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 默認開啟。

const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false // 錯誤的做法,預設該屬性使用默認值即可。
}
});

定義 CSP 安全策略

內容安全策略 CSP 允許 Electron 通過webRequest對指定 URL 的訪問進行約束,例如允許載入https://uinika.github.io/這個源,那麼https://hack.attacker.com將不會被允許載入,CSP 是處理跨站腳本攻擊、數據注入攻擊的另外一層保護措施。

const { session } = require("electron");

session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": ["default-src none"]
}
});
});

使用file://協議打開本地文件時,可以通過元數據標籤<meta>的屬性來添加 CSP 約束。

<meta http-equiv="Content-Security-Policy" content="default-src none" />

別設置 allowRunningInsecureContent 為 true

Electron 默認不允許在 HTTPS 頁面中載入 HTTP 來源的代碼,如果將allowRunningInsecureContent屬性設置為true會禁用這種保護。

const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true // 錯誤的做法,預設該屬性使用默認值即可。
}
});

不要開啟實驗性功能

開發人員可以通過experimentalFeatures屬性啟用未經嚴格測試的 Chromium 實驗性功能,不過 Electron 官方出於穩定性和安全性考慮並不建議這樣做。

const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true // 錯誤的做法,預設該屬性使用默認值即可。
}
});

不要使用 enableBlinkFeatures

Blink 是 Chromium 內置的 HTML/CSS 渲染引擎,開發者可以通過enableBlinkFeatures啟用其某些默認是禁用的特性。

const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: ["ExecCommandInJavaScript"] // 錯誤的做法,預設該屬性使用默認值即可。
}
});

禁用 webview 的 allowpopups

開啟allowpopups屬性將使window.open()創建一個新的窗口和BrowserWindows,若非必要狀況,盡量不要使用此屬性。

<!-- 錯誤 -->
<webview allowpopups src="page.html"></webview>

<!-- 正確 -->
<webview src="page.html"></webview>

驗證 webview 選項與參數

通過渲染進程創建的<WebView>默認不集成 NodeJS,但是它可以通過webPreferences屬性創建出一個獨立的渲染進程。在<WebView>標籤開始渲染之前,Electron 將會觸發一個will-attach-webview事件,可以通過該事件防止創建具有潛在不安全選項的 Web 視圖。

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()並添加額外的判斷來實現這一點。

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 以及相關選項,可以在這個事件中檢查窗口的創建,從而對其進行相應的限制。

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.envwindow對象上配置ELECTRON_ENABLE_SECURITY_WARNINGSELECTRON_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目錄:

├── app
│ ├── package.json
│ └── resource
│ ├── index.html
│ └── main.js
├── default_app.asar
└── electron.asar

2、將應用打包加密為asar文件以後放置到resources目錄,比如app.asar文件。

├── app.asar
├── default_app.asar
└── electron.asar

asar 打包源碼

asar 是一種簡單的文件擴展格式,可以將文件如同tar格式一樣前後連接在一起,支持隨機讀寫,並使用 JSON 來保存文件信息,可以方便的讀取與解析。Electron 通過它可以解決 Windows 文件路徑長度的限制,提高require語句的載入速度,並且避免源代碼泄漏。

首先,需要安裝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 文件視為一個虛擬的文件夾來進行操作。

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 編輯可執行文件

RcEdit是一款通過編輯窗口管理器的 rc 文件來對其進行配置的工具,Nodejs 社區提供了node-rcedit工具對 Windows 操作系統的.exe文件進行配置,首先通過npm i rcedit --save-dev為項目安裝該依賴項。

var rcedit = require("rcedit");

rcedit(exePath, options, callback);

rcedit()函數包含有如下屬性:

  1. exePath:需要進行修改的 Windows 可執行文件所在路徑。
  2. options:一個擁有如下屬性的配置對象。3.
  • version-string:版本字元串;
  • file-version:文件版本;
  • product-version:產品版本;
  • icon:圖標文件.ico的路徑;
  • requested-execution-level:需要修改的執行級別(asInvokerhighestAvailablerequireAdministrator)。
  • application-manifest:本地清單文件的路徑。

callback:函數執行完畢之後回調,完整的函數簽名為function(error)


推薦閱讀:
相關文章