electron
常见需求
-
在 Renderer 端创建菜单
相关 issue: https://github.com/electron/electron/issues/7455
在 Electron 中,无论是应用程序的主菜单(macOS 顶部的菜单)、窗口菜单(Windows/Linux)的窗口菜单、Tray 菜单、还是 Renderer 端的 Context 菜单等,凡是和菜单挂钩的功能都是通过 Menu.buildFromTemplate 进行创建的,他们只是被挂载到了不同的实体上,比如被挂载到了页面上的叫做 Context 菜单,被 setApplicationMenu 挂载的成为了主菜单。
Electron 的菜单其实限制也非常之大,其菜单在创建完成后便不能在运行时被直接修改,需要修改时,必须对整个菜单重新创建,从而达到动态菜单的目的。这也是非常不友好的。
另一方面,一个菜单项被点击后,只接受三个参数,也就是说 click 属性的回调只接收: menuItem(被点击的item)、focusedWindow(点击按钮时focus的窗口) 以及毫无用途的 event (提供键盘是否被按下的信息),这也就造成的诸多的不便。
例如,当我们希望一个按钮被点击后,其他的菜单项的 enbalbed 属性设置为 false,将无从下手。
为了解决这一问题,这时候我们可以巧妙的使用 Electron 的 ipc 机制在 renderer 端创建应用菜单,在 renderer 端创建的菜单便可以在内部使用 ipcRenderer.send() 这个方法,让 ipcMain 来处理其他其他菜单项的操作。
不仅如此,甚至于可以让整个菜单的 click 逻辑都交付于 main 端进行管理,例如下面这样的 menu template:
{
type: 'separator',
visible: (()=>{
if (process.platfrom == 'darwin') return true;
else return false;
})()
},
{
label: `Current Version: ${app.getVersion()}`,
enabled: false
},
{
type: 'separator'
},
{
label: 'Logout',
click: () => { ipcRenderer.send('application', 'logout'); }
},
{
type: 'separator'
},
{
label: 'Exit ${app.getAppName()}',
accelerator: 'CmdOrCtrl+Q',
click: () => { ipcRenderer.send('application', 'quit'); }
}
而在 main 端则可以:
ipcMain.on('application', (event, args) => {
switch(args){
case: 'logout':
....; break
case: 'quit':
...; break
default: ...
}
})
-
后台网络状态监测
相关 issue: https://github.com/electron/electron/issues/6633
问题: Electron 的官方文档其实就提供了网络检测的方法,思路是通过一个隐藏的后台窗口,检测网络网络状态,通过网络状态产生变化后,通过 ipcRenderer 发送消息给 ipcMain,然后响应所需的操作,见这里。
但事实上这个方法是基于 online 和 offline 事件的,换句话说这个方法只能检测到当系统网络连接被切断物理连接后的状态变化,无法检测网络本身。
这个问题可以参考issue,但其实现思路就是向苹果的 hotspot detect 页面发起请求,当未超时状态下有 Success 返回时,便说明网络状态正常。在实际应用编写过程中,我们也并不需要为了这样一个简单的功能而引入框架,只需定时向服务器发起任意一个能够判断网络状态的请求即可,与心跳连接殊途同归。
-
preload 执行阶段和使用场景
相关 issue: https://github.com/electron/electron/issues/7455
在前面创建基本应用中我们已经谈到了关于 preload 脚本用来引入 jQeury 的使用,事实上我们可以用 preload 做更多的事情。preload 脚本会在整个页面开始加载之前被执行,所以如果我们直接执行一些当整个 DOM 加载完成才能被执行的操作,是必定会失效的,因此这样的两个事件是非常有用的:DOMNodeInserted、DOMContentLoaded。
为此,我们可以把 preload 脚本大致分为三块区域:
// ---------------------------------------------------
// 在页面加载之前需要执行的相关代码
// ...
// ---------------------------------------------------
// -------------------------------------------------------
document.addEventListener('DOMNodeInserted', (event) => {
// 页面内容加载之前需要引入的一些代码
// ...
})
// -------------------------------------------------------
// -------------------------------------------------------
document.addEventListener('DOMContentLoaded', (event) => {
// 页面内容加载之后需要引入的一些操作
// ...
})
// -------------------------------------------------------
preload 脚本的作用非常大,有时候会有这样的需求:当我们加载一个网络上的页面时,我们不能控制从网络中读取到的页面内容,但 preload 提供了这样的可能性,使得我们能够向页面 注入 一些代码,满足一些神奇的需求,比如对网络加载页面增加 Context Menu。但也有使用时值得注意的地方:
Electron 的 main 进程、preload 脚本、renderer 进程、以及 document 对象分别有彼此的创建和执行顺序。首先 main 进程会优先被创建毫无疑问,preload 会在 document 对象被创建之前优先加载(但能够使用 document),而 renderer 进程会在 document 创建之后被创建,而他们三者又是并发创建的,如下图所示。
那么,如果我们不小心在 preload 脚本中直接引入 ipcRenderer 发送一条消息给 ipcMain,那么 ipcMain 可能不能收到这条早期消息。为了保证我们能够收到这条消息,最好的方式就是:
// preload.js
// 不要再外面这么干
// ipcRenderer.send(...)
document.addEventListener('DOMContentLoaded', (event) => {
// 页面内容加载之后需要引入的一些操作
// ...
// 正确的做法
ipcRenderer.send(...)
})
-
下载
下载也是一个常见的需求,比如,你正在基于 Electron 实现一个 Web 文本应用,用户可能需要下载保存在服务器上的一个编辑好的文件,这时候当点击 Web 界面中的下载时,Electron 并不需要专门针对这个下载行为进行单独的处理,Electron 会想浏览器那样直接跳出一个保存的文件选择器,让用户获得下一步的操作。当我们真正需要处理一些特殊的下载操作时,同样可以用 electron 的 DownloadItem 来实现,但其接口设计着实有点让笔者难以接受,这里推荐可以尝试 electron-userland/electron-download 这个库,虽然其本质也是 DownloadItem,但其接口相比之下友善许多,因为库本身也并不复杂,也可以在项目中自行实现这部分逻辑。
-
软件更新
软件的日后更新一直都是产品日后迭代的杀手,一个需要被分发的桌面应用,在没有确定的更新机制之前,切忌发布。
Electron 虽然本身自带 audoUpdater 模块,但作为框架的使用者来说,笔者很难说它做得优秀,因为需要配置的内容相较于其他功能来说略加繁琐。因此这里推荐使用 electron-updater。下面的代码相当于一个纯粹的更新功能的封装,使用成本非常简单,只需根据 electron-builder wiki 的说明配置好 publisher 即可实现更新功能:
// updater.js
const { dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
let updater
// 禁用自动下载,给用户选择余地
autoUpdater.autoDownload = false
autoUpdater.on('error', (event, error) => {
dialog.showErrorBox('Error: ', error)
})
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
title: 'Found Updates',
message: 'Found updates, do you want update now?',
buttons: ['Sure', 'No']
}, (buttonIndex) => {
if (buttonIndex === 0) {
autoUpdater.downloadUpdate()
} else {
updater.enabled = true
updater = null
}
})
})
autoUpdater.on('update-not-available', () => {
dialog.showMessageBox({
title: 'No Updates',
message: 'Current version is up-to-date.'
})
updater.enabled = true
updater = null
})
// 下载完成时,提醒用户
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
title: 'Install Updates',
message: 'Updates downloaded, application will be quit for update...'
}, () => {
autoUpdater.quitAndInstall()
})
})
// 将这个回调输出给更新功能所在的菜单项的 click 回调
function checkForUpdates (menuItem, focusedWindow, event) {
updater = menuItem
updater.enabled = false
autoUpdater.checkForUpdates()
}
module.exports.checkForUpdates = checkForUpdates
-
发布
打包工具
期初的 Electron 打包是一个比较恼人的问题,因为可用的工具其实不多,electron-packager 是一个很原始的打包工具,虽然具备打包的功能,但是其提供以开发 API 为蓝本的入口使得构建还需要额外编写脚本进行,而它其实只具备将应用进行打包编译的功能,这与最终发布的 Installer 还有一步之遥,所以使用 electron-packager 是非常消耗开发成本的一件事情。好在有一个取而代之的工具 electron-builder。它的好处在本文前面的部分也已经多次提及,它不仅拥有方便的配置 protocol 的功能、内置的 Auto Update、简单的配置 package.json 便能完成整个打包工作,用户体验是相当优秀的。
代码签名
代码签名对于发布作为正式商业产品应用来说是非常重要的。如今的 electron-builder 对于代码签名已经做得相当友好,对于 macOS 来说,它能够自动获取系统中 Keychain 中的开发者证书,自动对代码进行签名,而 Windows 也可以通过配置一个 .p12 证书来达到签名的目的。
因此,到目前为止,代码的签名成本已经非常低,只需购买好证书,基本上没有什么烦心的事情,唯一一个值得注意的事情是:如果要分发 macOS 上的应用,那么构建平台将只有 macOS 是被推荐的,因为它是唯一一个能够同时构建 macOS/Linux/Windows 三平台应用的平台。但是,如果使用 CSC_LINK 将会出现冲突,因为 CSC_LINK 已被用于 macOS 平台的签名,因此额外在 package.json 中配置 Windows 平台的证书。
由于证书最终不会被分发,可以在签名时使用一个移除密码的证书;亦或者对两个平台的证书使用相同的密码,方便最终的签名和打包。