electron
Electron介绍
Electron的三个核心组成:
- Chromium:浏览器渲染
- Node.js:文件读取
- Native apis:提供统一原生界面能力(操作系统交互)
用Electron做的比较出名的软件大概就是VSCode了,
electron主要的特点就是跨平台兼容性好,
但是在内存占用方面也经常被人诟病,
因为electron应用就算什么内容都没有,
还有整整200MB的Chromium引擎包在里面
桌面应用和web应用不同的是涉及到对操作系统的操作,
但这一部分都由Native apis实现,
前端工程师只需要关心渲染层面的东西就可以。
electron中的两种进程:
- 主进程 Main Process
- 一个应用主进程唯一
- 启动入口一般在main.js中(package.json中main配置入口)
- 首先启动,启动后调用Native UI创建一个或多个BrowsersWindow界面
- 渲染进程 Renderer Process
- 一个应用可以有多个渲染进程
- 在BrowersWindow上运行的进程
- 各个渲染进程互不干扰,在自己的沙箱环境中运行
各个进程之间使用IPC进行通信
step1
启动APP
step2
主进程创建window
step3
window加载界面
step4
界面交互涉及到操作系统,
渲染进程通过IPC和主进程通信,
主进程再调用native apis
Electron开发
框架结构
官方提供的框架案例:
1 | const {app, BrowserWindow} = require("electron") |
使用nodemon辅助开发
使用node命令启动server时,
每当代码修改,都需要重启server:
1 | node server.js |
electron开发也是这样,每当主进程代码(main.js)发生变化时,
都需要重启项目:
1 | electron . |
使用nodemon,可以监听指定源码的变化,自动执行命令:
1 | "scripts": { |
生命周期
ready
app初始化完成
dom-ready
webContents监听,窗口文本加载完成
did-finish-load
webContents监听,导航完成时触发
window-all-closed
所有窗口都被关闭时触发
before quit
关闭窗口前触发
will-quit
窗口关闭应用程序退出时触发
quit
所有窗口被关闭时触发
closed
窗口被关闭时触发,此时应删除窗口引用
可以在closed中将窗口对象置null进行内存回收
窗体
由于窗体先创建后显示,因此在dom渲染之前会出现白屏的情况,
可以先将窗口show设置为false,
loadFile后监听ready-to-show事件,
再用win.show将窗口调出:
1 | let mainWin = new BrowserWindow({ |
调出控制台快捷键:Ctrl+Shift+i
多个窗体
如果MainWindow里渲染的界面有一个按钮,
点击按钮,创建一个新界面,
这意味着需要给按钮绑定一个事件,并在对应事件中进行窗口调用操作。
在窗口对应的html文件中,引入js脚本,
脚本内引入electron提供的用于创建窗口的对象:
(这部分参考Electron最新版remote问题)
注意:需要打开MainWindow中的WebPreferences.nodeIntegration后才能在渲染进程中使用node。
调用Electron的API还需要打开WebPreferences中的两个属性:
1 | contextIsolation:false; // 上下文隔离 |
新版本的electron远程调用需要安装@electron/remote库,
注意,安装@electron/remote库时报错可以用cnpm安装:
1 | cnpm install @electron/remote |
在渲染进程中,通过引入@electron/remote远程调用api
在主进程中需要对remote进行初始化:
1 | app.whenReady().then(()=>{ |
index.html引入的index.js
1 | const {BrowserWindow} = require('@electron/remote') |
自定义窗体
和在渲染进程中创建一个新窗口一样,
需要使用从remote中引用api进行窗口操作:
getCurrentWindow 获取当前窗口
- win.close() 关闭当前窗口,会触发window.onbeforeunload事件
- win.isMaximized() 查询当前窗口是否已经最大化
- win.maximize() 窗口最大化显示
- win.restore() 窗口回归原始状态
- win.isMinimized() 窗口是否已经最小化显示
- win.minimize() 窗口最小化显示
- win.destroy() 摧毁当前窗口,不会触发onbeforeunload事件
代码实现
1 | const {getCurrentWindow} = require("@electron/remote") |
父子和模态窗体
有些应用程序点击按钮会出现一个弹窗,
并且不能跨越弹窗点击到后面的窗口,
这就是父子和模态窗口,
需要在子窗口上配置parent和modal属性:
1 | let win = new BrowserWindow({ |
渲染进程中创建子窗体,需要通过getCurrentWindow()设置parent父窗体,
但是在主进程中创建子窗体时,需要给主进程提供一个父窗体标识,
然后由BrowserWindow.fromId获取到目标窗体:
保存父窗体的id:
1 | let mainWindowId = null //主窗口id |
子窗口创建时,parent指向id代表的窗体:
1 | let subWin1 = new BrowserWindow({ |
父子窗体还有一个特点:父窗体在关闭后,子窗体同时也会被关闭
菜单
自定义菜单需要一个数组变量作为菜单配置:
1 | let menuTemp = [ |
用配置生成菜单项,然后将生成的菜单项设置到应用的菜单中:
1 | let menu = Menu.buildFromTemplate(menuTemp) |
动态创建菜单需要使用到MenuItem方法,
现有一个菜单项的子菜单指向一个Menu类型的数据
1 | let menuItem = new Menu() |
要为子菜单动态添加菜单项,需要用到Menu.append方法为菜单添加子项:
1 | menuItem.append( |
右键菜单的创建和导航栏菜单创建一样,都是由Menu.buildFromTemplate完成,
不同在于不会使用Menu.setApplicationMenu,
而是Menu.popup:
1 | const {Menu,getCurrentWindow} = require("@electron/remote") |
弹窗
弹窗方法api:electron.dialog
异步获取文件弹窗配置:
1 | btn.addEventListener('click',()=>{ |
错误弹窗:
1 | errBtn.addEventListener('click',()=>{ |
shell
Shell可以用来:
- 打开文件管理器
- 将通过浏览器打开url
使用shell在资源管理器中打开文件目录:
1 | const {shell} = require('electron') |
shell在浏览器中打开链接:
1 | shell.openExternal(urlPath) // 外部浏览器打开链接 |
消息提示
在electron应用中触发window.Notification,
能够触发操作系统的消息提示,
同时,还能监听到消息提示的点击事件
1 | let option = { |
进程通信
主进程与渲染进程通信
主进程与渲染进程之间的通信有几类:
- 渲染进程发起 - ipcRenderer
- 异步
- 同步
- 主进程发起的通信 - ipcMain
- 异步
同步(不支持)
异步通信是没有返回值的通信
渲染进程 → 主进程
渲染进程发送:
1 | asyncBtn.addEventListener('click',()=>{ |
主进程回复:
1 | ipcMain.on('msg1', (ev, data)=>{ |
使用chcp 65001命令改变当前活动代码页格式为utf-8
主进程 → 渲染进程
主进程需要通过根据当前focus的窗口的WebContent,来实现向指定渲染进程的通信:
1 | let menuTemp = [ |
子进程开启事件的on监听:
1 | // 自进程发送的消息 |
同步通信只支持渲染进程向主进程发送,
渲染进程可以用一个值接收sendSync的返回值:
1 | // 发送同步请求 |
主进程通过设置returnValue作为返回值:
1 | ipcMain.on("msg2",(ev,data)=>{ |
渲染进程之间传值
使用localStorage能够跨窗口传值,
之前在一个vue项目中就遇到了跨窗口传值的需求,
结果发现vuex无法跨窗口传值,所以用的localStorage
localStorage适合具有父子关系的窗口之间传值
A进程和Main建立通道
B进程和Main建立通道
AB进程就能通过Main进行通信:
A进程和Main建立的通道:
1 | ipcRenderer.send('stm', text) // A → Main |
Main将A进程数据转发到B进程
1 | ipcMain.on('stm',(ev,data)=>{ // Main ← A |
B进程接收数据
1 | ipcRenderer.on('msgAtoB',(ev,data)=>{ // B ← Main |
通过主进程通信方式创建窗口的同时传值,
父窗口渲染需要在告知主进程开启新窗口的同时传递值,
然后在主窗口将子窗口进程创建好后,将值通过自通信的方式传递过去:
父窗口通知主进程创建子窗口进程(监听did-finish-load事件):
1 | ipcRenderer.send('openWin',123) |
主进程创建新窗口并传值:
1 | ipcMain.on('openWin',(ev,data)=>{ |
子窗口监听:
1 | ipcRenderer.on('its',(ev,data)=>{ |
快捷键
globalShortcut可以在操作系统中注册/销毁全局快捷键。
这种绑定是全局,即便是应用失去焦点,也能持续监听。
在ready中注册快捷键
1 | app.on('ready',()=>{ |
快捷键的销毁:
1 | app.on('will-quit',()=>{ |
剪切板
剪切板文本的读写:
1 | let res = null // 复制内容 |
剪切板图片的复制:
1 | const {clipboard, nativeImage} = require('electron') |
nativeImage是electron为多种缩放大小准备的图标操作类,
读取的图标具有一定格式要求(png、jpg、ico),
还有一定尺寸要求(见官网介绍)
如果尺寸和格式不满足要求,读入进来的图片就默认为空