Monorepo:依赖漏斗与平台边界
一、目录不是架构,依赖方向才是架构
根目录 pnpm-workspace.yaml 把 apps/*、packages/**/*、桌面 layer/* 和移动端 Web 子工程纳入同一个 workspace。根 package.json 再用 Turbo 统一 build、typecheck、test、lint 和 Web 构建。
从工程角色看,可以画成五层:
这是一个“依赖漏斗”:越底层越稳定、越少知道平台;越靠上越接近产品和部署环境。
二、包的职责地图
| 包/目录 | 角色 | 不应该负责什么 |
|---|---|---|
packages/internal/types | 全局声明与共享类型入口 | 不承载业务副作用 |
constants | 枚举、标签、视图和 App 常量 | 不直接发请求 |
models | RSSHub/模型形状与数据映射辅助 | 不决定 UI 生命周期 |
database | SQLite schema、迁移、服务查询 | 不渲染界面 |
shared | 环境、认证、桥、设置、跨端工具 | 不承载某个平台页面 |
utils | URL、HTML、语言、媒体、数据结构工具 | 不隐藏网络请求 |
store | 领域状态、同步、hydrate、mutation | 不依赖 Electron API |
components | 跨端可复用 UI 和编辑器组件 | 不拥有整个应用路由 |
desktop/layer/renderer | 桌面/Web 主应用与页面 | 不把数据库实现散落到页面 |
apps/ssr | Web 客户端、SSR、Meta/OG 处理 | 不成为移动端业务层 |
apps/mobile | Expo/React Native 壳与原生能力 | 不复制服务端模型 |
三、依赖方向里最关键的三条规则
规则 A:共享层不反向依赖应用层
@follow/store 可以被桌面和移动端共同依赖,但不能 import apps/desktop 的页面。否则一旦移动端复用 Store,Metro 会把 Electron 特有代码拖入打包。
规则 B:平台差异通过条件入口解决
数据库存在 db.desktop.ts 和 db.rn.ts 两种实现;共享设置通过 env.common.ts、env.desktop.ts、env.rn.ts、env.ssr.ts 分流;工具也有 event-bus.ts 与 event-bus.rn.ts。这比在业务代码里到处写 if (Platform.OS) 更容易审计。
规则 C:第三方协议在边界处翻译
@follow-app/client-sdk 是远端协议;本地 Store 使用自己的 Model;Drizzle 使用自己的 schema。翻译点集中在 store/src/morph 和数据库服务中,避免 SDK 版本升级把整个 UI 变成“远端 DTO 的直接消费者”。
四、为什么根脚本偏向 Turbo,而不是一个巨型脚本
根脚本只表达跨包任务:build:packages、typecheck、test、lint、build:web。具体应用如何构建写在各自 package.json,Turbo 负责任务依赖和缓存。
例如 typecheck 依赖 @follow/electron-main#build,说明类型检查不是纯静态动作:Renderer 依赖 Electron Main 导出的类型/声明,必须先让这条边界产出可消费的结果。
五、把 Folo 搬到新平台要复制什么
如果要增加一个新客户端,最小集合不是“复制一套页面”,而是:
- 接入共享
types/constants/models; - 初始化
database和store,确定平台 DB 适配; - 复用 client SDK、认证、设置、查询和内容解析;
- 只在平台壳层实现启动、导航、通知、文件、推送和支付等差异;
- 为该平台补构建和发布流水线。
这套边界的核心价值是让业务复杂度只增长一次,平台数量增长不必线性复制。