跳到主要内容

Electron 工程一个比较好的架构介绍

MoyuScript

Electron 是一个跨平台桌面应用程序构建工具,你可以使用前端技术和 NodeJS 来构建桌面应用程序(MacOS、Windows、Linux),我们熟悉的 VSCode 就是使用它来构建的。

我在前段时间写了个跨平台哔哩哔哩视频下载工具,发现网上关于 Electron 的相关信息还是挺少的,因此我在这给大家分享一些我开发这个工具的一些经验。

当然,一篇文章肯定讲不完所有细节,所以这里只是简单介绍一下,以后有机会可能还会详细介绍各个细节。

一些 Electron 基本的概念在本文不再赘述,如果你还不清楚的话,可以到 Electron 官网进行学习

项目架构

整体使用了 Typescript

总架构

这里只列一下关键目录和文件:

app/
├─ assets/ -- 项目资源,比如给 README 引用的图片,还有 LOGO 设计源文件等
├─ bin/ -- 可执行文件,文件夹名为 {平台名}/{架构},比如有一个 64 位 exe 的文件应该放在 bin/win32/x64/xxx.exe
├─ build-resources/ -- 构建资源,比如程序图标
├─ docs/ -- 文档,也可以是 GitHub Pages 主页
├─ src/ -- 源文件
│ ├─ common/ -- 通用,主进程和渲染进程都会用到的东西
│ ├─ main/ -- 主进程
│ │ ├─ services/ -- 服务
│ │ ├─ bridge.ts -- RPC 注册
│ │ ├─ main.ts -- 程序入口
│ │ ├─ preload.ts -- Preload
│ ├─ renderer/ -- 渲染进程
│ │ ├─ components/ -- 通用组件
│ │ ├─ public/ -- 静态资源文件
│ │ ├─ redux/ -- 状态管理 Redux
│ │ ├─ windows/ -- 窗口
│ │ │ ├─ main/ -- 主窗口
│ │ │ │ ├─ components/ -- 只会在主窗口用到的组件
│ │ │ ├─ other/ -- 其他窗口
│ │ ├─ env.d.ts -- 环境变量类型声明
│ │ ├─ global.d.ts -- 全局类型声明(比如定义 window.jsBridge)
│ │ ├─ vite.config.js -- Vite 配置
│ ├─ types/ -- 公共类型

页面(渲染进程)

构建工具:Vite。不得不说 Vite 确实很好用,极大提高了开发效率。

框架:React。其实用 Vue 也是一个很好的选择,不过我更熟悉 React 就用了,只是个人习惯问题。

组件库:Ant Design。部分页面使用 antd 开发会非常节约时间,如果有什么特殊的样式要求,也可以只使用其中部分功能。

Hook 库:ahooks。封装了一些常用的 React Hook,非常好用。

状态管理:Redux。使用了官方的 @reduxjs/toolkitreact-redux

目录结构:参考了业界的一些比较好的实践(如目录结构 (umijs.org)),结合了自身习惯和具体需求,但是后面发现结构其实还不太完美,后面学习中发现了一个更好的实践:bulletproof-react

风格规范:ESLintHuskyPrettierEditorconfig 等。

主进程

构建:其实只是 tsc 了一下。

RPC:主要是将服务分离出来,然后在 preload 脚本里面进行注册。

RPC

RPC 服务使用到了 IPC 通信,但是 Electron 接口只是给了最基本的通信实现,还需要自己设计一个良好的架构,否则随着项目越大,维护起来就会越困难。

我采用了定义和注册服务的方式,可以简单举个例。

定义一个服务:

// ./services/example-service
const exampleService = {
name: 'example',
fns: {
async hello() {
return 'hello';
}
}
};

export default exampleService;

导出所有服务:

// ./services.js
import exampleService from './services/example-service';

export const services = [
exampleService,
];

export function makeChannelName(name, fnName) {
return `${name}.${fnName}`;
}

主进程注册服务:

// ./main.js
import { services, makeChannelName } from './services.js';

services.forEach((service) => {
Object.entries(service.fns).forEach(([apiName, apiFn]) => {
ipcMain.handle(makeChannelName(service.name, apiName), (ev, ...args) =>
apiFn(...args)
);
});
});

preload 脚本:

// ./preload.js
import { services, makeChannelName } from './services.js';

function createJsBridge(): any {
const bridge = {};

services.forEach((service) => {
bridge[service.name] = {};

Object.keys(service.fns).forEach((fnName) => {
bridge[service.name][fnName] = (...args) =>
ipcRenderer.invoke(makeChannelName(service.name, fnName), ...args);
});
});
return bridge;
}

contextBridge.exposeInMainWorld('jsBridge', createJsBridge());

调用:

// Html 页面
await window.jsBridge.example.hello(); // 返回 hello

这样,我们如果需要新增服务或者修改服务,它就会被自动注入到渲染进程中,维护起来非常方便。

调试

调试可以分为两种,一种是主进程调试,另外一种是页面调试。

主进程调试

主进程调试可以参考:使用 VsCode调试 | Electron (electronjs.org)

此外,如果需要代码修改后就自动重启,需要用到 nodemon 来监控文件变更,检测到变更就重启应用。

如果你的项目用到了 TS,需要将 TS 编译后才能进行调试,记得打开 sourceMap 开关,这样你就可以直接在 TS 文件中打断点了。

页面调试

页面调试和普通的浏览器页面调试基本是一样的。

首先需要启动调试服务器,我这里用的是 Vite,因此很容易就可以启动开发服务器,然后记下服务器链接。

然后我们需要窗口加载的是上面获取到的 URL 而不是文件,因此我们可以判断当前环境来确定是加载文件还是 URL,代码如下:

if (process.env.NODE_ENV === 'development') {
await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS]);
mainWindow.once('show', () => mainWindow.webContents.openDevTools());
// 开发环境加载开发服务器 URL
await mainWindow.loadURL('http://localhost:3000/');
} else {
await mainWindow.loadFile('./build/renderer/index.html';
}

这样就可以开始愉快地调试了。

构建

构建我使用的是 electron-builder,相关配置可以参考官方文档。

需要注意的是,在构建前需要进行一些操作,比如打包页面代码、主进程 TS 代码编译为普通 JS 代码。

我的 package.json 部分配置如下:

"scripts": {
"clear": "rimraf ./build",
"build:main": "tsc -p ./src/main/tsconfig.json",
"build:renderer": "vite build -c ./src/renderer/vite.config.js",
"build": "npm-run-all clear build:main build:renderer",
"dist": "npm run build && electron-builder"
}

这样就可以使用 npm run dist 一键自动打包并构建应用了。

构建我目前是手动在 Win 和 Mac 上构建的,如果你有构建服务器的话也可以配置自动化流程,或者用 GitHub Actions 也可以(可能会比较慢)。

主要是我需要手动测试一下两端的功能是否正常,就选择手动构建了。