Electron上手
来瞅瞅自己电脑哪些软件用了Electron?
$ cd /Applications
$ find . | grep Electron
# 精简后
./Visual Studio Code.app/Contents/Frameworks/Electron Framework.framework
./XMind.app/Contents/Frameworks/Electron Framework.framework
./Lark.app/Contents/Frameworks/Electron Framework.framework
./StarUML.app/Contents/Frameworks/Electron Framework.framework
./VMware Fusion.app/Contents/Library/VMware Fusion Applications Menu.app/Contents/Frameworks/Electron Framework.framework
./SwitchHosts.app/Contents/Frameworks/Electron Framework.framework
./aDrive.app/Contents/Frameworks/Electron Framework.framework
./Docker.app/Contents/MacOS/Docker Desktop.app/Contents/Frameworks/Electron Framework.framework
./balenaEtcher.app/Contents/Frameworks/Electron Framework.framework
./Feishu.app/Contents/Frameworks/Electron Framework.framework
./BaiduNetdisk_mac.app/Contents/Frameworks/Electron Framework.framework
./哔哩哔哩.app/Contents/Frameworks/Electron Framework.framework
./斗鱼直播.app/Contents/Frameworks/Electron Framework.framework
./Postman.app/Contents/Frameworks/Electron Framework.framework
./Trello.app/Contents/Frameworks/Electron Framework.framework
./AdGuard for Safari.app/Contents/Frameworks/Electron Framework.framework
./新东方大学考试.app/Contents/Frameworks/Electron Framework.framework
./Discord.app/Contents/Frameworks/Electron Framework.framework
./QQ.app/Contents/MacOS/QQGuild.app/Contents/Frameworks/Electron Framework.framework
老实说有被这个搜索结果吓到,知道有一些软件是用Electron写的,没想到会有这么多;网盘和直播基本都在用Electron,抖音最近也推出了桌面估计跑不了;另外有些app只用它写部分模块,比如,VMware只用它写了菜单,QQ只用它写了QQGuild.app
频道;不过最近QQ内测版本被爆出也在用Electron重写主界面。
另外一个值得注意的问题是,不少桌面软件现在也有做前-后端分离,所以部分软件只是用Electron是重写前端-界面展示的部分,后端或者核心部分可以用其它语言去实现,C++/Rust之类,并不是用Electron写所有部分
网页体验不好显示慢的误区:
- 慢的原因:显示的Html文件是从远程服务器下载的,慢的大多时间都花在去下载Html文件上面
- 本地的Html渲染显示并不慢(其它UI框架Android UI、Qt Widgets也是显示本地的xml布局Layout文件)
简单认识桌面软件开发
桌面软件通用布局:
Menu bar
菜单栏Main Window
主窗口Status bar 状态栏
(可选)
![](/assets/image-20220822140545744.DiRmoLEN.webp)
Notepad++界面示例:
从上往下,依次是:
Title
:标题Menu bar
:菜单栏Tool bar
:工具栏Main Window
:Tab + Frame
主窗口Status bar
:状态栏
![](/assets/image-2022082210627361_PM.Ds5AOp0g.webp)
跨平台UI框架:
- QT:
C++ + XML
(C++ + QML、JS
)- WPS
- Electron: JavaScript、HTML 和 CSS
- VSCode:Electron
- Feishu:
/Applications/Feishu.app/Contents/Frameworks/Electron Framework.framework
+ Rust(业务) - Dingtalk:node-webkit --> Chromium Embedded Framework --> Native UI + 前端 + fluttter
- JAVA GUI:
- Idea、Android Studio
其他:专属平台,比如:Windows平台的C#
(比如:VS、ITE Designer),macOS用SwiftUI(苹果自已的软件也用了不少js写界面),Linux平台上用的较多的GTK
Electron
Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架 Electron = Chromium 和 Node.js
资源
官网:https://www.electronjs.org/ Awesome electron:https://github.com/sindresorhus/awesome-electron#tools
背景知识
- Web 入门 (MDN Web Docs) :JS + BOM + DOM
- NodeJS官方入门指南:暴露操作系统的接口,及常用工具库
- Electron API
开发环境
Overview
Electron开发环境:Nodejs项目 + Electron 开发依赖
安装Nodejs
# macOS
$ brew install node@16
# linux
$ wget https://nodejs.org/dist/v16.17.0/node-v16.17.0-linux-x64.tar.xz # 下载Nodejs
$ sudo tar -xvf node-v16.17.0-linux-x64.tar.xz --directory /opt # 解压
$ echo 'export PATH=/opt/node-v16.17.0-linux-x64/bin:$PATH' >> ~/.bashrc # 添加环境变量
$ source ~/.bashrc # 让刚刚设置的环境变量生效
# verify
$ node --version
$ npm --version
# install yarn
$ npm install --global yarn
$ yarn --version
项目添加Electron开发依赖
$ mkdir ${project_name} && cd ${project_name}
$ npm init
$ npm install electron --save-dev # 添加electron开发依赖
Hello Electron
初始化项目
# init projcet
$ mkdir hello-electron-app && cd hello-electron-app
$ npm init
$ npm install electron --save-dev # 添加electron开发依赖
$ npm run start # 运行
添加package.json
{
"name": "hello-electron-app",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
// 运行脚本
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "shibin",
"license": "MIT",
"devDependencies": {
"electron": "^20.0.3"
}
}
界面index.html
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
</body>
</html>
界面创建加载main.js
const { app, BrowserWindow } = require('electron');
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
});
win.loadFile('index.html');
};
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
小结
- 简单的说:是一个node项目,添加了electron的开发依赖
- 网页显示慢的误区:
- 慢的原因:显示的html是从远程的服务器去下载的,大多时间都花在去下载上面
- 本地的Html渲染显示并不慢,(Android、Qt、ITE同理,显示本地的xml)
进程模型
Chrome浏览器进程模型
![](/assets/image-2022082220956421_PM.B2of-KDB.webp)
- 一个网页的崩溃不能引起其它网页的崩溃:
site-per-process
一个网页一个进程 - 安全问题:js是一个解释类语言,浏览器要执行从服务器拉过来的代码(有很多位置和不确定性)。如何去限制这些代码的能力?
- Sanbox沙箱:通过限制对大多数系统资源的访问来限制恶意代码可能造成的危害
- ......
![](/assets/image-20220822180000566.DHCxvbw9.webp)
Electron多进程模型
Electron进程模型:一个Main进程 + 一个或多个Rendered进程
![](/assets/image-20220822180400610.YewUT6q2.webp)
- 两个进程的能力不一样
- main进程向上可以通用Nodejs调用操作系统的能力,向上管理渲染进程
- Rendered进程,用来显示UI
- 进程之间通信
Main进程
- 每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。
- 主进程在 Node.js 环境中运行,使用所有 Node.js API 的能力。
Window management窗口管理
main进程使用BrowserWindow
模块来创建和管理窗口界面
const { BrowserWindow } = require('electron');
const win = new BrowserWindow({ width: 800, height: 1500 });
win.loadURL('https://github.com');
const contents = win.webContents;
console.log(contents);
Application lifecycle
Main进程可以用app
模块来控件应用的生命周期
// quitting the app when no windows are open on non-macOS platforms
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Native APIs
Main进程与Native系统的API交互;比如菜单、对话框和托盘图标。(不同系统的菜单、对话框有差异,兼容不同系统)
Rendered渲染进程
每个 Electron 应用程序为每个打开BrowserWindow生成一个单独的渲染器进程。顾名思义,渲染进程负责 渲染网页内容,在渲染器进程中运行的代码应该按照 Web 标准运行
Web 规范:
- HTML 文件是渲染器进程的入口点。
- UI 样式是通过级联样式表 (CSS) 添加的。
- 可以通过
<script>
元素添加可执行的 JavaScript 代码。
渲染器进程无法直接访问require
或其他 Node.js API
Perload预加载脚本
文档:https://www.electronjs.org/zh/docs/latest/tutorial/context-isolation
- Preload脚本在web内存加载前执行
- 运行在Renderer渲染进程的上下文里面
- 可以通过访问 Node.js API 被授予更多权限
通俗的讲preload.js是Rendered渲染进程(低权限进程)的一个代理、客户端,通过preload.js可以有更高的权限
![](/assets/image-20220822203128238.CTEaB3iV.webp)
示例代码:
IPC进程间通信
文档:https://www.electronjs.org/zh/docs/latest/tutorial/ipc
前面讲的都是多进程的模型,需不需要自已写IPC通信?不需要,已经封装好了。
- Main进程中使用
ipcMain
模块 - Rendered进程中使用
ipcRenderer
模块
几种通信方式:
- 模式 1:渲染器进程到主进程(单向)
- 模式 2:渲染器进程到主进程(双向)
- 模式 3:主进程到渲染器进程
- 模式 4:渲染器进程到渲染器进程
- 将主进程作为渲染器之间的消息代理
- 从主进程将一个 MessagePort 传递到两个渲染器
模块
- app,控制应用的事件生命周期。
- BrowserWindow,它负责创建和管理应用的窗口。
常见模块
进程能力不一样,分类:
- Main进程模块
- Rendered进程模块:preload.js
Electron 遵循 JavaScript 传统约定:比如 app 和 BrowserWindow 两个模块名的大小写差异。
- 以帕斯卡命名法 (PascalCase) 命名可实例化的类 (如 BrowserWindow, Tray 和 Notification),
- 以驼峰命名法 (camelCase) 命名不可实例化的函数、变量等 (如 app, ipcRenderer, webContents) 。
Main进程模块
- app应用
- BrowserWindow浏览器窗口
- Menu菜单
- dialog对话框
- Notification通知
- ipcMain进程通信
- net网络
- 函数:
net.isOnline()
- 函数:
- autoUpdater自已更新模块;服务端配置下就可以了
- 函数:
autoUpdater.checkForUpdates()
- 函数:
autoUpdater.quitAndInstall()
- 函数:
Rendered进程模块
- contextBridge
- ipcRenderer
Menu菜单代码示例
![](/assets/image-20220823100410246.BqvjExCe.webp)
const { app, Menu } = require('electron');
const isMac = process.platform === 'darwin';
const template = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
},
]
: []),
{
label: 'File',
submenu: [
{
label: 'New',
accelerator: process.platform === 'darwin' ? 'Cmd+N' : 'Ctrl+N',
click: () => {
console.log('Electron rocks!');
},
},
{
label: 'New Tab',
click: () => {
console.log('Electron rocks!');
},
},
{
label: 'New Windows',
click: () => {
console.log('Electron rocks!');
},
},
{ type: 'separator' },
{
label: 'Open...',
click: () => {
console.log('Electron rocks!');
},
},
{
label: 'Open Recent',
click: () => {
console.log('Electron rocks!');
},
},
{
label: 'Open Quickly...',
click: () => {
console.log('Electron rocks!');
},
},
{ type: 'separator' },
],
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
],
},
{
label: 'Edit',
submenu: [{ role: 'reload' }],
},
{
label: 'Pargraph',
submenu: [{ label: 'foo' }],
},
{
label: 'Format',
submenu: [{ label: 'foo' }],
},
{
label: 'View',
submenu: [{ label: 'foo' }],
},
{
label: 'Theme',
submenu: [{ label: 'foo' }],
},
{
label: 'Windows',
submenu: [{ label: 'foo' }],
},
{
label: 'Help',
submenu: [{ label: 'foo' }],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
和硬件交互
https://web.dev/devices/ 从网站上通过蓝牙、USB、NFC、串行和 HID 与硬件设备进行通信。
Web & 设备/硬件
https://www.electronjs.org/zh/docs/latest/tutorial/deviceshttps://web.dev/devices/
和硬件设备的交互能力不用太担心,但还需要测试:
- 浏览器及JS引擎是C/C++写的
- Nodejs底下也是C/C++写的
- Web正在把这部分标准化
调试
VS Code 调试
配置文件.vscode/launch.json
:
{
"version": "0.2.0",
"compounds": [
{
"name": "Main + renderer",
"configurations": ["Main", "Renderer"],
"stopAll": true
}
],
"configurations": [
{
"name": "Renderer",
"port": 9222,
"request": "attach",
"type": "pwa-chrome",
"webRoot": "${workspaceFolder}"
},
{
"name": "Main",
"type": "pwa-node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": [".", "--remote-debugging-port=9222"],
"outputCapture": "std",
"console": "integratedTerminal"
}
]
}
三个配置项:
Main
用来运行主程序,并且暴露出 9222 端口用于远程调试 (--remote-debugging-port=9222
) 。 我们将把调试器绑定到那个端口来调试renderer
。 因为主进程是一个 Node.js 进程,type 被设置为pwa-node
(pwa-
这个前缀用来告诉 VS Code,使用最新的 JavaScript 调试器) 。Renderer
用来调试渲染器进程。 因为后者是由主进程创建的,我们要把它 “绑定” 到主进程上 ()"request": "attach"
,而不是创建一个新的。 渲染器是 web 进程,因此要选择pwa-chrome
调试器。Main + renderer
是一个 复合任务,可同时执行前两个任务。
日志输出
因为是多进程的模型,不同的进程输出到不同的地方
- Main进程输出到Terminal启动终端
- Redered渲染进程输出到开发者工具控制台
Redered渲染进程打开DevTools:
const { BrowserWindow } = require('electron');
const win = new BrowserWindow();
win.webContents.openDevTools();
打包
使用 Electron Forge 打包。
$ npm install --save-dev @electron-forge/cli
$ npx electron-forge import
# 打包,生成在out目录下
$ npm run make # ./out/my-electron-app-darwin-arm64/my-electron-app.app
总结
- 很多成熟的商业应用(杀手级应用):VSCode、Facebook Messager、Twitch、InVision
- Js、html、css开发成本相对Qt / C++较低
- 开发效率/周期
- 对人素质的要求相对Cpp较低
- JS没有多线程(回调 --> promise,JS底下是多线程)
- 前端网页本地加载的速度并不慢,网页显示的耗时大多在网络下载上;Android、QT这种也是加载的本地XML
- Electron有很多
out-of-box
开箱即用的模块:自已更新 - Nodejs生态,有很多库可以用
- JavaScript和JSON就是一个东西:配置、协议都用jSON,少写JSON转换结构体的代码
- 打包程序至少80MB网上
- 对运行内存的要求相对也较高(内嵌Chromium内核,多进程模型)
- 对性能要求特别高的场景可能不太合适