Expo Brownfield 测试应用实战:expo-app 如何构建与分发 Android/iOS Brownfield 工件
本篇以 Expo 仓库中的 brownfield 测试应用(apps/brownfield-tester/expo-app/README.md)为主体,讲解 expo-app 的用途、目录角色、运行方式,以及如何用 expo-brownfield 内置 CLI 将应用打包为可复用的 brownfield 工件(Android 的 Maven 制品、iOS 的 xcframework)。读完后,你将掌握 brownfield 模式的完整工作流:从 Metro 调试到工件构建、再到原生宿主应用消费工件的两种集成路径,并了解 CLI 各参数在源码中的实际定义。
brownfield-tester 的定位与目录角色
apps/brownfield-tester/expo-app 是 Expo 官方的 Expo Brownfield Test App,其职责有两个(见 README):
- 用于测试
expo-brownfield包本身,以及它与expo-router、expo-updates、expo-dev-menu等包的集成; - 作为
expo-brownfieldE2E 测试的基础应用(base app)。
结合上层目录的 README 可以看到,整个 brownfield-tester 由三部分构成,三者关系是理解本文档的骨架:
| 目录 | 角色 | 关键点 |
|---|---|---|
expo-app/ |
React Native / Expo 应用 | 提供 JavaScript 源码,同时是 brownfield 工件的构建源头 |
integrated/ |
原生 Android/iOS 应用 | 通过 Expo autolinking 直接集成 monorepo,构建期解析模块 |
isolated/ |
原生 Android/iOS 应用 | 消费预构建的 brownfield 工件(Maven / xcframeworks),构建期不依赖 monorepo |
其中 isolated/ 被明确标注为「推荐的分发方式」(the recommended distribution approach),即把 brownfield 能力以二进制工件形式交付给已有原生应用;而 integrated/ 则在其 README 中带有醒目警告:它把构建与项目根目录重定向到 ../expo-app,属于「不应复制」的临时性测试配置,仅用于 autolinking 场景的验证。
expo-app 的依赖与入口结构
从 package.json 可以看出,该应用的所有 Expo 相关依赖均以 workspace:* 方式引用 monorepo 内的本地包,核心依赖包括:
expo、expo-brownfield(brownfield 工具包)expo-router(路由)、expo-dev-menu(开发菜单)、expo-splash-screen、expo-constants、expo-device、expo-linking、expo-font、expo-image、expo-system-ui、expo-symbols、expo-web-browser、expo-glass-effect- 原生侧依赖
react-native0.87.0、react19.2.3,以及react-native-reanimated、react-native-screens、react-native-safe-area-context、react-native-gesture-handler等
入口文件 index.js 展示了 brownfield 测试的典型双注册模式:
import { registerRootComponent } from 'expo';
import { App } from 'expo-router/build/qualified-entry';
import { AppRegistry } from 'react-native';
import { CustomComponent } from './src/components';
// "main" component from expo-router
registerRootComponent(App);
// Additional custom component
AppRegistry.registerComponent('custom-component', () => CustomComponent);
这里把 expo-router 的 App 注册为主组件,同时额外注册了一个 custom-component。这一设计对应 E2E 测试中验证「brownfield 宿主能否独立拉起非主入口组件」的场景,src/app/ 下的路由(apis/navigation.tsx、apis/communication.tsx、apis/state.tsx、apis/dev-menu.tsx 等)则分别覆盖导航、原生通信、状态共享、开发菜单等功能面。
app.json 中的 plugins 配置声明了 expo-brownfield 插件及其关键参数:
[
"expo-brownfield",
{
"ios": {
"buildReactNativeFromSource": false
},
"android": {
"publishing": [
{ "type": "localMaven" }
]
}
}
]
ios.buildReactNativeFromSource: false:iOS 构建时不重新编译 React Native 源码;android.publishing声明了localMaven发布目标——这正是后文build:android --repo MavenLocal命令能够把工件发布到本地 Maven 仓库的前提。
用 Metro 调试(Android)
README 给出的调试约束是:
若要在调试模式下通过 Metro Bundler 使用本应用,请在应用目录运行
yarn start,并且使用Debug或All构建类型(build type)产出的工件——该能力目前仅支持 Android。
对应 package.json 中的 scripts.start(expo start)即可启动 Metro。这条限制说明 brownfield 工件中的 Debug 构建类型保留了开发服务器连接能力,而 Release 类型面向生产分发;Android 侧的 Debug/All 构建类型差异由工件中内嵌的启动逻辑控制(expo-brownfield 的 Android 插件会向 Gradle 工程注入相关配置,见 withProjectBuildGradlePlugin.ts 等插件实现)。
构建 brownfield 工件:CLI 完整命令与参数
README 中给出的最简构建命令是:
# Android
npx expo-brownfield build:android --repo MavenLocal --all --verbose
# iOS
npx expo-brownfield build:ios --release --verbose
npx expo-brownfield build:ios --debug --verbose
iOS 侧之所以要分两次执行 --release 与 --debug,是因为宿主原生应用在 Xcode 中通常按 Debug/Release 两种 configuration 链接 xcframework,需要各一份对应构建类型的工件。
结合 isolated/README.md 给出的前置步骤,完整构建流程应为(先 prebuild 生成原生工程,再构建发布):
cd apps/brownfield-tester/expo-app
# Android — 构建并发布到 MavenLocal
npx expo prebuild --clean -p android
npx expo-brownfield build:android --repo MavenLocal --all --verbose
# iOS — 构建 xcframeworks
npx expo prebuild --clean -p ios
npx expo-brownfield build:ios --release --verbose
npx expo-brownfield build:ios --debug --verbose
npx expo-brownfield 实际执行的是 packages/expo-brownfield 包中的 CLI(其 package.json 声明 "bin": "./bin/cli.js")。从 CLI 入口源码 cli/src/index.ts 可以看到各命令的完整参数集:
build:android(构建并发布 Android 工件)
| 参数 | 含义 |
|---|---|
-d, --debug |
构建 debug 变体 |
-r, --release |
构建 release 变体 |
-a, --all |
同时构建 debug 和 release 变体 |
--fused |
通过 AGP Fused Library 以每个变体一个 fat AAR 的形式发布 |
--verbose |
把 Gradle 全部输出转发到终端 |
-l, --library <name> |
指定 brownfield 库名 |
-t, --task <task...> |
指定要执行的发布任务,可传多个 |
--repo, --repository <repo...> |
指定发布目标仓库,可传多个(如 MavenLocal) |
--dry-run |
仅打印将执行的命令,不实际运行 |
build:ios(构建 iOS 工件)
| 参数 | 含义 |
|---|---|
-d, --debug |
构建 debug configuration |
-r, --release |
构建 release configuration |
--verbose |
转发全部 Xcode 构建输出 |
-s, --scheme <scheme> |
指定 iOS scheme 名 |
-x, --xcworkspace <path> |
指定 Xcode workspace 路径(.xcworkspace) |
-a, --artifacts <path> |
指定工件输出目录 |
-p, --package [name] |
将工件打包为 Swift Package,可附带包名 |
--host-provided <frameworks...> |
宿主 App 已提供的 framework 列表,构建时从工件中剥离(如 SDWebImage,SDWebImageWebPCoder),避免符号冲突 |
--dry-run |
仅打印命令 |
两个命令都支持 --dry-run,这是排查「CLI 到底会执行什么」时非常实用的手段。此外 CLI 还提供 tasks:android 命令,用于列出当前工程可用的发布任务与仓库,配合 -t/--task 与 --repo 使用。
工件如何被消费
- Android:工件发布到 Maven Local 后,宿主应用在 Gradle 仓库配置中声明
mavenLocal()即可解析(见 isolated/README.md),打开isolated/android/工程即可正常构建; - iOS:构建出 xcframeworks 后,可借助 E2E 脚本 add_xcframeworks.rb 把包含框架的本地 Swift Package 加入 Xcode 工程,也可以手动通过 Xcode 的 “Add Package Dependencies” 指向本地包路径。
integrated 与 isolated:两条验证路径
expo-app 生成的工件最终要回答一个问题:brownfield 能力放进一个独立的原生宿主后是否可用。仓库为此提供了两条验证路径:
- integrated(autolinking 路径):原生工程把项目根指向
../expo-app,模块在构建期从 monorepo 的node_modules解析。其 README 记录了初始化细节:Android 侧基于 Android Studio 2025.1.3 的 Empty Activity 项目创建,并按 Brownfield Integration 指南集成 Expo 模块;由于 React Native 面向 Java 17,需移除app/build.gradle.kts中默认的compileOptions,并移除settings.gradle.kts中dependencyResolutionManagement的repositoriesMode(因为react-native插件会自行配置 maven 仓库)。iOS 侧基于 Xcode 26 的 SwiftUI 项目创建,且因 Swift 6 支持尚不完整,需在项目设置中将 “Default Actor isolation” 设为 “nonisolated”。 - isolated(工件路径,推荐):原生工程完全自包含,Android 从
mavenLocal()解析 brownfield 库,iOS 通过本地 Swift Package 消费 xcframeworks,构建期不接触 monorepo。
这种「先工件、后消费」的结构,也解释了为什么 E2E 测试需要以 expo-app 为 base app:E2E 脚本(如 run-e2e-android.sh、run-e2e-ios.sh)先构建工件,再将其安装到原生宿主应用中执行 Maestro 场景(e2e/maestro 下的 navigation.yml、state.yml、dev-menu.yml、communication.yml 等流程),端到端验证 expo-app 中注册的导航、状态、通信与开发菜单功能在 brownfield 环境下的行为。
小结
apps/brownfield-tester/expo-app 是验证 expo-brownfield 的核心试验场:它既是 expo-router / expo-updates / expo-dev-menu 集成的测试宿主,也是 E2E 流水线中工件的构建源头。掌握它的两条主线命令即可复现完整 brownfield 工作流——Android 用 npx expo-brownfield build:android --repo MavenLocal --all --verbose 发布 Maven 工件,iOS 分别用 build:ios --release/--debug --verbose 产出 xcframeworks;调试期则依赖 Metro 与 Android 的 Debug/All 构建类型。所有 CLI 参数均可在 packages/expo-brownfield/cli/src/index.ts 中逐条核对,--dry-run 与 tasks:android 是调试构建流程时的可靠帮手。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00