Appearance
四、技术选型思考 (Technology Decisions)
技术选型没有绝对的对错,只有在特定约束条件下的权衡取舍。本章记录每个核心技术选型背后的思考过程,以及放弃其他方案的原因。
4.1 为什么选 Flutter 而非 React Native / Electron
备选方案对比
| 维度 | Flutter | React Native | Electron |
|---|---|---|---|
| 语言 | Dart | JavaScript / TypeScript | JavaScript / TypeScript |
| 渲染方式 | 自绘引擎(Skia/Impeller) | 调用平台原生组件 | Chromium 内核 |
| 跨平台范围 | Android · iOS · Win · Mac · Linux · Web | Android · iOS(桌面支持较弱) | Win · Mac · Linux |
| 性能 | 高,接近原生 | 中,受 JS Bridge 限制 | 低,内存占用大 |
| 包体积 | 中 | 中 | 大(含 Chromium,100MB+) |
| 社区生态 | 活跃,Google 主导 | 活跃,Meta 主导 | 活跃,适合工具类应用 |
选择 Flutter 的理由
目标平台决定了选型:本项目明确要求同时支持 Windows 桌面端和 Android 移动端。React Native 的桌面支持(react-native-windows)社区活跃度远不如移动端,生态碎片化严重。Electron 虽然桌面支持成熟,但不支持 Android,且 100MB+ 的包体积对一个待办应用来说过于臃肿。
性能与一致性:Flutter 的自绘引擎绕过了平台原生组件,在所有平台上渲染出像素级一致的界面。React Native 依赖 JS Bridge 与原生通信,在复杂列表场景下容易出现帧率抖动。
Dart 语言:强类型、async/await、Stream 响应式编程,与后端同语言,模型定义可以共享思路,降低了全栈开发的心智成本。
4.2 为什么选 Dart Shelf 而非 Node.js / Go
备选方案对比
| 维度 | Dart Shelf | Node.js (Express) | Go (Gin) |
|---|---|---|---|
| 语言 | Dart | JavaScript | Go |
| 性能 | 高 | 中(单线程事件循环) | 极高 |
| 开发效率 | 高 | 高 | 中 |
| 前后端同语言 | ✅ | ❌ | ❌ |
| 生态成熟度 | 较小 | 极成熟 | 成熟 |
| 部署产物 | 编译为原生二进制 | 需要 Node 运行时 | 编译为原生二进制 |
选择 Dart Shelf 的理由
前后端同语言是核心驱动力。前端已经用 Dart,后端选 Dart 意味着:
- 数据模型(
Todo类、枚举定义)的设计思路可以直接复用,不需要维护两套格式转换逻辑 - 只需要熟悉一套语言的工具链、包管理、异步模型
- 错误处理的模式和思路在前后端保持一致
对于一个独立开发者来说,减少语言切换的心智成本比选择"最流行的技术栈"更重要。
Dart 编译出的原生二进制部署在 Docker 容器里,启动快、内存占用低,完全满足个人项目的性能需求。
4.3 为什么选 SSE 而非 WebSocket / 轮询
备选方案对比
| 维度 | SSE | WebSocket | 短轮询 | 长轮询 |
|---|---|---|---|---|
| 通信方向 | 单向推送 | 双向 | 单向 | 单向 |
| 协议 | 标准 HTTP | 独立协议 | HTTP | HTTP |
| 实现复杂度 | 低 | 中 | 极低 | 低 |
| 实时性 | 高 | 极高 | 低(取决于间隔) | 中 |
| 服务端资源 | 低 | 中 | 高(频繁请求) | 中 |
| Nginx 配置 | 需关闭缓冲 | 需配置 Upgrade | 无特殊要求 | 无特殊要求 |
| 断线重连 | 自动 | 需手动实现 | 不需要 | 不需要 |
选择 SSE 的理由
本项目的实时需求非常明确:服务端有数据变更时,通知客户端去拉取。这是严格单向的推送场景,不需要客户端通过长连接向服务端发消息。
- 对比 WebSocket:双向能力用不上,引入 WebSocket 只增加复杂度
- 对比轮询:短轮询每隔几秒发一次请求,大量无效请求浪费服务器资源;长轮询实现相对复杂,且在 Dart Shelf 里的支持不如 SSE 自然
- SSE 的优势:基于普通 HTTP,Nginx 只需关闭缓冲即可透传,断线后 HTTP 客户端自动重连,实现代价最小
4.4 为什么选 Drift(SQLite) 而非 Hive / Isar
备选方案对比
| 维度 | Drift (SQLite) | Hive | Isar |
|---|---|---|---|
| 数据模型 | 关系型(表、SQL) | 键值对(NoSQL) | 文档型(NoSQL) |
| 查询能力 | 强(SQL 全特性) | 弱(只能按 key 查) | 中(支持索引查询) |
| 类型安全 | ✅ 编译时检查 | 部分 | ✅ |
| 响应式查询 | ✅ Stream | ✅ | ✅ |
| 事务支持 | ✅ | ❌ | ✅ |
| 离线队列实现 | 自然(额外一张表) | 困难 | 可以 |
选择 Drift 的理由
本项目有一个关键需求:离线队列(sync_queue)需要与待办数据在同一个事务中写入,保证"本地数据写成功"和"操作入队"要么同时成功、要么同时失败,不能出现只写了本地数据但没入队的情况。
Hive 是键值存储,不支持事务,无法满足这个需求。Isar 支持事务,但面向文档型数据,SQL 查询能力弱,而本项目需要按优先级、截止时间多维度排序查询。
Drift 基于 SQLite,天然支持 ACID 事务,多张表的关联操作通过 SQL 表达简洁清晰,watchAll() 返回 Stream 实现响应式 UI 更新,是最自然的选择。
4.5 为什么选 JWT 而非 Session
备选方案对比
| 维度 | JWT | Session(服务端存储) |
|---|---|---|
| 服务端状态 | 无状态 | 有状态(需存储 Session) |
| 数据库查询 | 验证时无需查库 | 每次请求需查库验证 |
| 水平扩展 | 天然支持 | 需要共享 Session 存储 |
| Token 撤销 | 困难(需黑名单机制) | 简单(删除 Session 即可) |
| 适用规模 | 中大型、多实例部署 | 小型、单实例部署 |
选择 JWT 的理由
本项目虽然目前是单实例部署,但选 JWT 有两个实际原因:
一、移动端存储天然契合 Token 模式。Flutter 客户端把 Token 存储在本地(shared_preferences),每次请求携带,逻辑简单直接。如果用 Session,移动端需要维护 Cookie,在 Flutter 的 HTTP 客户端里处理 Cookie 反而更麻烦。
二、双 Token 机制平衡了安全与体验。access_token 有效期 1 小时,即使泄露影响窗口极短;refresh_token 有效期 30 天,存储在本地,用于静默刷新,用户无需频繁重新登录。这套机制用 Session 实现反而更复杂。
4.6 Nginx 在架构中的定位
Nginx 在本项目中不是可选项,而是必要的基础设施层,承担三个无法被替代的职责:
HTTPS 终止:Let's Encrypt 证书配置在 Nginx 上,后端 Dart 服务只处理明文 HTTP,不需要关心 TLS 细节。证书自动续期由 Certbot 管理,对后端完全透明。
安全隔离:后端服务绑定在 127.0.0.1:8080,外部网络无法直接访问。所有流量必须经过 Nginx,防止后端服务被直接暴露在公网。
SSE 专属路由:/events/ 路径在 Nginx 配置中单独处理,关闭 proxy_buffering 和 gzip,设置 86400 秒超时,确保 SSE 长连接数据能实时穿透到客户端,不被代理层缓冲积压。
nginx
location /events/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
gzip off;
}