Skip to content
Published at:

Electron上手

来瞅瞅自己电脑哪些软件用了Electron?

bash
$ 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 状态栏(可选)

Notepad++界面示例:

从上往下,依次是:

  • Title:标题
  • Menu bar:菜单栏
  • Tool bar:工具栏
  • Main Window: Tab + Frame 主窗口
  • Status bar:状态栏

跨平台UI框架:

  • QT:C++ + XMLC++ + 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 = ChromiumNode.js

资源

官网:https://www.electronjs.org/ Awesome electron:https://github.com/sindresorhus/awesome-electron#tools

背景知识

开发环境

Overview

Electron开发环境:Nodejs项目 + Electron 开发依赖

安装Nodejs

bash
# 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开发依赖

bash
$ mkdir ${project_name} && cd ${project_name}
$ npm init
$ npm install electron --save-dev	# 添加electron开发依赖

Hello Electron

初始化项目

bash
# init projcet
$ mkdir hello-electron-app && cd hello-electron-app
$ npm init

$ npm install electron --save-dev	# 添加electron开发依赖
$ npm run start										# 运行

添加package.json

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

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

javascript
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浏览器进程模型

  • 一个网页的崩溃不能引起其它网页的崩溃:site-per-process 一个网页一个进程
  • 安全问题:js是一个解释类语言,浏览器要执行从服务器拉过来的代码(有很多位置和不确定性)。如何去限制这些代码的能力?
    • Sanbox沙箱:通过限制对大多数系统资源的访问来限制恶意代码可能造成的危害
  • ......

Electron多进程模型

Electron进程模型:一个Main进程 + 一个或多个Rendered进程

  • 两个进程的能力不一样
    • main进程向上可以通用Nodejs调用操作系统的能力,向上管理渲染进程
    • Rendered进程,用来显示UI
  • 进程之间通信

Main进程

  • 每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。
  • 主进程在 Node.js 环境中运行,使用所有 Node.js API 的能力。

Window management窗口管理

main进程使用BrowserWindow 模块来创建和管理窗口界面

javascript
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 模块来控件应用的生命周期

javascript
// 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可以有更高的权限

示例代码:

image-20220822201718626

IPC进程间通信

文档:https://www.electronjs.org/zh/docs/latest/tutorial/ipc

前面讲的都是多进程的模型,需不需要自已写IPC通信?不需要,已经封装好了。

几种通信方式:

  • 模式 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
javascript
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

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:

javascript
const { BrowserWindow } = require('electron');

const win = new BrowserWindow();
win.webContents.openDevTools();

打包

使用 Electron Forge 打包。

bash
$ 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内核,多进程模型)
  • 对性能要求特别高的场景可能不太合适

Updated at: