全栈开发者做桌面应用,Electron 依然是最务实的选择。
2026 年了,每次提到 Electron,评论区总有人说"太臃肿了""吃内存"。但作为一个独立开发者,我的选择逻辑很简单:
开发效率 > 运行时性能。
原因有三:
我的个人财务管理软件「财富自由之路」就是一个 Electron + Vue 3 项目。本文分享完整的开发实战经验。
框架:Electron 28 + electron-vite 2
前端:Vue 3.4 + TypeScript 5.3
UI库:Element Plus 2.5 + 自定义主题
图表:ECharts 5.5(vue-echarts 封装)
状态管理:Pinia 2.1
数据库:better-sqlite3(本地 SQLite)
构建:electron-builder 24
测试:Vitest 1.2 + @vue/test-utils
electron-vite 而非 electron-forge?
electron-vite 基于 Vite,开发时热更新速度极快(<100ms),而 electron-forge 基于 Webpack,冷启动动辄 10 秒以上。对于日常开发体验,这是质的区别。
better-sqlite3 而非 IndexedDB?
财务数据需要复杂查询("过去 6 个月餐饮类支出趋势"),SQL 是天然的选择。better-sqlite3 是同步 API,在主进程中使用性能极佳,不需要处理异步回调地狱。
Element Plus 而非 Naive UI?
Element Plus 的表单组件更成熟,日期选择器、数字输入框、表格等开箱即用。财务应用大量使用表单,这是决定性因素。
wealth-freedom/
├── apps/
│ └── desktop/
│ ├── src/
│ │ ├── main/ # Electron 主进程
│ │ │ ├── index.ts # 入口
│ │ │ ├── database/ # SQLite 封装
│ │ │ └── ipc/ # IPC 通信处理
│ │ ├── renderer/ # Vue 3 渲染进程
│ │ │ ├── views/ # 页面组件
│ │ │ ├── components/# 通用组件
│ │ │ ├── stores/ # Pinia stores
│ │ │ └── router/ # 路由配置
│ │ └── preload/ # 安全桥接层
│ └── electron.vite.config.ts
├── packages/
│ └── shared/ # 主进程/渲染进程共享类型
├── pnpm-workspace.yaml
└── package.json
1. 主进程管数据,渲染进程管 UI
严格的职责分离。渲染进程通过 contextBridge 暴露的安全 API 与主进程通信,绝不直接访问 Node.js API。
// preload/index.ts
const api = {
// 财务记录 CRUD
getTransactions: (filter: TransactionFilter) =>
ipcRenderer.invoke('db:getTransactions', filter),
addTransaction: (data: TransactionData) =>
ipcRenderer.invoke('db:addTransaction', data),
// 数据导出
exportToExcel: (month: string) =>
ipcRenderer.invoke('export:excel', month),
}
contextBridge.exposeInMainWorld('app', api)
2. Monorepo 管理共享类型
用 pnpm workspace 把共享的 TypeScript 类型放在 packages/shared/,主进程和渲染进程都引用它。避免了类型定义重复和不同步的问题。
-- 核心表:交易记录
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- 'income' | 'expense' | 'transfer'
amount REAL NOT NULL,
category_id TEXT NOT NULL,
account_id TEXT NOT NULL,
date TEXT NOT NULL,
note TEXT,
tags TEXT, -- JSON 数组
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
-- 分类表(支持嵌套)
CREATE TABLE categories (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT,
parent_id TEXT,
type TEXT NOT NULL, -- 'income' | 'expense'
sort_order INTEGER DEFAULT 0
);
错误做法:在渲染进程中使用 better-sqlite3。
正确做法:在主进程中封装数据库操作,通过 IPC 暴露。
// main/database/db.ts
import Database from 'better-sqlite3'
import { app } from 'electron'
import path from 'path'
const dbPath = path.join(app.getPath('userData'), 'finance.db')
const db = new Database(dbPath)
// 开启 WAL 模式,提升并发读性能
db.pragma('journal_mode = WAL')
// 预编译语句,提升批量操作性能
const insertStmt = db.prepare(`
INSERT INTO transactions (id, type, amount, category_id, account_id, date, note)
VALUES (@id, @type, @amount, @categoryId, @accountId, @date, @note)
`)
// 批量插入用事务
export function batchInsert(records: TransactionRecord[]) {
const insertMany = db.transaction((records) => {
for (const record of records) {
insertStmt.run(record)
}
})
insertMany(records)
}
导入 10,000 条交易记录:
查询过去 12 个月分类汇总(含 JOIN):
财务应用的首页必须一眼看到关键信息:
不要让用户点三次才能看到自己的钱在哪。
记账是高频操作,录入体验决定用户留存:
50+30*2 自动计算)Cmd+N 新建记录,Cmd+E 编辑,Cmd+D 复制用 ECharts 做图表,几个关键配置:
// 折线图:平滑曲线 + 区域填充 + 鼠标悬浮显示详情
const chartOption = {
xAxis: { type: 'category', data: months },
yAxis: { type: 'value', axisLabel: { formatter: '¥{value}' } },
series: [{
type: 'line',
smooth: true,
areaStyle: { opacity: 0.15 },
data: netWorthData,
}],
tooltip: {
trigger: 'axis',
formatter: (params) => `${params[0].name}<br/>净资产:¥${params[0].value.toLocaleString()}`
}
}
{
"build": {
"appId": "com.wealth-freedom.app",
"productName": "财富自由之路",
"mac": {
"target": ["dmg", "zip"],
"category": "public.app-category.finance",
"hardenedRuntime": true,
"entitlements": "build/entitlements.mac.plist"
},
"win": {
"target": ["nsis"]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true
},
"extraResources": [
{ "from": "assets/", "to": "assets/" }
]
}
}
坑1:better-sqlite3 原生模块编译失败
# 用 electron-rebuild 重新编译原生模块
npx electron-rebuild
在 CI 中加入这个步骤,否则打包出来的应用会崩溃。
坑2:macOS 上提示"已损坏无法打开"
# 签名问题,开发阶段可以绕过
xattr -cr /Applications/财富自由之路.app
正式发布需要 Apple Developer 证书签名($99/年)。
坑3:DMG 体积过大(>200MB)
Electron 自带 Chromium,基础包就 130MB+。优化手段:
webview)electron-builder --config.compression=maximum数据分析和报表生成放在主进程(或 Worker Thread),不要阻塞渲染进程:
// main/workers/report-generator.ts
import { parentPort, workerData } from 'worker_threads'
// 在 Worker 中生成月度报告
const report = generateMonthlyReport(workerData.month, workerData.dbPath)
parentPort?.postMessage(report)
交易列表可能有几千条记录,全部渲染会卡死。用虚拟滚动只渲染可见区域:
<!-- 用 Element Plus 的虚拟表格 -->
<el-table-v2
:columns="columns"
:data="transactions"
:width="800"
:height="600"
:row-height="50"
fixed
/>
const routes = [
{
path: '/dashboard',
component: () => import('../views/Dashboard.vue')
},
{
path: '/analysis',
component: () => import('../views/Analysis.vue') // 重型图表页面按需加载
}
]
重点测试数据处理逻辑,不测 UI 渲染:
// 财务计算工具函数测试
describe('calculateNetWorth', () => {
it('应正确计算净资产', () => {
const assets = [{ amount: 100000 }, { amount: 50000 }]
const liabilities = [{ amount: 30000 }]
expect(calculateNetWorth(assets, liabilities)).toBe(120000)
})
it('应处理空数据', () => {
expect(calculateNetWorth([], [])).toBe(0)
})
})
用 @vue/test-utils 测试组件与 Store 的交互:
it('添加交易后应更新列表', async () => {
const wrapper = mount(TransactionList, {
global: { plugins: [createPinia()] }
})
await wrapper.find('.add-btn').trigger('click')
// 填写表单...
expect(wrapper.vm.transactions.length).toBeGreaterThan(0)
})
electron-rebuild 确保原生模块编译正确electron-updater)app.getPath('userData'))Electron 的"缺点"——内存占用、包体积——对于一个财务工具来说完全可以接受。用户每天打开 5 分钟记个账、看看数据,128MB 内存不是问题。
真正重要的是:你能多快把产品做出来,然后开始收集真实用户反馈。
技术选型的最优解,永远是你最熟悉的那套。独立开发者的核心竞争力不是技术栈的先进性,而是产品洞察力和执行力。
本文基于「财富自由之路」桌面端开发实践整理。项目使用 MIT 协议开源,欢迎交流:GitHub
用 ❤️ 和 AI 构建
GitHub: Wealth Freedom →