Expo 主仓库 apps 目录全解析:测试用应用矩阵的定位、职责与运行方式
Expo(当前仓库根目录为 expo)采用 pnpm workspace 组织数十个源码包,而 apps 目录承载的正是验证这些源码包的一组"测试宿主应用"。本文以 apps/README.md 为核心脉络,逐一剖析 bare-expo、Expo Go、Native Component List、Sandbox、Test Suite 这五类应用的定位差异与真实运行方式,并结合仓库源码说明它们如何通过 workspace 依赖、Jasmine 测试框架与自动链接(autolinking)机制协同工作,让读者对"如何在跨平台 RN 框架仓库中设计多形态测试应用"获得可迁移的工程经验。
一、为什么 Expo 需要一个 apps 目录:测试应用的定位
apps/README.md 的第一句话就点明了该目录的本质:"This directory contains the apps we use for testing Expo."——apps 目录不是产品发布地,而是 Expo 团队验证框架自身的"试验台"。
其治理原则非常鲜明:不要向本仓库新增应用("Do not add new apps to this repository.")。原因在于每个应用都会抬升维护成本:多个应用会被打包进同一份 node_modules,不同应用依赖的包之间会以难以察觉的方式相互影响,导致依赖树膨胀、版本冲突排查困难。为了保证仓库健康可控,应用数量必须被刻意压到最少。
文档还给出了一个"理想形态"的演进方向:最好只保留一个目录,内含一个定制化工程,同时携带 Xcode 与 Android Studio 工程,通过不同的 build target 挂载不同的 native modules——比如分别产出 Expo Client、定制化 Test Suite(支持支付类 API)、定制化 Native Component List 三个构建目标。也就是说,当前的多应用矩阵本质上是向"单一宿主 + 多构建目标"收敛的过渡形态。
二、apps 目录下五大核心应用一览
| 应用 | 一句话定位 | 类型 |
|---|---|---|
| bare-expo | 可加载 Test Suite 与 Native Component List 的 JS,用于跑端到端验证 | 裸 RN 工程 + 开发客户端 |
| Expo Go | Expo Go 客户端的 UI(包名 @expo/home) |
托管式客户端宿主 |
| native-component-list | 默认 Expo 预设中组件与 API 的 showcase | 组件/API 展示应用 |
| sandbox | 被 Git 排除的本地试验项目 | 开发沙箱 |
| test-suite | 运行在 Expo 上的端到端测试应用 | 测试宿主 |
需要补充的是,从当前仓库 apps 目录的实际文件列表观察,除了上述五个 README 文档点名的应用外,目录中还演进出了 brownfield-tester、expo-go(eas-expo-go)、expo-workflow-testing、minimal-tester、native-tests、notification-tester、observe-tester、router-e2e 等更垂直化的验证工程,它们分别覆盖混合集成(brownfield)、通知、路由 E2E 等专项场景,印证了"每类专项验证单独建宿主"与"最小化应用数量"之间的持续权衡。
三、bare-expo:一切端到端验证的裸工程宿主
bare-expo 是仓库中最关键的应用之一。它把 Test Suite 与 Native Component List 的 JS 源码直接当作 workspace 依赖加载(见其 package.json 中 "native-component-list": "workspace:*" 与 "test-suite": "workspace:*"),从而在一个完整原生构建(iOS 工程 + Android 工程)里运行面向整个 SDK 的测试。
3.1 日常开发命令
bare-expo/README.md 与 bare-expo/package.json 中的 scripts 段共同定义了完整的命令面:
| 命令 | 作用 |
|---|---|
pnpm android |
在 Android 模拟器上运行:按需执行 npm install、构建 React Android 二进制、生成模拟器、启动 Metro,最后在模拟器打开应用 |
pnpm ios |
在 iOS 模拟器上运行:自动 pod install、npm install、打开模拟器、清理并启动 Metro,再于模拟器中打开应用 |
pnpm test:ios |
等同 pnpm ios,但按 E2E 测试模式准备(对应脚本中 NODE_ENV="test") |
pnpm test:android |
等同 pnpm android,同样以 NODE_ENV="test" 环境启动 |
pnpm open <ios | android> <模块...> |
深链进 test-suite 应用并只运行指定测试,要求平台已在运行 |
pnpm nuke |
清空全部生成产物(ios/Pods/、ios/build/、android/.gradle),用于验证初始化脚本 |
pnpm clear-metro |
清理 Metro 缓存与 watchman 状态 |
其中 pnpm open 是官方最推荐的高频调试姿势,README 给出的两个示例值得逐字对照:
# 深链到 iOS 上的 test-suite,只跑 Constants、Crypto 两组测试
pnpm open ios Constants Crypto
# 深链到 Android,只跑 Random 一组测试
pnpm open android Random
该命令实际执行 bare-expo/scripts/deep-link.sh(对应 package.json 中 "open": "./scripts/deep-link.sh test-suite"),通过深链将平台与模块参数透传给 test-suite。这种"平台运行一次、按需精测指定模块"的模型,能让开发者在修改某个原生模块后毫秒级复测,而不必每次都全量跑测试。
3.2 定制化的原生配置
从 bare-expo/package.json 的 expo.autolinking 段还能看出这个裸工程的"宿主野心":
"expo": {
"autolinking": {
"exclude": ["expo-ads-facebook", "expo-ads-admob", "expo-module-template"],
"ios": { "flags": { "inhibit_warnings": false } },
"android": { "buildFromSource": [".*"] }
}
}
- Android 侧
buildFromSource: [".*"]:所有自动链接到的原生模块一律从源码编译,这正是验证模块源码正确性的关键设置; - 排除列表把已废弃的广告模块与模板模块挡在链接之外,避免无关原生代码进入宿主工程。
四、Test Suite:用 Jasmine 承载的端到端测试宿主
test-suite 的 README 用一句玩笑话概括了它的地位:"Hi! This is what will make Expo never break ever.🙏"——它是保证 Expo"永不坏"的回归防线。
4.1 运行与组织方式
按 apps/test-suite/README.md 的说明:
- 在目录内执行
expo start启动,从初始界面勾选要运行的测试组; - 所有测试源码位于 apps/test-suite/tests,测试组名通常与文件名一致;
- 新增测试 = 在
tests/下新建文件(或扩展既有分类),并把新文件注册进index.js的testModules列表,否则不会生效。
4.2 Jasmine 风格但全面支持 async
Test Suite 直接采用 Jasmine 语法,但其 t 对象是整套 Jasmine 接口的载体,且额外补齐了关键能力:patch 过的 Jasmine 支持 async 函数,异步异常也能被正确捕获与展示。以最小的测试模块 apps/test-suite/tests/Basic.js 为例,可以清晰看到其模块契约:
export const name = 'Basic';
export function test(t) {
t.describe('Basic', () => {
t.it('waits 0.5 seconds and passes', async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
t.expect(true).toBe(true);
});
t.it('2 + 2 is 4?', () => {
t.expect(2 + 2).toBe(4);
});
});
}
即每个测试模块必须导出:
name:测试名(字符串),与文件名同名;test(t):接收t对象并设置 Jasmine 的 suite/spec,t.describe、t.it、t.expect等即 Jasmine 全局函数的替身。
由于 SDK 大部分能力是异步的,apps/test-suite/README.md 特别强调以 apps/test-suite/tests/Contacts.js 为 async/await 范式范例。
4.3 模块注册的静态依赖约束
从源码看,模块注册并非简单的运行时循环,而是必须静态声明。在 apps/test-suite/TestModules.ts 中作者写明了原因:"Each file path must be statically present for the packager to pick them all up."——Metro 打包器必须能在静态分析阶段看到每个 require('./tests/...') 才能把测试文件打进去。
该文件里的 getTestModules() 对模块做了精细的分层:
- 先注册 JS 语法/运行时"健全性"测试(
Basic、JSDestructuring、JSAsync、JSPrivateMethods、JSNamedGroupsRegexes、JSNullishCoalescing、JSOptionalChaining等); - 再追加在所有平台都跑的 Expo 核心模块(
Asset、Constants、Font、ImagePicker等)与通用 API(Crypto、Clipboard、SQLite等); - 按平台分支:
LinearGradient排除 Android、Hermes仅 Android、Web 单独挂Contacts/GLView等; - 大量使用
optionalRequire(() => require(...))包裹非必要模块,实现"能加载就测、加载失败静默跳过"; - 依赖运行环境的判断再排除权限敏感项:
isRunningInExpoGo()决定是否启用Notifications,isDeviceFarm()决定是否启用 Location 弹窗类测试,注释中甚至记录了如"Audio 在 CI 下因异步拉取资源而 flaky""Google location service 弹窗"等实测坑位。
另外,apps/test-suite/ExponentTest.ts 显示测试宿主通过原生模块 NativeModules.ExponentTest 与宿主应用通信(上报 completed(results)、执行 action(action)、读取 isInCI);当宿主未注入该原生模块时,则回退为纯 JS 实现(isInCI 读 CI 环境变量),保证 test-suite 即便运行在纯 Expo Go/Web 环境也能降级工作。
4.4 一个提升调试效率的彩蛋
Test Suite 还支持 f.describe / f.it 聚焦(focus)到测试树的某个子树,其余测试被自动跳过。官方建议的用法非常实用:把聚焦测试写好 → 把本地 app 的链接发给他人 → 对方设备只跑这一组测试 → 你继续改测试、对方刷新即看到新结果。这对"在别人设备上定位纯 JS 级 Bug"的场景几乎是零成本复现工具。
五、Native Component List:Expo 预设能力的活目录
native-component-list 是"默认 Expo 预设里组件与 API 的 showcase",从 app.json 看它的应用描述是 "This demonstrates a bunch of the native components that you can use in React Native core and Expo.",支持 android / ios / web 三端,且以 expo-router 作为入口(package.json 的 "main": "expo-router/entry")。
它的依赖清单在 native-component-list/package.json 里相当壮观:几乎全部 Expo 官方模块都以 workspace:* 引用本地源码(如 expo-camera、expo-location、expo-sqlite、expo-video、expo-maps),外加 Skia、Reanimated、WebView 等大型第三方原生库。可以说,它是检验"Expo 全家桶默认预设能否在一套依赖树中共存"的试金石。
其 package.json 还内置了用于"打包模板再 prebuild"的脚本:
# 先 npm pack 出 bare-minimum 模板,再基于模板重新生成原生工程
pnpm prebuild
该脚本把 templates/expo-template-bare-minimum 打成 pack.tgz,再用 npx expo prebuild --clean --template ... 重建 android/ 与 ios/,保证了展示应用的"地基"始终与官方模板一致。其余命令 pnpm ios / pnpm android(对应 expo run:*)、pnpm web(expo start --web)、pnpm build:web(expo export -p web)则覆盖了 iOS、Android、Web 三端的启动与导出。
六、Expo Go:客户端 UI 所在的"应用"
expo-go 承载的是 Expo Go 客户端的 UI 层,其包名是 @expo/home(见 expo-go/package.json)。与前述三个"为了跑测试而存在"的应用不同,Expo Go 是用户真正会安装的宿主客户端,首页中加载远程/本地项目、展示项目列表、进入项目等界面逻辑都在这一层。
它的 autolinking.searchPaths 单独注入了 ../../react-native-lab/react-native/packages,并排除了自身包名 @expo/home,表明它运行在仓库自带的 react-native fork 之上;依赖侧同样以 workspace 方式引用了 expo-* 各模块与 @stripe/stripe-react-native 等支付能力。由于它是"客户端宿主",SDK 的 UNVERSIONED 开发版本能力验证(配合 dev client)也多经由它完成。
七、Sandbox:被 Git 忽略的本地试验场
sandbox 是一个刻意"不入库"的临时试验应用。从其 README 可知:
- 它被配置为 yarn workspaces 成员,可直接使用仓库本地工作副本的 expo-sdk 与全部 universal modules;
- 除已提交文件外,目录内其他一切内容均被
.gitignore忽略; - 想使用它,需本地新建
App.js——最简单的方式是从 blank 模板复制一份(对应仓库templates/expo-template-blank)。
这种"占位目录 + 全忽略"的做法非常适合验证本地未发布改动:改完模块源码立即在 sandbox 里肉眼验证,又不污染 Git 历史,完美契合 apps 目录"最小化、只为测试服务"的整体哲学。
八、从文档到代码的对照:五类应用如何协作
纵观整个 apps 目录,可以提炼出一条清晰的协作链路:
- 原生工程:
bare-expo与 Expo Go 提供完整 iOS/Android 原生构建目标; - 共享 JS 宿主:
native-component-list与test-suite是两大 JS 资产包,通过workspace:*被 bare-expo 直接依赖加载,无需重复维护原生工程; - 运行编排:
pnpm ios/android负责把原生工程与 Metro 拉起,pnpm open <ios|android> <模块>通过深链把 bare-expo 引导进 test-suite 的指定测试组; - 结果上报:test-suite 借助原生模块
ExponentTest把completed(results)回传给 CI 或宿主; - 回归防线:所有
expo-*模块的改动都会在这套矩阵中跑 Jasmine 套件,验证 Android/iOS/Web 的行为一致性。
九、何时才应该新增应用:写在最后的工程准则
apps/README.md 的核心告诫值得再强调一次:不要轻易新增应用。每一个新宿主都意味着新的维护面与新的依赖交织。若确有新验证需求,优先按文档给出的路线图走:在既有工程的**构建目标(build target)**上做文章,而不是再复制一个工程目录。仓库后续出现的 brownfield-tester、notification-tester、router-e2e 等专项工程,都是在"验证价值 > 维护成本"的权衡下才被接纳的特例。
对读者而言,这套"多宿主矩阵 + workspace 共享 + Jasmine 注入式测试 + 深链精测"的架构,本身就是一份高价值的 RN 仓库工程范式参考:无论你是在为 SDK 编写回归宿主,还是为企业内部组件库搭建验证沙箱,apps 目录的分层与取舍逻辑都值得直接复用。
延伸阅读
- 应用治理总纲:apps/README.md
- bare-expo 使用说明与命令面:apps/bare-expo/README.md、apps/bare-expo/package.json
- Test Suite 的 Jasmine 用法与注册机制:apps/test-suite/README.md、apps/test-suite/TestModules.ts、apps/test-suite/ExponentTest.ts、apps/test-suite/tests/Basic.js
- Native Component List 依赖与脚本:apps/native-component-list/package.json、apps/native-component-list/app.json
- Expo Go 客户端 UI:apps/expo-go/package.json
- 沙箱说明:apps/sandbox/README.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00