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,读者可以基于此来运行本文涉及的示例代码。

construction

Getting Start

首先,让我们通过npm initgit 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.htmlmain.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打开浏览器控制台。

hello-electron

一个 Electron 应用的主进程只会有一个,渲染进程则会有多个。

主进程与渲染进程

  • 主进程main process)管理所有的 web 页面以及相应的渲染进程,它通过BrowserWindow来创建视图页面。
  • 渲染进程renderer processes)用来运行页面,每个渲染进程都对应自己的BrowserWindow实例,如果实例被销毁那么渲染进程就会被终止。

structure

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当中获取BrowserWindowconst 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-closedready等 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对象。

用于主进程,用于创建原生应用菜单和上下文菜单。

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 应用程序菜单和上下文菜单当中。

menu

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 相关的模块 (如 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指定的模块相对路径将会相对于主进程入口点进行解析。
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对象的webContentssession属性来获取。

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

使用SessionWebRequest属性可以访问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)

  1. options 对象

    • types:字符串数组,列出需要捕获的桌面类型是screen还是window
    • thumbnailSize:媒体源缩略图的大小,默认为150x150
  2. 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 属性

webContentsBrowserWindow对象的一个属性,负责渲染和控制 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渲染进程,browserconsole.log(process.versions.node); // NodeJS版本,10.2.0console.log(process.versions.chrome); // Chrome版本,66.0.3359.181console.log(process.versions.electron); //Electron版本,3.0.13console.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 提供了一系列相关的类与操作接口:TouchBarTouchBarButtonTouchBarColorPickerTouchBarGroupTouchBarLabelTouchBarPopoverTouchBarScrubberTouchBarSegmentedControlTouchBarSliderTouchBarSpacer

创建应用图标

用于将 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换成HTTPSWS换成WSSFTP换成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 集成

使用BrowserWindowBrowserView<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) => {// 通知操作系统在默认浏览器上打开URLevent.preventDefault();shell.openExternal(navigationUrl);});
});

Electron 2.0 版本开始,会在可执行文件名为 Electron 时会为开发者在控制台显示安全相关的警告和建议,开发人员也可以在process.envwindow对象上配置ELECTRON_ENABLE_SECURITY_WARNINGSELECTRON_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 里主要有如下两种部署方式:

  1. 直接将代码放置到resources下的子目录,比如app目录:
1
2
3
4
5
6
7
├── app
│   ├── package.json
│   └── resource
│       ├── index.html
│       └── main.js
├── default_app.asar
└── electron.asar
  1. 将应用打包加密为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()函数包含有如下属性:

  1. exePath:需要进行修改的 Windows 可执行文件所在路径。
  2. options:一个拥有如下属性的配置对象。
    • version-string:版本字符串;
    • file-version:文件版本;
    • product-version:产品版本;
    • icon:图标文件.ico的路径;
    • requested-execution-level:需要修改的执行级别(asInvokerhighestAvailablerequireAdministrator)。
    • application-manifest:本地清单文件的路径。
  3. 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 的同学可以移步官网查看更多信息。

使用Electron打造跨平台桌面应用相关推荐

  1. app头像上传vue_Vue+Electron开发跨平台桌面应用实践

    总篇43篇 2019年第17篇 背景 公司去年对 CDN 资源服务器进行了迁移,由原来的通过 FTP 方式的文件存储改为了使用 S3 协议上传的对象存储,部门内 @柴俊堃 同学开发了一个命令行脚本工具 ...

  2. 入坑 Electron 开发跨平台桌面应用

    ‍ 作为一个跨平台的桌面应用开发框架,Electron 的迷人之处在于,它是建立在 Chromium 和 Node.js 之上的 -- 二位分工明确,一个负责界面,一个负责背后的逻辑,典型的「你负责貌 ...

  3. Electron开发跨平台桌面应用

    虽然 B/S 是目前开发的主流,但是 C/S 仍然有很大的市场需求.受限于浏览器的沙盒限制,网页应用无法满足某些场景下的使用需求,而桌面应用可以读写本地文件.调用更多系统资源,再加上 Web 开发的低 ...

  4. 使用Rust + Electron开发跨平台桌面应用 ( 一 )

    前言 近段时间学习了Rust,一直想着做点什么东西深入学习,因为是刚学习,很多地方都不熟悉,所以也就不能拿它来做编译器这些,至于web开发,实际上我并不建议拿这个来学习一门语言,大概有几个方面,一是w ...

  5. (win环境)使用Electron打造一个桌面应用翻译小工具

    初始化项目 npm init 修改package.json {"name": "trans","version": "1.0.0& ...

  6. Electron:新一代基于Web的跨平台桌面技术

    1.引言 现在开发IM应用动不动就要求多端--即Android端.iOS端.PC端.Web端等,Android端和iOS端作为两种不同的移动端技术,单独开发和维护还能理解,PC端和Web端如果要单独开 ...

  7. 用HTML和CSS和JS构建跨平台桌面应用程序的开源库Electron的介绍以及搭建HelloWorld

    场景 Electron介绍 Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库. Electron通过将Chromium和Node.js ...

  8. Electron - 创建跨平台的桌面客户的应用程序

    Electron 框架的前身是 Atom Shell,可以让你写使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序.它是基于io.js 和 Chromium 开源项目,并用于在 ...

  9. vue引用electron_如何搞定跨平台桌面开发?Electron助你快速起步

    嗨,我是勾勾.今天要介绍的是 Electron 跨平台桌面应用开发. Electron(https://electronjs.org/)是一个运行平台,它能够让我们通过 HTML + CSS + Ja ...

最新文章

  1. Python计算机视觉——SIFT特征
  2. linux 文件批量转utf8
  3. [云炬ThinkPython阅读笔记]2.3 表达式和语句
  4. Java中ArrayList的使用
  5. 'htmlentities(): charset `utf8' not supported, assuming utf-8'
  6. leetcode python3 简单题28. Implement strStr()
  7. 数据字典中的数据类型与ABAP中的中数据类型对应关系
  8. Linux安装搜狗输入法
  9. oracle分析函数over(Partition by...)及开窗函数详解
  10. CentOS安装完没有ip地址的解决方法
  11. STM32的PC13、PC14、PC15用作普通IO口设置方法
  12. 阿里语音识别sdk_demo--发送音频数据帧的过程
  13. 反问疑问句的一些用法
  14. CodeForces 74 C.Chessboard Billiard(并查集)
  15. 【SAP-CO】统计指标
  16. 转载(deepin商店下载微信登录显示版本过低无法登录)
  17. 我国现行的计算机软件保护条例是在,等三条例将施行
  18. RuntimeWarning: Glyph 19979 missing from current font.
  19. python:实现布赖恩·克尼汉法算法(附完整源码)
  20. 如何在linux编写perl脚本,关于linux:如何在perl脚本中插入awk命令?

热门文章

  1. 重学Elasticsearch第8章 : SpringBoot整合Jest客户端
  2. mysql提取5号的数据库_五、各类数据库信息的提取
  3. 机器学习——pandas基础
  4. 游戏制作之路(18)隐藏游戏里的鼠标
  5. 智慧城市借助计算机 物联网,面向智慧城市的物联网应用支撑平台解决方案(CCIDIT-IOT)...
  6. 详解USG5500防火墙基础配置
  7. Python数据分析与机器学习28-新闻分类
  8. 【Jetson TX2】下载并安装JetPack
  9. PDAF相位对焦原理
  10. 文明IV: Hints 游戏提示