生活本质就是遇到问题解决问题的过程
前言
好长时间没有写过文章了,这段时间确实很忙,终于整完了,这里做个复盘,记录一下探索过程中遇到的问题。
背景:项目需求说:我要一个桌面客户端程序,最好能跨平台。
技术选型:Electorn
因为使用 Web 技术构建应用、开源、跨平台
的优秀特性,自然首当其冲。我有得选吗?没有,我没有。
知识储备:HTML、CSS、JS、VUE
开始
打开 Electorn 官网,首先看到:
我们常用的开发工具vscode
竟然是用Electorn
开发的,直呼Electorn 强大
。
然后下翻,了解下特性:
好像挺牛的样子。
通过官网首页,该知道的也知道了,接下来,我们打开文档的快速入门页面,参照教程一步步构建出自己的第一个 demo
。
我们需要重点关注 流程模型 这一节,理解在每个 Electron
应用中都是由一个运行在 Node.js
环境中的单一的主进程来管理多个渲染进程(如果你创建多个窗口的话)以及辅助进程等。主进程与渲染进程之间通过ipc管道
通信,具体的几种写法,百度一大堆。预加载脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码,我们通常的做法是通过预加载脚本向渲染进程传递数据,这些数据会被挂载在 window
对象下面。
好,知道这些就足够了,余下的就是在具体开发中反复查阅API
的事情了。
本次共用 Electorn
开发了两个应用,第一个应用比较简单,先来谈第一个。
第一个应用
考虑到该应用较为简单,总共下来也就三个页面。所以vue
单文件组件、webpack
打包、路由vue-router
、axios
等等这些我通通都不需要,即使是有electorn-vue
这现成的项目可以更方便的做二次开发,我也不会去用。我认为这么小的项目不应该用工程化的东西去增加复杂性,这就是我的思考。偶尔写写原生开发,感觉还是不错的。
我直接用显示与隐藏取代掉vue-router
实现的路由功能,vue-router
的内部实现原理无非也是根据url
去加载的对应的组件。因为Electorn
的渲染进程是跑在Chromium
中,所以请求部分我直接用fetch
来发起。UI
部分你还要用ElementUI
框架?别懒,自己手写。
在这第一个应用中,发现的第一个坑是存在于渲染进程中的ipcRenderer
对象的send
方法不存在,导致渲染进程向主进程通信受阻。给到的解决办法是,在预加载脚本中传递数据:
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 向渲染进程传递参数
contextBridge.exposeInMainWorld('ipc', {
"ipcRenderer": {
send: (channel, data) => {
ipcRenderer.send(channel, data)
},
on: (channel, callback) => {
ipcRenderer.on(channel, callback)
}
}
})
复制代码
第二个坑是关于打包的问题,在 官方文档 - 快速入门 - 打包并分发您的应用程序一节,Electron Forge
这个打包工具包并不好用,后来发现Electorn-builder
真香。
第一个应用就这些,没有太多的东西,重点来了,第二个应用,难度一下子上来了。
第二个应用
第二个应用,根据需求来看,判定以后会集成很多东西,功能比较复杂,所以一开始我就考虑electorn-vue
这样便捷的脚手架,但是技术是发展的,我发现现有的electorn-vue
脚手架只有vue2.x
版本,我想用vue3 + ts + vite
来做这个项目,推动技术革新。
我将之前根据从 0 开始手把手带你搭建一套规范的 Vue3.x 项目工程环境这篇文章搭建的vue3.x
项目模板拿过来,集成electorn
进去。注意到此为止还只是个web
项目,只是添加了electorn
依赖包,关于本地启动方式、打包等,还需要进一步去修改。
将 vue3.x 项目改造为 electorn 工程化项目
这一部分是最为复杂的,涉及打包配置,本人还在学习NodeJS
,所以对于打包也没有很深入的理解、探究,后续会学习打包这部分,工程化可是很重要的呦。
好在github
上已经有现成的项目模板 electron-vue-vite,我关于打包部分的内容更多的是参考该作者的,致敬。当然,在发现这个仓库之前,我也进行了深入的思考,下面是我的一些思考,或者你可以认为是对electron-vue-vite
这个项目的解读。
思考:electorn
项目需要一个跑在主进程的如main.js
入口文件来控制应用程序,还需要至少一个跑在渲染进程的如index.html
文件来渲染应用界面,最后还可以根据需要提供一个预加载文件如preload.js
在渲染器进程加载之前来传递一些数据。
现在我们vue3.x
项目打包后生成在dist
文件夹下的文件就是最终跑在渲染进程的文件,我们还缺main.js
与preload.js
。现在着手修改项目,首先修改src
目录,我们修改项目目录为如下三部分:
将src
目录下原先的内容移入render
目录下。render
目录的样子类似如下:
修改vit.config.ts
配置,将vite
打包的根目录变更为src/render
。
main
文件夹与preload
文件夹,考虑到下面可能也会划分很多细小的文件模块,那我们也可以将这两个文件夹分别使用rollup
打包。rollup
相比webpack
更适合打包js
库,所以这里使用rollup
来打包更合适。
现在新建目录script
用来编写rollup
打包脚本,目录可能如下:
然后,我们去package.json
文件去配置相关打包命令,大概长这个样子:
"build:render": "vite build",
"build:preload": "node -r ts-node/register script/build-preload --env=production",
"build:main": "node -r ts-node/register script/build-main --env=production",
"build": "rimraf dist && npm run build:render && npm run build:preload && npm run build:main"
复制代码
关于npm scripts
的使用,可以阅读npm scripts 使用指南这篇。
现在我们执行npm run build
命令,打包后的目录如下:
已经满足最开始思考中所需要的三部分。
在最终集成electorn-builder
打包后,我还添加了如下命令,方便打包:
"win32": "npm run build && electron-builder --win --ia32",
"win64": "npm run build && electron-builder --win --x64",
"mac": "npm run build && electron-builder --mac",
"linux": "npm run build && electron-builder --linux"
复制代码
注意&&
符号表示继发执行。
就目前情况而言,我们构建好了打包相关的东西,但这也只是满足了在生产环境的需要。在本地开发环境下我们又该怎样让electorn
程序启动且方便的做到热重载?
思考:可以先将web
项目启动起来,然后再打包mian、preload
文件夹的内容并执行,所以开发环境的npm script
命令会配置如下:
"dev": "concurrently -n=R,P,M -c=green,yellow,blue \"npm run dev:render\" \"npm run dev:preload\" \"npm run dev:main\"",
"dev:render": "vite",
"dev:preload": "node -r ts-node/register script/build-preload --env=development --watch",
"dev:main": "node -r ts-node/register script/build-main --env=development --watch"
复制代码
注意这里的dev
命令,会平行的执行dev:render、dev:preload、dev:main
三个命令,这三个命令执行的先后顺序每次可能都不一样,所以在打包的那部分脚本(build-main.ts
)中有个waitOn
函数轮询监听vite
的启动状态,目的即在vite
启动后,也就是本地的web服务器
起来后,才去执行rollup
打包main
文件夹下文件的操作,最后用child_process.spawn()
执行打包后的main.js
,这样就做到了在本地启动electorn + vue3.x + vite + ts
的应用程序。
项目已经很好的搭建起来了,能够较好的完成本地开发与生产环境的打包。遂着手开发项目,下面是我在开发中碰到的问题记录,或许大家也会碰到这方面的问题。
问题记录
1. RabbitMQ 的使用
打开RabbitMQ
的官网JavaScript Get Started,关于生产者、消费者、交换机、路由、RPC这些的介绍都在文档里了,所以撸文档就好了。
参照官方教程,我在建立RabbitMQ
连接的时候总是不成功。
第一个问题,我的项目RabbitMQ
服务端是有SSL
证书验证的,添加证书的写法在amqplib官网-SSL
第二个问题,即使我配置好了ssl
证书,连接也会有问题,需要添加
checkServerIdentity: () => {
return null
}
复制代码
完整的连接代码,Promise
形式,附加接收消息与发送消息示例:
const amqp = require('amqplib')
const constains = require('constants')
const url = {
protocol: 'amqps',
hostname: 'xxx.xxx.xxx.xxx',
port: 'xxxx',
username: 'xxxxxxx',
password: 'xxxxxxxxxxxxxxxx'
}
const opts = {
cert: fs.readFileSync('clientcert.pem'),
key: fs.readFileSync('clientkey.pem'),
passphrase: 'MySecretPassword',
ca: [fs.readFileSync('cacert.pem')],
secureOptions: constains.SSL_OP_NO_TLSv1_1, // SSL 协议版本
checkServerIdentity: () => {
return null
}
}
function reconnect() {
console.log('到服务器的连接已断开')
return connectRabbitMq()
}
// 连接 RabbitMQ 示例
function connectRabbitMq() {
return amqp
.connect(url, opts)
.then(connect => {
connect.on('error', reconnect) // 错误重连
console.log('到服务器的连接正常')
return connect.createChannel()
})
.then(async (channel) => {
await channel.assertExchange('exchange', 'topic', {
durable: true, // 消息持久化
autoDelete: false
})
return channel
})
.catch(reconnect)
}
// 接收消息示例
function receiveMessage() {
connectRabbitMq()
.then(async (channel) => {
// 声明队列
await channel.assertQueue('receiveQueue', {
durable: true,
autoDelete: true
})
// 在处理并确认前一条消息之前,不要向工作人员发送新消息
await channel.prefetch(1)
// 绑定队列
await channel.bindQueue('receiveQueue', 'exchange', 'receiveRoutingKey')
// 消费消息
channel.consume(
'receiveQueue',
async (msg) => {
console.log('======= receive =======')
console.log(msg.content.toString())
await dealMsg(msg) // 处理消息
channel.ack(msg) // 应答
},
{
noAck: false
}
)
})
.catch(console.warn)
}
// 发送消息示例
function sendMessage(msg) {
connectMQ()
.then((channel) => {
channel.publish('exchange', 'sendRoutingKey', Buffer.from(JSON.stringify(msg)))
})
.catch(console.warn)
}
复制代码
2. NodeJS 下载文件并显示下载进度
我首先使用了axios
去下载文件,发现axios
的下载进度只支持在浏览器环境下能获取到,Node
环境无法获取到。这一点在官网Request Config可以看到:
于是axios
不再做考虑,最终我使用了另外两个包来实现,示例如下:
const fs = require('fs')
const fetch = require('node-fetch')
const progressStream = require('progress-stream')
/**
* @param {*}
* fileURL: string 文件下载地址
* fileSavePath: string 文件保存地址
* callback 可选参数,默认空函数,可用于文件下载过程中的一些操作
*/
function downLoadFile(
fileURL,
fileSavePath,
callback = function () {}
) {
const fileStream = fs
.createWriteStream(fileSavePath)
.on('error', () => {
console.log('下载出错')
})
fetch(fileURL, {
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' }
})
.then(res => {
let fsize = res.headers.get('content-length')
//创建进度
let str = progressStream({
length: fsize,
time: 100 /* ms */
})
// 下载进度
str.on('progress', function (progressData) {
let progress = Math.round(progressData.percentage)
callback(process) // 拿到进度后可以做进一步处理
})
// 保存文件
res.body.pipe(str).pipe(fileStream)
})
.catch(console.warn)
}
复制代码
关于断点续传,更多的可以参考NodeJS使用node-fetch下载文件并显示下载进度示例
3. electorn-builder 打包,修改程序默认安装路径
这个功能需要通过编写nsis
脚本来实现,首先修改package.json
中的nsis
配置项,添加include
选项,如下:
"nsis": {
"oneClick": true,
"allowElevation": true,
"installerIcon": "dist/favicon.ico",
"uninstallerIcon": "dist/favicon.ico",
"installerHeaderIcon": "dist/favicon.ico",
"createDesktopShortcut": false,
"createStartMenuShortcut": true,
"shortcutName": "rabbit",
"deleteAppDataOnUninstall": true,
"include": "./installer.nsh"
}
复制代码
关于nsh
脚本,你可以去electorn-builder
官网看看,点这里。
installer.nsh
脚本示例如下:
!macro preInit
SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files (x86)\rabbit"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files (x86)\rabbit"
SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files (x86)\rabbit"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "C:\Program Files (x86)\rabbit"
!macroend
复制代码
4. rollup 打包时文件的复制
我项目的根目录下有个图标文件favicon.ico
,在rollup
打包的时候,我需要将它移动到dist
目录下,这样才符合我上面nsis
配置项中图标项的地址配置。
我还没有傻到通过fs
内置模块来操作的地步,我先去查看了vue-cli
项目中webpack
是怎样做到的,我找到了copy-webpack-plugin
这个包。于是我也去找rollup
类似的包,于是找到了rollup-plugin-copy
这个包,示例代码如下:
import copy from 'rollup-plugin-copy'
const RollupOptions = {
plugins: [
copy({
// 复制 favicon.ico 到指定目录
targets: [
{ src: 'favicon.ico', dest: 'dist' }
]
})
]
}
复制代码
你可以在npm
官网上看到更多的使用示例,点这里rollup-plugin-copy
5. 注册 windows 服务
首先给到结论:electorn
应用注册为windows 服务
的做法不可取,建议放弃。
两点原因:
- 通过
electorn-builder
打包出来的exe
可执行程序,根本就不满足服务的规范。
在知道这一点之前,我尝试了两种方式。
第一种是使用NSIS Simple Service Plugin 这个插件,我按照文档编写好nsh
脚本后打包程序,然后运行程序,发现服务注册上了,但是启不来,查看windows 日志
也无果。
第二种就是SC
命令,长这样子:SC [Servername] command Servicename [Optionname= Optionvalues]
,我直接cmd
执行命令注册,得到的结果与第一种情况相同。
网络上更多人说node-windows
,我的可是要将整个程序注册为windows 服务
,又不是一个NodeJS
脚本,况且我的应用还要打包为exe
可执行程序,所以要视情况,不能人云亦云。
windows 服务
属于操作系统核心态,也就是说windows 服务
工作在操作系统内核。在操作系统内核工作的程序是不会有图形化界面这些展示功能的。
最有说服力的情况就是,我用nssm工具(NSSM 是一款可将 Nodejs 项目注册为 Windows 系统服务的工具)将我的应用程序注册为windows
服务后,查看进程发现程序在运行,但是应用程序的托盘图标无论如何都显示不出来。所以将electorn
应用注册为windows
根本不可取。
我最终的做法就是项目拆分,将需要注册为windows 服务
的部分作为NodeJS
项目单独打包,这里的打包我用到的是pkg这个包,需要注意的是NodeJS
项目里面如果你没有配置babel
,请不要使用ESModule
,否则打包会失败。另外的部分你可以作为图形化界面来展示,拆分的两部分之间如果需要通信,可以使用websocket
,ws这个包还不错。
最后
项目还在持续迭代中,以后遇到更多的问题,我都会在这里记录,欢迎大家一起探讨。
参考
从 0 开始手把手带你搭建一套规范的 Vue3.x 项目工程环境
RabbitMQ 官网 JavaScript Get Started