阅读 520

Electorn + Vue3 + Vite + TS 实战探索

前言

好长时间没有写过文章了,这段时间确实很忙,终于整完了,这里做个复盘,记录一下探索过程中遇到的问题。

背景:项目需求说:我要一个桌面客户端程序,最好能跨平台。

技术选型:Electorn 因为使用 Web 技术构建应用、开源、跨平台的优秀特性,自然首当其冲。我有得选吗?没有,我没有。

知识储备:HTML、CSS、JS、VUE

开始

打开 Electorn 官网,首先看到:

image.png

我们常用的开发工具vscode竟然是用Electorn开发的,直呼Electorn 强大

然后下翻,了解下特性: image.png

好像挺牛的样子。

通过官网首页,该知道的也知道了,接下来,我们打开文档的快速入门页面,参照教程一步步构建出自己的第一个 demo

我们需要重点关注 流程模型 这一节,理解在每个 Electron 应用中都是由一个运行在 Node.js 环境中的单一的主进程来管理多个渲染进程(如果你创建多个窗口的话)以及辅助进程等。主进程与渲染进程之间通过ipc管道通信,具体的几种写法,百度一大堆。预加载脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码,我们通常的做法是通过预加载脚本向渲染进程传递数据,这些数据会被挂载在 window 对象下面。

好,知道这些就足够了,余下的就是在具体开发中反复查阅API的事情了。

本次共用 Electorn 开发了两个应用,第一个应用比较简单,先来谈第一个。

第一个应用

考虑到该应用较为简单,总共下来也就三个页面。所以vue单文件组件、webpack打包、路由vue-routeraxios等等这些我通通都不需要,即使是有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.jspreload.js。现在着手修改项目,首先修改src目录,我们修改项目目录为如下三部分:

image.png

src目录下原先的内容移入render 目录下。render目录的样子类似如下:

image.png

修改vit.config.ts配置,将vite打包的根目录变更为src/render

main文件夹与preload文件夹,考虑到下面可能也会划分很多细小的文件模块,那我们也可以将这两个文件夹分别使用rollup打包。rollup相比webpack更适合打包js库,所以这里使用rollup来打包更合适。

现在新建目录script用来编写rollup打包脚本,目录可能如下:

image.png

然后,我们去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命令,打包后的目录如下:

image.png

已经满足最开始思考中所需要的三部分。

在最终集成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可以看到:

image.png

于是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 服务

image.png

首先给到结论:electorn应用注册为windows 服务的做法不可取,建议放弃。

两点原因:

  1. 通过electorn-builder打包出来的exe可执行程序,根本就不满足服务的规范。

在知道这一点之前,我尝试了两种方式。

第一种是使用NSIS Simple Service Plugin 这个插件,我按照文档编写好nsh脚本后打包程序,然后运行程序,发现服务注册上了,但是启不来,查看windows 日志也无果。

第二种就是SC命令,长这样子:SC [Servername] command Servicename [Optionname= Optionvalues],我直接cmd执行命令注册,得到的结果与第一种情况相同。

网络上更多人说node-windows,我的可是要将整个程序注册为windows 服务,又不是一个NodeJS脚本,况且我的应用还要打包为exe可执行程序,所以要视情况,不能人云亦云。

  1. windows 服务 属于操作系统核心态,也就是说windows 服务工作在操作系统内核。在操作系统内核工作的程序是不会有图形化界面这些展示功能的。

最有说服力的情况就是,我用nssm工具(NSSM 是一款可将 Nodejs 项目注册为 Windows 系统服务的工具)将我的应用程序注册为windows服务后,查看进程发现程序在运行,但是应用程序的托盘图标无论如何都显示不出来。所以将electorn应用注册为windows根本不可取。

我最终的做法就是项目拆分,将需要注册为windows 服务的部分作为NodeJS项目单独打包,这里的打包我用到的是pkg这个包,需要注意的是NodeJS项目里面如果你没有配置babel,请不要使用ESModule,否则打包会失败。另外的部分你可以作为图形化界面来展示,拆分的两部分之间如果需要通信,可以使用websocket,ws这个包还不错。

最后

项目还在持续迭代中,以后遇到更多的问题,我都会在这里记录,欢迎大家一起探讨。


作者:伟大的兔神
链接:https://juejin.cn/post/7019244597213675533

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐