跳到主要内容

移动端壳:Expo、原生模块与业务复用

一、移动端不是缩小版桌面端

apps/mobile 同时包含 Expo 配置、iOS/Android 原生目录、E2E 流程、原生模块、导航/屏幕、播放器、推送、后台任务、IAP、OTA 和移动专属设置。它使用 React Native 0.86、Expo 57,并通过 NativeWind、FlashList、Reanimated、Screens 等建立移动交互基础。

移动端的复用重点在业务状态和领域模型,而不是强行复用所有 DOM 组件。

二、移动端启动的能力集合

src/initializesrc/providerssrc/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.tssecure-store.tsmessaging-registration.ts
  • native/iosnative/android 中的原生实现;
  • React Native 专用 UI 和手势。

四、信息流 UI 的移动化

移动端 Entry 列表按内容形态拆成 EntryNormalItemEntrySocialItemEntryVideoItemEntryPictureItemEntryListContentArticle 等模板。这里没有把所有 Entry 都塞进一张巨大组件,而是先由数据/视图选择内容模板,再由模板处理媒体、摘要和阅读动作。

@shopify/flash-list 用于大量条目,viewable-mark-read 负责“可见即读”一类行为,并有测试保护 refresh、可见项和批量标记逻辑。滚动行为因此成为领域同步的一部分,而不是纯动画。

五、播放器与 TTS 是独立子系统

播放器目录包含 TtsStreamProvider、stream controller、TTS core、WebView HTML 和 Tab Bar。它把语音合成的流式数据、播放控制、音量、后台和 UI 状态分开,避免 Entry 页面直接管理音频生命周期。

这是一个可迁移的模式:任何“在页面之外持续运行”的能力(播放、上传、下载、同步、通知)都应该有自己的 provider/controller,而不是绑在一个页面组件的 mount/unmount 上。

modules/loginlib/auth.tsauth-cookie-migration.tstoken.ts 和 better-auth 相关依赖构成认证边界。移动端同时支持社交登录和邮箱登录,还要处理 Cookie/token 在原生存储和 WebView/服务端之间的迁移。

认证状态应当通过 Provider/Store 提供给页面,不应由每个请求自己解析一次本地存储。这样登出、账号切换、设备恢复时才能集中清理本地投影和 Query 缓存。

七、移动端 OTA 的特殊性

移动端不是每次发布都重新走 App Store/Play Store。apps/mobileupdate:exportapps/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 入口已经在做这项工作,但新增功能仍应优先从“平台无关核心 + 平台薄适配”开始设计。