首页
/ Expo 主仓库 apps 目录全解析:测试用应用矩阵的定位、职责与运行方式

Expo 主仓库 apps 目录全解析:测试用应用矩阵的定位、职责与运行方式

2026-09-07 13:22:06作者:苗圣禹Peter

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-testerexpo-goeas-expo-go)、expo-workflow-testingminimal-testernative-testsnotification-testerobserve-testerrouter-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.mdbare-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.jsonexpo.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.jstestModules 列表,否则不会生效。

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.describet.itt.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 语法/运行时"健全性"测试(BasicJSDestructuringJSAsyncJSPrivateMethodsJSNamedGroupsRegexesJSNullishCoalescingJSOptionalChaining 等);
  • 再追加在所有平台都跑的 Expo 核心模块(AssetConstantsFontImagePicker 等)与通用 API(CryptoClipboardSQLite 等);
  • 按平台分支:LinearGradient 排除 Android、Hermes 仅 Android、Web 单独挂 Contacts/GLView 等;
  • 大量使用 optionalRequire(() => require(...)) 包裹非必要模块,实现"能加载就测、加载失败静默跳过";
  • 依赖运行环境的判断再排除权限敏感项:isRunningInExpoGo() 决定是否启用 NotificationsisDeviceFarm() 决定是否启用 Location 弹窗类测试,注释中甚至记录了如"Audio 在 CI 下因异步拉取资源而 flaky""Google location service 弹窗"等实测坑位。

另外,apps/test-suite/ExponentTest.ts 显示测试宿主通过原生模块 NativeModules.ExponentTest 与宿主应用通信(上报 completed(results)、执行 action(action)、读取 isInCI);当宿主未注入该原生模块时,则回退为纯 JS 实现(isInCICI 环境变量),保证 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-cameraexpo-locationexpo-sqliteexpo-videoexpo-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 webexpo start --web)、pnpm build:webexpo 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 目录,可以提炼出一条清晰的协作链路:

  1. 原生工程bare-expo 与 Expo Go 提供完整 iOS/Android 原生构建目标;
  2. 共享 JS 宿主native-component-listtest-suite 是两大 JS 资产包,通过 workspace:* 被 bare-expo 直接依赖加载,无需重复维护原生工程;
  3. 运行编排pnpm ios/android 负责把原生工程与 Metro 拉起,pnpm open <ios|android> <模块> 通过深链把 bare-expo 引导进 test-suite 的指定测试组;
  4. 结果上报:test-suite 借助原生模块 ExponentTestcompleted(results) 回传给 CI 或宿主;
  5. 回归防线:所有 expo-* 模块的改动都会在这套矩阵中跑 Jasmine 套件,验证 Android/iOS/Web 的行为一致性。

九、何时才应该新增应用:写在最后的工程准则

apps/README.md 的核心告诫值得再强调一次:不要轻易新增应用。每一个新宿主都意味着新的维护面与新的依赖交织。若确有新验证需求,优先按文档给出的路线图走:在既有工程的**构建目标(build target)**上做文章,而不是再复制一个工程目录。仓库后续出现的 brownfield-testernotification-testerrouter-e2e 等专项工程,都是在"验证价值 > 维护成本"的权衡下才被接纳的特例。

对读者而言,这套"多宿主矩阵 + workspace 共享 + Jasmine 注入式测试 + 深链精测"的架构,本身就是一份高价值的 RN 仓库工程范式参考:无论你是在为 SDK 编写回归宿主,还是为企业内部组件库搭建验证沙箱,apps 目录的分层与取舍逻辑都值得直接复用。

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525