记录使用 electron 的一次经历
前言
记录使用 electron 的一次经历。
已经快一个月没有写帖子了,时间过的好快,转眼就又到年末了。
一号才从老家回到广州,歇了差不多一个月,这段时间也是用 electron + vue 开发了一个桌面软件。
项目 github 地址:jm-desktop
这篇帖子就记录下这一个月的过程。
正文
前期准备
首先,在编写之前,分析了接口加密以及图片编码的原理,这部分在下面两篇帖子中。
技术选型
Web 菜鸡,什么 Qt 、 winui 都不会,只能用 electron 来做套壳应用,这对我来说上手最快,并且可以快速实现需要的功能。
最开始的时候选择了 electron + react + ant-design ,写到一半发现 hold 不住 react ,写的太别扭了,果断换成了 electron + vue + ant-design-vue ,再写到一半发现 ant-design-vue 写出来像一个后台管理系统,并且社区维护不积极,果断转到 vuetify 上。
所以最后的技术栈就是 electron + vue ,组件库选择了 vuetify 。以及一些杂七杂八的库。
需求
对于一个第三方的漫画 APP ,核心的功能我觉得就两个,一个是搜索、一个是浏览,细分下来就是:
- 搜索页
- 漫画详情页
- 漫画阅读页
其他都是甜头,比如什么首页,每周推荐,评论,甚至登录(除非需要登录才能查看)等等。
所以当时基本上是先实现这些核心的模块。
相关资源
项目模板
使用 electron + vue ,自己搭环境就太慢了(并不是我不会),直接使用下面这个模板。
项目地址: electron-vite-vue
electron-vite 这个组织开源了多个项目,包括和 vue 或者 react 集成的模板,以及开箱即用的 vite 插件,省去了自己搭建目录,对于我这个体量的项目完全够用了。
使用 electron-vite-vue 还有一个好处就是 electron 主进程的代码直接可以使用 TypeScript ,太棒了。
组件库
这里选用了 vuetify ,它是一个基于 material 的组件库,组件全面(不过 material 感觉和国内的审美水土不服,看多了国内的 app ,再看谷歌系的总感觉到了另一个次元)。
配合 unplugin-vue-components 插件完成自动导入,省略很多 import 语句 。
1 | // vite.config.ts |
启动之后会生成 components.d.ts
文件,注意要将它加入到 TypeScript 中,这样可以得到模板中的类型提示。
1 | // tsconfig.json |
PS:之前在使用 ant-design-vue 的时候,发现了一个 bug ,但是提交到了 issue 上还没修(虽然我也不确定到底是 bug 还是特性),果然还是得有官方的团队的组件库才比较可信…
这里贴上 issue 地址:the args of notification.useNotification does not work 。
也不知道要多久才能处理这个 issue …
原子 css
这里使用了 unocss 来生成 css 类,主要是使用一些工具类,使用较多的是 flex 相关的。
首先要在 vite 中添加 unocss 插件。
1 | // vite.config.ts |
然后额外创建一个 uno.config.ts
文件。
1 | // uno.config.ts |
这里我们配置了 tailwind 规则,以及多生成了两个类,主要是来快速处理头部部分是否能够拖动的情况。
最后在 main.ts
中导入 uno.css
即可
1 | // src/main.ts |
接口请求
以往基本上都是用 axios 的,这次用了 alova ,它附带的功能更多,而且内置了 useRequest 、 usePagination 的组合式 api ,大大减少了很多样板代码,很棒,以及提供了接口缓存,这样子在某些接口上开启缓存,可以减少接口调用次数,切换页面的体验上升很多。
alova 和 axios 在某种情况下还是很像的,都是创建一个请求实例,用这个实例来发起 Get 、Post 等请求。
1 | // src/apis/http.ts |
这里 onSuccess
就和 axios 中的拦截器差不多, beforeRequest
在请求前添加一些全局的请求头,注意这里使用了 xhrRequestAdapter
,即基于 XMLHttpRequest 封装的适配器,由于 fetch 适配器不支持获取上传下载进度,而功能里面有下载漫画,所以需要使用 xhrRequestAdapter
。 vueHook
则让 alova 能够在 setup 中使用组合式的 api ,这里用的最多的就是 useRequest
和 usePagination
,特别是 usePagination
相当的方便,很多的列表页都只要一个 usePagination
就能拿到大部分的状态。
接着我们只需要编写相应的业务请求即可,比如:
1 | // src/apis/ajax.ts |
这里有两个注意的点
一个是封装的时候不要添加 async ,即 loginApi
不要加 async ,不然 setup 内传入 useRequest
和 usePagination
会提示类型错误,原因是 http.Post
返回的是一个 Method
类型,它是一个类 Promise 而不是 Promise ,如果我们直接调用 loginApi('username', 'password')
是不会发起请求的,只有 loginApi('username', 'password').then()
才会。
另一个是 http.Post
的两个泛型分别是转换后类型以及转换前类型,如果接口需要进行数据转化的话,填写这两个类型可以在 transform
内获得良好的类型提示,如果不需要转化则直接编写第一个泛型即可。
在 setup 中,使用 usePagination
可以获取绝大部分的状态:
1 | <script setup lang="ts"> |
主进程的 ioc
既然是自己写的项目,那就得玩玩一些平时用不上的东西。
所以这次我在项目里面使用了微软的依赖注入库 tsyringe 。
这是一个轻量的 IOC 库,用法也很简单,和 spring 很相似。
首先我们要在 tsconfig.json
内开启装饰器支持。
1 | { |
接着我们还要安装一个 reflect-metadata
,它是一个对 Reflect 的一个扩展,可以让对象存储元数据,在入口处直接导入即可。
1 | // electron/main.ts |
接着我们给模块加上 @singleton
装饰器
1 | import { singleton } from "tsyringe"; |
这里使用 singleton
而不是 injectable
的原因是 injectable
每次注入都会生成一个新的对象,而 singleton
只会有一个对象。
要注意这样只是注册了这个类,如果我们直接运行容器内是不会有这个类的实例的,如果想要让类初始化实例,则需要调用另一个 API 。
1 | // electron/main.ts |
所以整个 electron 侧的逻辑就是在 electron/modules
下编写模块,然后在 electron/main.ts
调用 win 模块来初始化整个应用,不过这里目前还有一些问题,就是某些模块并不会和 win 模块存在依赖关系,通常只会通过 ipcMain 来监听一些事件然后处理,这里用的一个方法是注入但不使用
1 | import { inject, singleton } from "tsyringe"; |
这里要加这个注释,不然 TypeScript 会报错。
或许这里改成扫描 electron/modules
下的模块并且直接 container.resolve
会更好?🤔
better-sqlite3
在编写下载功能的时候,需要一个轻量的数据库,这里选用了 sqlite 。
这里提一嘴这个是可能在开发或者打包会报错,提示 node(或其他) 版本不正确问题。
这时需要使用执行 electron-builder install-app-deps
来讲依赖重新编译为当前环境的版本
可以在 package.json
添加 postinstall
来让 pnpm i
完成后触发
1 | { |
快速引入字体
这里可以使用 Fontsource 。
这个仓库收录了开源可商用的字体,可以在此处搜索相应的字体。
导入也非常简单,安装对应的字体文件,然后导入即可。
1 | pnpm install @fontsource/roboto |
引入:
1 | // src/main.ts |
然后在需要的地方使用即可。
1 | body { |
Express 代理接口
在生产环境下,应用会在主进程启动一个 Express 实例,来代理相关的接口。
1 | // 核心代码 |
由于接口在大陆并不总是能使用,所以我们需要为这个代理启用一个“梯子”
做法就是配置它的 agnet
属性。
先安装 https-proxy-agent
,表明我们想代理一个 https 域名(这里要根据需要选择 http-proxy-agent
或者 https-proxy-agent
)。
使用很简单,如下:
1 | import { HttpsProxyAgent } from "https-proxy-agent"; |
这里的 http://localhost:10809
就是代理服务的域名,由于我们使用的是 v2rayN ,所以这里默认就是 http://localhost:10809
。
至于代理的这些信息,就通过渲染进程的用户填写后拿到。
渲染进程使用代理
除了 Express ,在渲染进程中我们需要访问第三方的图片,在网络不好的时候,我们希望使用代理进行加速,这里需要导入 electron 的 session
,然后使用 setProxy 这个方法。
1 | import { |
这样设置之后渲染进程的请求就会走这个代理。
缩放
electron 提供给了我们一个 win.webContents.setZoomFactor(zoomFactor)
来设置缩放因子
但是要注意,这个功能默认是关闭的,直接调用这个方法是没法改变的,这里需要先调用 win.webContents.setVisualZoomLevelLimits(1, 3)
后再调用 win.webContents.setZoomFactor(zoomFactor)
才能成功缩放,这里当时卡了很久…
1 | // 必须先调用 setVisualZoomLevelLimits 解除缩放限制 |
记录用户窗口位置
核心的 API 是 win.getBounds()
可以获取窗口当前的位置和大小,我们只需在退出前将信息保存在本地,然后下次启动前读取这个信息设置位置和大小即可。
1 | // 保存 |
使用工作流
因为仓库是在 github 上开源的,所以可以使用工作流来帮我们打多个环境的包。
在本项目中,有两个工作流,一个是监听 tag 推送然后根据 tag 打包,一个是手动触发的最新打包。
监听 tag 只需要如下配置:
1 | on: |
这里我们使用 npm version major|minor|patch
命令来更新版本,这个命令会生成一个提交,在这个提交上面标上一个 tag 。
接着我们 git push && git push origin v1.9.0
来把这个 tag 提交到远程就可以触发上面的工作流了。
手动触发的则需要使用如下配置
1 | on: |
这样在 Actions 页面就有一个按钮可以启动工作流了。
一般使用工作流的时候,基本上是在 linux 上跑的,但是 electron 的打包是需要在对应系统上打对应系统的包的,即 linux 只能打 linux 的包,而 windows 只能打 windows 的包,所以我们需要配置一下,让工作流跑两个环境,核心就是下面这个配置。
1 | jobs: |
这样子就会有两个任务在跑。
然后某些步骤如果需要指定只在某个环境跑,可以在步骤上用一个 if 语法,比如如果要缓存 pnpm 的包的话,在 window 上的路径和 linux 上的路径不一样,所以需要两个 step 。
1 | jobs: |
最后,如果是基于 tag 的推送的,可以借助 electron-builder 自动提交到 release 上。
1 | jobs: |
这里记得要注入 GH_TOKEN
,这样才能推送成功,pnpm run build
实际上运行了 vite build && electron-builder --publish always
这里自动推送就是 --publish always
起作用。
而如果是基于最新的打包,则需要指定 --publish
为 never
,然后借助工作流 Artifacts 暂存文件,将打包后的文件上传到这里即可,这里需要使用 actions/upload-artifact
。
1 | jobs: |
后记
虽然启动慢了点,但是功能上还是相当完整的,自己还是非常开心能够完成这个项目。
虽然在不起眼的地方有很多 bug 。
在修了在修了,新建 fix 分支。