Appearance
Electron 开发
概述
Electron 是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在 Windows 上运行的跨平台应用 macOS 和 Linux——不需要本地开发 经验;
本文将基于 electron 18.x、vue3、vite 2.0 来实现客户端开发;
起步
新建Electron
项目,只需要新建正常的 node 工程项目并将electron
添加到开发依赖即可;
electron 项目的启动也很简单,只需要在命令行中加键入electron .
,electron 会自动执行根目录下的main.js
作为入口文件来启动客户端,当然,你也可以通过指定 electron 入口文件的路径来启动客户端;
入口文件
electron 的入口文件管理着客户端的生命周期,开发者可以通过使用不同的生命周期钩子函数来实现应用窗口的加载、销毁、更新等等操作;
下面实现一个最基础的客户端页面加载;
// app是客户端全局对象,BrowserWindow用于实例化窗口
const { app, BrowserWindow } = require("electron");
const createWindow = () => {
// 通过BrowserWindow类来创建一个宽高为800x600的窗口
const win = new BrowserWindow({
width: 800,
height: 600,
});
// 窗口对象加载本地文件`index.html`来作为窗口内容
win.loadFile("index.html");
};
// 客户端生命周期`ready`后,即客户端启动,可用来加载窗口
app.whenReady().then(() => {
createWindow();
});
进程
主进程与渲染进程
electron
的入口文件即主进程,而渲染进程就是维护 html 文件渲染的进程,主进程控制这窗口的创建、管理着应用的生命周期,渲染进程负责界面渲染、人机交互等工作;
单个electron
应用只有一个主进程,但该主进程可以同时维护多个渲染进程(创建多个窗口界面),多个同时存在的渲染进程由各自的 browserWindow 实例管理,当browserWindow
实例销毁时,对应的渲染进程被终结,且各个渲染进程之间相互隔离,只有当browserWindow
实例化时,开启nodeIntegration
选项,才能使渲染进程获得访问 Node.js 相关 API 的能力(有一定的安全问题);
Electron 内置主要模块归属:
归属情况 | 模块名 |
---|---|
主进程模块 | app、autoUpdater、browserWindow、contentTracing、dialog、globalShortcut、ipcMainMenu、MenuItem、net、netLog、Notification、powerMonitor、powerSaveBlocker、protocol、screen、session、systemPreference、TouchBar、Tray、webContents |
渲染进程模块 | desktopCapturer、ipcRenderer、remote、webFrame |
公用模块 | clipboard、crashReporter、nativeImage、shell |
调试进程
调试主进程,其实就是调试一般 node 程序,这里用vsCode
调试为例:
{
// .vscode目录下新建launch.json
"version": "0.2.0",
"configurations": [
{
// 配置启动类型为node,当前工作区为启动路径,用node_modules下的electron启动main.js
"type": "node",
"request": "launch",
"name": "Electron main progress",
"cwd": "${workspaceFolder}",
"args": ["."],
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"outputCapture": "std"
}
]
}
调试渲染进程,electron
是基于 chromium 实现的,其渲染进程就是正常的渲染页面的逻辑,所以在窗口渲染出来的时候,我们可以在界面中使用 chromium 的 devtool 来进行调试,唤起devTool
的方法是在 electron 界面菜单的View
下来菜单中选中Toggle Developer Tool
或者是键入快捷键Ctrl + shift + I
(正常来说,鼠标右击是没有右键菜单的);
除此之外,还可以在browserWindow
实例 loadFile 或 loadURL 之后,调用实例的webContents.openDevTools
方法来唤起devTool
;
进程互访
渲染进程访问主进程对象
为browserWindow
实例开启nodeintegration
配置项,渲染进程就可以通过require
引入electron
,使用electron
的remote
模块来访问browserWindow
实例对象,具体操作如下:
<script>
let { remote } = require("electron");
remote.getCurrentWindow().webContents.openDevTools();
</script>
注意点
remote
对象的属性和方法都是主进程的属性和方法的映射;除了使用remote.getCurrentWindow().webContents
来获取渲染进程实例的webContents
之外,还可以通过remote.getCurrentWebContents
方法来获取到webContents
对象;
渲染进程访问主进程类型
当然,在remote
模块上,不止可以获取到进程实例、webContents
,还可以通过remote
模块访问主进程的 app、BrowserWindow 等对象的类型,
<script>
let { remote } = require("electron");
win = new remote.BrowserWindow({
width: 200,
height: 200,
nodeIntegration: true,
});
</script>
在表现上,貌似是在一个渲染进程内维护了另一个渲染进程,但实际上这种操作的背后,还是在主进程进行处理, remote 模块只是通知主进程完成相关的操作;
渲染进程访问主进程自定义内容
上文介绍了通过remote
模块访问electron
的内部对象和类型,我们还可以通过remote.require
方法来加载自定义内容,
<script>
let { remote } = require("electron");
let modules = remote.require("./modules.js");
</script>
注意点
不能去除 remote,直接使用 require 来加载 node 模块,否则该模块内的electron
对象是不可达的(会报错);
主进程访问渲染进程对象
主进程中是没有remote
模块的,不能像渲染进程那样通过remote
模块来访问进程,但在主进程上,还是可以访问渲染进程的某些接口的(刷新页面、打印页面等等);
进程通信
渲染进程向主进程发送消息
渲染进程使用electron
内置的ipcRenderer
模块可以向主进程发送自定义消息(发布-订阅模式/事件总线通讯),
<script>
let { ipcRenderer } = require("electron");
ipcRenderer.send("msgEventName", "payload");
</script>
在主进程中,使用ipcMain
对象来监听消息,
let { ipcMain } = require(electron);
ipcMain.on("msgEventName", (event, payload) => {
console.log(payload);
});
注意点
进程间进行通信,在通信过程中,消息发送的 json 对象都会被序列化和返序列化,所以 json 对象上的方法和原型链上的数据不会被传送;
在 ipcMain 的消息监听的回调函数中,其第一个参数是事件对象,event.sender
是渲染进程的webContents
对象实例;
消息传递都是异步操作,如需要同步操作,可使用icpRenderer.sendSync
来发送消息,但该方法是同步的,所以未接收到返回数据前,会阻塞代码运行;
主进程在接收到同步消息后,通过设置event.retrunValue
即可给同步消息设置返回值,结束同步消息;
主进程向渲染进程发送消息
主进程通过browserWindow
实例上的webContents
上的send
方法来向渲染进程发送消息,
let win = new BrowserWindow({});
win.webContents.send("msgEventName", "payload");
在渲染进程上,使用ipcRenderer.on
来接收消息,
<script>
let { ipcRenderer } = require("electron");
ipcRenderer.on("msgEventName", (...payload) => {
console.log(payload);
});
</script>
注意点
上一小节中提到,ipcMain
监听事件的事件回调的第一参数event
的event.sender
就是渲染进程的webContents
,这样的话,我们同样可以使用event.sender.send
方法来向渲染进程推送消息,其次,还可以通过event.reply
方法来回应对应渲染进程的消息;
渲染进程间传递消息
多个同时存在的渲染进程进行通讯,通过主进程进行中转派发,有一种简化写法是利用ipcRenderer.sendTo
方法,在已知对方渲染进程的webContents.id
的情况下,可以省略主进程的转发函数;
<script>
ipcRenderer.sendTo("别的进程id", "msgEventName", "payload");
</script>
remote 模块的局限性
remote
模块大大降低了进程通讯的难度,但其存在的问题却不可小觑,大致有一下四点;
- 性能消耗大,通过
remote
模块进行访问主进程的对象、方法、属性操作都是跨进程的,对性能是有消耗的; - 制造混乱,当
remote
模块使用了主进程的某个对象,此对象在某一时刻触发一个事件,事件处理函数是在渲染进程中注册的,那么在事件发生时,实际上是主进程的原始对象先接收到事件通知,再异步通知给渲染进程,这个过程是耗时的,所以在此过程中会错过很多时机(例如 event.preventDefault()将变得毫无意义),在业务复杂的应用中,这类错误将难以排查; - 制造假象,
remote
模块访问主进程的某个对象,得到的是该对象的映射,这就造成了一个假象,就是对象上的原型属性无法映射,NaN、Infinity 等等特殊值会转换成 undefined; - 存在安全问题,因为
remote
模块底层是通过 IPC 管道与主进程通讯的,如果应用内加载第三方页面,即使该页面运行的安全杀星中,恶意代码仍可以通过原型污染共计来模拟remote
模块的远程消息来获取访问主进程的能力,导致安全问题;
窗口
窗口常用属性及应用场景
- 窗口位置:x、y、movable,通过 x、y 来控制窗口在屏幕的位置,常用于窗口定位(不设置时,默认窗口在屏幕居中);
- 窗口大小:width、height、minWidth、minHeight、maxWidth、maxHeight、resizable、minimizable、maximizable,从词义可以看出,这些属性是控制窗口宽高及拖拽设置宽高、全屏/最小化;
- 边框、标题、菜单栏:title、icon、frame、autoHideMenuBar、titleBarStyle,将 frame 设为 false 可屏蔽系统标题栏(一般用在定制标题栏时,自定义标题栏时,可以为标题栏加上
electron
特有 css:-webkit-app-region:drag
来标记该元素可被用户拖拽),还有一点需要注意的是,在 Mac 系统下,系统菜单关系到"复制"/"粘贴"等快捷指令,所以在禁用系统菜单时,需要考虑这个兼容点;
webPreferences
- 渲染进程访问 Node.js 的能力:nodeIntegration、nodeIntegrationInWoker、nodeIntegrationInSubFrames,这些属性都控制这窗口是否集成 node.js 环境,当打开这些属性时,应确保窗口页面不包含第三方提供的内容,否则第三方页面有可能越权访问 node.js 环境,导致安全问题;
- 增强渲染进程能力:preload、webSecurity、contextIsolation,允许开发者最大限度的控制渲染进程加载的页面,preload 配置项使开发者可以为渲染进程加载的页面注入脚本,就算渲染进程加载的第三方页面,且开发者关闭 nodeIntegration,注入的脚本仍有能力访问 Node.js 环境,webSecurity 配置项控制这页面的同源策略,开发者可自行选择是否关闭(解决跨域);
窗口实例方法
通过browserWindow
实例的窗口实例中,可调用一系列方法(文档位置)来控制窗口,
- 最大化/最小化、关闭窗口:maxmize、minimize、restore、close,使用这些方法的时候需要注意在窗口实例化前是否关闭了对应的窗口控制能力(maxmizable、resizable 等等),监听
maximize
/unmaximize
、minimize
(文档介绍)等等事件可得到窗口状态; - getBounds:返回一个
Rectangle
对象,反应窗口相对屏幕的坐标、大小等等信息(可以使用setBounds
在启动应用后恢复上一次窗口); - show/hide:显示/隐藏窗口;
不规则窗口
实现不规则窗口,在browserWindow
实例化时,需要将属性transparent
设为 true,然后在写页面样式的时候,写好自己想要的效果即可,但这只是窗口背景透明而已,例如我们绘制一个圆球,实质是平行四边形的透明窗口内盖着一个圆形的 dom 元素而已,点击窗口的透明区域,并不会将点击事件穿透到应用后面的内容;
要实现这种点击穿透效果,我们可以借助browserWindow
实例对象上的setIgnoreMouseEvents
方法来实现,该方法可以使窗口忽略当前窗口内的所有鼠标事件,该方法传入forward: true
可实现鼠标点击事件穿透,但鼠标移动事件不穿透的效果,至此,我们可以在透明区和非透明区上监听鼠标移动事件,在透明区内且非透明区外,则设置点击穿透,否则设置点击穿透无效;
页面容器
在electron
中实现页面容器的方案有三种,分别是webFrame
(iframe)、webview
标签、BrowserView
脚本注入
脚本注入可以让开发者将一段 JS 代码注入到目标网页中去,而这段代码是具有超能力的,它不单止可以访问 dom,更可以使用 Node 相关的 API,需要注意的是,无论是否开启webPreferences.nodeIntegration
,注入的脚本都可以访问 Node 相关的 API,不开启的话,可以防止第三方页面访问 Node,提升安全性;
数据
使用本地文件持久化数据
操作系统都专门为应用程序提供一个专有目录来存储应用程序的用户个性化数据;
Windows: C:\Users\[user name]\AppData\Roaming
Mac: /Users/[user name]/Libary/Application Support/
Linux: /home/[user name]/.config/
我们可以使用 JSON 文件、浏览器存储、SQLite 等手段来存储数据,大部分的桌面应用使用 SQLite 来保存数据;
系统
remote
模块下的dialog
支持我们打开系统支持的对话框;
electron
提供menu
对象,支持自定义菜单栏、右键菜单;
globalShortcut
支持注册全局的快捷键;
在ready
事件回调中注册tray
来实现系统托盘;
参考文献
Electron 实战: 刘晓伦--机械工业出版社