Skip to content

四、技术选型思考 (Technology Decisions)

技术选型没有绝对的对错,只有在特定约束条件下的权衡取舍。本章记录每个核心技术选型背后的思考过程,以及放弃其他方案的原因。


4.1 为什么选 Flutter 而非 React Native / Electron

备选方案对比

维度FlutterReact NativeElectron
语言DartJavaScript / TypeScriptJavaScript / TypeScript
渲染方式自绘引擎(Skia/Impeller)调用平台原生组件Chromium 内核
跨平台范围Android · iOS · Win · Mac · Linux · WebAndroid · 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/awaitStream 响应式编程,与后端同语言,模型定义可以共享思路,降低了全栈开发的心智成本。


4.2 为什么选 Dart Shelf 而非 Node.js / Go

备选方案对比

维度Dart ShelfNode.js (Express)Go (Gin)
语言DartJavaScriptGo
性能中(单线程事件循环)极高
开发效率
前后端同语言
生态成熟度较小极成熟成熟
部署产物编译为原生二进制需要 Node 运行时编译为原生二进制

选择 Dart Shelf 的理由

前后端同语言是核心驱动力。前端已经用 Dart,后端选 Dart 意味着:

  • 数据模型(Todo 类、枚举定义)的设计思路可以直接复用,不需要维护两套格式转换逻辑
  • 只需要熟悉一套语言的工具链、包管理、异步模型
  • 错误处理的模式和思路在前后端保持一致

对于一个独立开发者来说,减少语言切换的心智成本比选择"最流行的技术栈"更重要。

Dart 编译出的原生二进制部署在 Docker 容器里,启动快、内存占用低,完全满足个人项目的性能需求。


4.3 为什么选 SSE 而非 WebSocket / 轮询

备选方案对比

维度SSEWebSocket短轮询长轮询
通信方向单向推送双向单向单向
协议标准 HTTP独立协议HTTPHTTP
实现复杂度极低
实时性极高低(取决于间隔)
服务端资源高(频繁请求)
Nginx 配置需关闭缓冲需配置 Upgrade无特殊要求无特殊要求
断线重连自动需手动实现不需要不需要

选择 SSE 的理由

本项目的实时需求非常明确:服务端有数据变更时,通知客户端去拉取。这是严格单向的推送场景,不需要客户端通过长连接向服务端发消息。

  • 对比 WebSocket:双向能力用不上,引入 WebSocket 只增加复杂度
  • 对比轮询:短轮询每隔几秒发一次请求,大量无效请求浪费服务器资源;长轮询实现相对复杂,且在 Dart Shelf 里的支持不如 SSE 自然
  • SSE 的优势:基于普通 HTTP,Nginx 只需关闭缓冲即可透传,断线后 HTTP 客户端自动重连,实现代价最小

4.4 为什么选 Drift(SQLite) 而非 Hive / Isar

备选方案对比

维度Drift (SQLite)HiveIsar
数据模型关系型(表、SQL)键值对(NoSQL)文档型(NoSQL)
查询能力强(SQL 全特性)弱(只能按 key 查)中(支持索引查询)
类型安全✅ 编译时检查部分
响应式查询✅ Stream
事务支持
离线队列实现自然(额外一张表)困难可以

选择 Drift 的理由

本项目有一个关键需求:离线队列(sync_queue)需要与待办数据在同一个事务中写入,保证"本地数据写成功"和"操作入队"要么同时成功、要么同时失败,不能出现只写了本地数据但没入队的情况。

Hive 是键值存储,不支持事务,无法满足这个需求。Isar 支持事务,但面向文档型数据,SQL 查询能力弱,而本项目需要按优先级、截止时间多维度排序查询。

Drift 基于 SQLite,天然支持 ACID 事务,多张表的关联操作通过 SQL 表达简洁清晰,watchAll() 返回 Stream 实现响应式 UI 更新,是最自然的选择。


4.5 为什么选 JWT 而非 Session

备选方案对比

维度JWTSession(服务端存储)
服务端状态无状态有状态(需存储 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_bufferinggzip,设置 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;
}