移动端壳:Expo、原生模块与业务复用
一、移动端不是缩小版桌面端
apps/mobile 同时包含 Expo 配置、iOS/Android 原生目录、E2E 流程、原生模块、导航/屏幕、播放器、推送、后台任务、IAP、OTA 和移动专属设置。它使用 React Native 0.86、Expo 57,并通过 NativeWind、FlashList、Reanimated、Screens 等建立移动交互基础。
移动端的复用重点在业务状态和领域模型,而不是强行复用所有 DOM 组件。
二、移动端启动的能力集合
从 src/initialize、src/providers 和 src/modules 可以抽出启动顺序:
移动端的“启动完成”不是 React Root 挂载就结束,而是设备、数据库、认证、服务端配置、字体、通知和播放器逐步准备后才进入完整交互。
三、数据库复用与平台差异
移动端通过 database/src/db.rn.ts 打开 Expo SQLite,schema 和 migration 与桌面共享。store 的 hydrate 过程对页面透明,因此 Feed、Entry、List 和未读逻辑不需要为 iOS/Android 重写。
平台差异被隔离在:
- Expo module(文件、分享、设备、IAP、通知、任务);
src/lib/platform.ts、secure-store.ts、messaging-registration.ts;native/ios、native/android中的原生实现;- React Native 专用 UI 和手势。
四、信息流 UI 的移动化
移动端 Entry 列表按内容形态拆成 EntryNormalItem、EntrySocialItem、EntryVideoItem、EntryPictureItem 和 EntryListContentArticle 等模板。这里没有把所有 Entry 都塞进一张巨大组件,而是先由数据/视图选择内容模板,再由模板处理媒体、摘要和阅读动作。
@shopify/flash-list 用于大量条目,viewable-mark-read 负责“可见即读”一类行为,并有测试保护 refresh、可见项和批量标记逻辑。滚动行为因此成为领域同步的一部分,而不是纯动画。
五、播放器与 TTS 是独立子系统
播放器目录包含 TtsStreamProvider、stream controller、TTS core、WebView HTML 和 Tab Bar。它把语音合成的流式数据、播放控制、音量、后台和 UI 状态分开,避免 Entry 页面直接管理音频生命周期。
这是一个可迁移的模式:任何“在页面之外持续运行”的能力(播放、上传、下载、同步、通知)都应该有自己的 provider/controller,而不是绑在一个页面组件的 mount/unmount 上。
六、登录与跨端 Cookie
modules/login、lib/auth.ts、auth-cookie-migration.ts、token.ts 和 better-auth 相关依赖构成认证边界。移动端同时支持社交登录和邮箱登录,还要处理 Cookie/token 在原生存储和 WebView/服务端之间的迁移。
认证状态应当通过 Provider/Store 提供给页面,不应由每个请求自己解析一次本地存储。这样登出、账号切换、设备恢复时才能集中清理本地投影和 Query 缓存。
七、移动端 OTA 的特殊性
移动端不是每次发布都重新走 App Store/Play Store。apps/mobile 有 update:export,apps/ota 有 Worker 路由和版本/manifest/policy/store-version 逻辑,GitHub Actions 的 publish-ota.yml 会生成并上传 OTA 资产,再通知同步端点。
OTA 因而必须绑定三种版本:
- native runtime version:原生二进制能承载哪些 JS;
- release version:本次 JS bundle 的版本;
- channel:production/preview 等分发环境。
如果把它们当成一个版本字符串,回滚和兼容判断会变得不可靠。
八、移动端带来的架构压力
移动端使共享包必须更严格:任何误引入 Electron/DOM 的依赖都可能让 Metro 构建失败;任何只依赖浏览器 API 的工具都要提供 RN 版本或明确禁用。Folo 的文件命名和 workspace 入口已经在做这项工作,但新增功能仍应优先从“平台无关核心 + 平台薄适配”开始设计。