[electron] electron の 快速尝试
[electron] electron の 快速尝试
起因太长了,写完了回顾了一下,我还是选择用分 section 的方法方便跳过
起因
起因是上个礼拜老板让我从数据库里面拉点数据,修改一下内容,然后打算用当前的数据去准备新环境。我目前主要还是负责前端部分的功能,按道理来说后端的东西不会到我手上的,不过之前一个同事离职了,到现在还没找到新的 resource,所以这事儿最后还是落在了我的头上
本来以为我们手上其实会有一些 script/pipeline 之类的,可以直接运行拉数据,但是令人意外的是……没有……
所以大家拉数据的方法……可能还是非常原始的,使用 datagrip 连接数据库,选中对应的表单,手动拉数据,然后手动校准,然后……没了……?
sh 脚本的构思
这种操作对我的电脑来说其实压力蛮大的,毕竟 intellij 家的 app 是出了名的吃资源。于是借着 copilot 的帮助,我还是简单的写了一个 sh 脚本,大体的跑法如下:
❯ ./downloadFile.sh --host 'some-host-address' --port 5432 --password 'somepasswordstring' --user 'some-user-name'
然后脚本里面有一些 pre-defeind 连接——感谢 bash4.0 的升级,终于支持了 array,可以迭代跑 csv 文件名-query
大体是通过 SELECT id, fielda, fieldb, fieldc FROM some_data_base ORDER BY id
去 query 数据,同时通过 libpq——postgres 的 client,将 query 到的结果 cv 到 csv 文件里。再修改了一下本身就写好的 node script,把一些数据按照要求修改了一下
整体来说操作不算太难,实现起来也比较简单,但是老板做了 code review 之后提出了一个新的需求——postgres 默认返回的 boolean 是 t/f
,但是我们处理数据的 pipeline 要求必须是 TRUE/FALSE
这个解决方法其实也不算太难——在 node 里面遍历数据是不可能的,因为一些 enum 也有可能是 t/f
,所以想要正确获得数据的结果,必须要是要知道数据的类型。最终想到的方法是用 sql 语法,也就是 upper(some_field::text) AS some_field
——换句话说还是要手动校准不少的数据
vscode 中用 regex 替换
一些数据比较一致的,比如说 allow_???
, enable_???
, ???_allowed
, ???_enabled
这种,尚且可以用 regex 进行替换——虽然我早就知道 vscode 可以用 regex 进行模糊搜索,但这还是真的第一次用,不过还是比较麻烦……
顺便记一下 regex 的语法:
-
\b(enable_\w+)\b
-> 这可以 match 所有的enable_???
,并保存为一个变量 -
UPPER($1::text) AS $1
-> 这里的 $1 就是上面找到所有的 matching 结果以
enable_???
为例,跑完替换后就把所有enable_???
替换成UPPER(enable_???::text) AS enable_???
如此这般,大概用了一天的时间写完了脚本完成了下载。老板甚是欢喜,觉得我太有效率了(?),于是又丢了一堆需求让我做下载文件。也就是说,弄十几个 csv 文件真的可能要搞一天啊……这也太好摸鱼了吧……
反正这种东西,不会是我永远负责的,为了提升一下效率,我打算写一个 app,可以保存 config/profile 之类的,可以一键导出已经保存好的 config/profile 里定义的文件
正好又很久之前学了一点点 electron,又一直没什么动力写个项目,所以趁着双休日的时间写了个 electron 的 app,目前是完成了 0.0.1 版本,即:
-
实现了数据库的连接
一个比较 scrum 的写法是:
作为一个用户,我需要可以连接到数据库
-
可以管理连接的数据库
作为用户,我需要可以连接新的数据库、修改/删除/重新加载 已经连接的数据库
-
本地持久化连接的数据库
作为用户,再次打开 app 的时候,我需要能够看到之前保存的数据库
-
同时拉取所有 table 的 shema
这部分还没有做完整的展示功能,还没想好应该怎么合理的描述
-
打包了两个 build
一个是 intel chip 的,另外一个是 apple chip 的 arm64
intel 版本我自己试了下可以运行,顺便抓到了一个双休日出现的同时让他安装了一下,也没什么问题
这也就是目前 0.0.1 的功能:
- 支持连接 PostgreSQL 数据库
- 管理连接(新增 / 编辑 / 删除)
- 数据库连接信息持久化(localStorage 或 config 文件)
- 拉取所有 table 的 schema(展示功能未完成)
- 打包 x64 + arm64(mac)
项目配置
首要的目标是,必须要在双休日跑一个可以运行的 beta 版本,所以想要的就是快、快、快,不想要手动配置很多东西,想要一键生成可以运行的 boilerplate。所以在简单搜索一下后,锁定用 electron-vite
这个脚手架真的挺方便的,跑一下指令就可以:
❯ npm create @quick-start/electron@latest
# 下面就是自动提示,根据提示选择就可以生成对应的结构
✔ Project name: … <electron-app>
✔ Select a framework: › vue
✔ Add TypeScript? … No / Yes
✔ Add Electron updater plugin? … No / Yes
✔ Enable Electron download mirror proxy? … No / Yes
Scaffolding project in ./<electron-app>...
Done.
不过我选的是 React,没看到 Add TypeScript?
这个选项,跑完了直接就是 TS 版本
我一开始在公司的 proxy 上所以跑失败了,它本身是要访问 github 去下载 dependencies 的,或者说这是 electron 需要的,所以需要保证可以稳定访问 github……不然的话就设置一下 mirror。对我来说,关掉了 proxy+用官方镜像源就没什么问题了
另一个可能要注意的是 node 的版本,它需要 v18.17.1+ 以及 v20+,我之前好像用的是 v18.16,运行就失败了
这个运行完的结果好像会根据版本的不同而造成一定的差异……(挠头)我个人电脑跑出来的结果和公司跑出来的就不太一样……
不过实现上不会有太大的问题,直接按照提示,cd,install,start dev 就行了
我用 node v22 新建出来的结构是这样的:
❯ tree .
.
├── README.md
├── build
│ ├── entitlements.mac.plist
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── dev-app-update.yml
├── electron-builder.yml
├── electron.vite.config.ts
├── eslint.config.mjs
├── package.json
├── resources
│ └── icon.png
├── src
│ ├── main
│ │ └── index.ts
│ ├── preload
│ │ ├── index.d.ts
│ │ └── index.ts
│ └── renderer
│ ├── index.html
│ └── src
│ ├── App.tsx
│ ├── assets
│ │ ├── base.css
│ │ ├── electron.svg
│ │ ├── main.css
│ │ └── wavy-lines.svg
│ ├── components
│ │ └── Versions.tsx
│ ├── env.d.ts
│ └── main.tsx
├── tsconfig.json
├── tsconfig.node.json
└── tsconfig.web.json
10 directories, 26 files
而 node18 运行的结果中,src
是作为 React 的代码进行存储的,也就是 renderer 在的地方,除此之外基本没什么差异
项目模块 & 描述实现
从上面的结构上可以看到,electron 中最重要的是 3 个模块:main,preload 和 renderer,那么我就简单的描述了一下做了什么
具体的代码在公司电脑上,出于安全和保密的问题,我就不动公司电脑了,因此也就不会贴代码,等之后抽空好好重新学一下 electron 在放代码
main
这是 electron 的主程序,可以理解成是后端部分,换言之,这个模块有着所有 node native support,包括 fs,os;同时它也处理 renderer → main 的 ipc 交流
这里才是处理数据库相关联的核心部分,也因此我在这里创建了两个文件:
-
db/postgres.ts
目前我们数据库用的是 postgres,所以暂时只做了 postgres
这是数据库链接的核心逻辑,为了保证 single responsibility principle,所有数据库相关的交互都在这里实现
-
ipc/connections.ts
这里就是负责 renderer → main 的 ipc 交流,大部分的操作都是通过
ipcMain.handle
去执行的,偶尔要操作ipcMain.on
,这时候就会触发 main → renderer 的交流这个文件负责的核心逻辑就是接收到 renderer 的 signal,并且触发对应的操作
如 UI 新增了一个
addConnection
,这里就需要 handle 对应的 ipc communication:-
接受到传过来的 connection 数据,调用 postgres 中本身写好的方法
-
通过 try-catch,判断 conn 是否成功
-
成功便更新本地数据,同时 emit 一个新的 event,表示数据更新
这里 renderer 也会监听对应的 event,同时 emit 一个新的 update/sync event 获取最新数据
我是选择通过 event-driven 的方法去实现,不过具体实现方法并不固定
-
失败则返回错误信息
-
大体如下:
const writeFilesSync = () => {}; ipcMain.handle("addConnection", async (event, conn) => { try { const result = await db.connect(conn); writeFilesSync(result); return { success: true, data: result }; } catch (err) { return { success: false, error: err instanceof Error ? err.message : String(err), }; } });
-
这里对于 package 的选择比较简单,用了 pg,周下载量七百万,也是个比较大、稳定的库了
目前我主要只用了 Client
去创建 connection,从 information_schema
中拉数据,还没有完成下一步的操作
pg-native
具体跑的时候我是遇到了一个问题:
Could not resolve pg-native imported by pg
这个原因是 pg 找不到 peer dependency,这个问题也不是很重要,因为 pg 本身是用纯 js 实现了 CRUD 的操作的,所以可以用下面这个配置,告诉 electron 无视掉 pg-native 这个实现:
main: {
// Tell Vite to externalize pg-native
external: ['pg-native'],
entry: 'main/index.ts',
},
preload
这是 main → renderer 和 renderer → main 的 event bridge
只有在 event bridge 中定义的方法,才能完成 main ⇔ renderer 的双向沟通,这里也是控制了 renderer 的权限部分
比如说,main 有一个方法可以获取系统本地的一些敏感文件,如 ssh 的密钥,这部分 renderer 是不需要访问的。如果没有 event bridge,而 main 所有的 ipc connection 都暴露了,那么 renderer 就获取了太多的信息
同时需要注意的是,preload 有完整的 node api 的权限,但是 preload 不应该暴露所有的 api
毕竟,electron 还是基于 Chromium 实现的,换言之,如果有什么表单、上传功能,那么就有可能上传一个 js 文件,从而造成 xss 攻击
preload 正是为了避免 xss 攻击而生
renderer
这里就是 UI 部分的实现了,本质上来说还是可以当成一个 web application 去实现的
为了快速实现,这里还是用了不少的 package:
-
react
还是用了我写的最熟的框架,主要是为了快,想要两天内能出一个 beta version
-
zustand
一直听说过这个 package 一直没用过。听说它快,就想学习一下,就发现……
很!好!用!
它主要的优点有:
-
开箱即用,用法简单
参考官网文档:
import { create } from "zustand"; const useStore = create((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), updateBears: (newBears) => set({ bears: newBears }), })); // in the component function BearCounter() { const bears = useStore((state) => state.bears); return <h1>{bears} bears around here...</h1>; } function Controls() { const increasePopulation = useStore((state) => state.increasePopulation); return <button onClick={increasePopulation}>one up</button>; }
相对比起来,redux 和 redux toolkit 的使用方法都要稍微麻烦一些:
创建 redux state → 需要实现 middleware,reducers → 实现单独一个 slice
-
原生支持 async
对比一下,redux 需要依赖第三方包实现 async;redux toolkit 需要实现
createAsyncThunk
,同时再管理 3 个 reducers -
支持更复杂的数据
redux 本身只支持 array 和 object,为了方便的 serializable
但是 zustand 支持各种各样的格式,包括 set、map 等,比起来更加的方便
以我目前对这个项目需求来说,zustand 绝对比 redux toolkit 更加的合适
甚至我觉得,如果不需要使用 RTKQ 的 caching 这种情况下,使用 zustand 绝对会比 redux toolkit 方便很多
-
-
tailwind,没什么好多说的,preset css 让开发方便很多
-
react-hot-toast,好用的 notification
-
@radix-ui/react-dialog
主要是做 confirmation,我想试着用 toast 写的,可惜的是 toast 写出来的 confirm dialog 卡顿太严重了……
-
clsx, tailwind-merge
这两个是我找到一个 app 里用的,可以比较方便的 merge tailwind 的 class,实现方法如下:
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } // usage cn("p-2", false && "text-red-500", { "bg-blue-500": isActive });
可以省略很多的 ternary
-
react-icons
简单好看的 icon
因为是 desktop app,所以就不需要考虑 router 的问题了
build
搞的时候很麻烦,大概 4-5 个小时,但是搞完了之后,发现其实改的东西也不是很多,而且版本不同配置也可能会没办法通用
TS 报错
这个问题主要出现在 unused variables 上,修改的方法比我想象中简单多了,所有没使用的变量前面加 _
即可。如 const unusedVar = ''
,改成 const _unusedVar = ''
,TS 就会自动忽略
html 没有加载 js
这个问题我找了很久,最后在我这里,肯定需要加的是 package.json 里面的配置,vite 的不是很确定
package.json 的修改如下:
{
"build": {
"files": ["dist-electron/**/*", "dist/**/*", "package.json"],
"asarUnpack": ["**/*/node"],
"mac": {
"target": [{ "target": "dmg", "arch": ["x64", "arm64"] }]
}
}
}
如果没有这个,那么 build 是肯定找不到对应的文件的
vite.config 修改如下:
export default defineConfig({
base: "./",
build: {
outDir: "dist",
emptyOutDir: true,
},
});
目前来说 bundle 的尺寸还是比较大,暂时我这里也不考虑优化问题了,等至少做到 1.0.0 再考虑下一步
上传 artifacts
不用不知道,才发现原来 gitlab 没办法通过 UI 上传 artifacts,不过这个操作可以通过 crul 完成:
❯ curl --header "PRIVATE-TOKEN: some-personal-access-token" \
--upload-file dist/electron-vite-project-0.0.1.dmg \
"https://<repo-address.com>/api/v4/projects/<project-id>/packages/generic/electron-vite-project/0.0.1/electron-vite-project-0.0.1.dmg"
personal access token 可以从 gitlab 中的 setting 找到
project id 可以在项目里的 settings 找到