在 Expo Monorepo 中做 Brownfield 集成测试:brownfield-tester 应用的结构、产物构建与两种消费模式
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
本篇技术指南以 Expo 官方仓库中的 brownfield-tester 示例应用 为核心,讲解如何用一个完整可运行的示例验证"把 Expo 集成进既有原生 App(即 brownfield 改造)"的工程路径:既包括产出可复用原生产物的 Expo 应用端,也包括分别依赖源码自动链接(integrated)与依赖预编译产物(isolated)的两类宿主原生工程。读完本文,你将掌握expo-brownfieldCLI 的产物构建命令、app.json中 config plugin 的关键参数,以及两种分发/消费模式各自的适用场景与工程前提。
什么是 Brownfield Tester,它在仓库中扮演什么角色
在 Expo 语境下,"brownfield" 指的是已有的、非 Expo 创建的原生 Android/iOS 应用,需要以增量方式接入 Expo 能力(Expo Modules、expo-router、expo-updates 等),而不是整体迁移到 Expo 托管工作流。Expo 为此提供了expo-brownfield工具包与相关 API。
brownfield-tester 就是这个 monorepo 中专门用于验证与回归测试 brownfield 集成的示例工程集。它主要有两个职责:
- 作为
expo-brownfield包及其与expo-router、expo-updates、expo-dev-menu等包集成效果的测试载体; - 作为
expo-brownfieldE2E 测试的基础应用(base app),供自动化测试反复构建与装载。
因此它不是一个玩具 Demo,而是一套"能真正跑通产物构建 → 宿主集成"的最小可复现基线。
三目录总体结构:一份 JS 源、两种原生宿主
仓库根目录为apps/brownfield-tester,内部按职责划分三个子工程:
| 目录 | 定位 | 与 monorepo 的关系 |
|---|---|---|
| expo-app | React Native / Expo 应用,既是JavaScript 源码所在,也是brownfield 产物的构建器 | 直接以workspace:*依赖 monorepo 中的expo、expo-brownfield等包 |
| integrated | 原生 Android/iOS App,通过 Expo autolinking 直接集成 monorepo,构建时把 project root 指向../expo-app | 构建期从 monorepo 的node_modules解析并链接 React Native 模块 |
| isolated | 独立、自包含的原生 App,消费预构建好的 brownfield 产物(Android 走 Maven Local,iOS 走 xcframework/Swift Package) | 构建期完全不依赖 monorepo,是推荐的分发方式 |
一句话概括:expo-app负责"把 Expo 应用打成可复用产物",而integrated与isolated是"消费这些产物的两种宿主 App 形态"。
expo-app:JavaScript 源与 brownfield 产物构建器
依赖与运行脚本
expo-app/package.json 显示其运行脚本与常规 Expo 应用一致(expo start、expo run:android、expo run:ios),依赖则全部指向 monorepo workspace:
expo、expo-brownfield、expo-router、expo-dev-menu、expo-updates(见其README提到的测试目标)、expo-image、expo-web-browser、expo-splash-screen、expo-status-bar、expo-constants等均为workspace:*;- 底层 React Native 为 0.87.0、React 19.2.3,并携带
react-native-reanimated/react-native-worklets等典型 Expo 生态依赖。
这意味着 expo-app 直接编译并测试的是 monorepo 中尚未发版的expo-brownfield源码,是包开发期最直接的验证环境。
Metro 调试模式
按 expo-app/README.md 的说明,若要在 Debug 下配合 Metro Bundler 使用:
# 在 expo-app 目录内 yarn start并且宿主 App 必须使用Debug或All构建类型的产物(该限制当前仅 Android 支持)。这是原生宿主在开发期加载 JS bundle 时最常见的坑:产物构建类型与 Metro 启动状态不一致,会导致加载不到 bundle。
入口注册:除了主入口,还有一个可独立装载的原生组件
expo-app/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,又额外通过AppRegistry.registerComponent注册了一个名为custom-component的独立组件。这对应真实 brownfield 场景中"宿主原生页面想嵌入一块 Expo 渲染的 UI"的需求——原生端可以选择把整个路由应用挂到全屏容器,也可以把这块自定义组件嵌入既有原生界面的任意位置。
插件化配置与 UI 测试面
expo-app/app.json 中配置了expo-router、expo-splash-screen以及最关键的自定义插件expo-brownfield:
{ "plugins": [ "expo-router", ["expo-splash-screen", { "backgroundColor": "#208AEF", "image": "./assets/images/splash-icon.png", "imageWidth": 76 }], [ "expo-brownfield", { "ios": { "buildReactNativeFromSource": false }, "android": { "publishing": [{ "type": "localMaven" }] } } ] ] }从该配置可见两个重要的产物策略开关:iOS 侧关闭了"从源码构建 React Native",Android 侧指定产物以localMaven(本地 Maven 仓库)方式发布。UI 结构则由 expo-app/src/app/_layout.tsx 中的Tabs路由承载,包含Home与APIs两个页签,其中apis组下按测试目标拆分了 communication(通信)、dev-menu(开发者菜单)、native-modules(原生模块)、navigation(导航)、state(状态)等入口——从目录设计可以推断,它们分别覆盖 brownfield 场景下宿主与 Expo 侧通信、开发菜单集成、自定义原生模块桥接、导航与状态同步等核心回归面。
构建可复用 brownfield 产物:expo-brownfield 内置 CLI
expo-app/README.md 明确指出,把 expo-app 打成可复用 brownfield 产物最直接的方式是使用expo-brownfield的内置 CLI:
# Android —— 构建并把产物发布到 Maven Local npx expo-brownfield build:android --repo MavenLocal --all --verbose # iOS —— 构建 xcframework 产物 npx expo-brownfield build:ios --release --verbose npx expo-brownfield build:ios --debug --verbose命令要点解读:
--repo MavenLocal:把 Android 的 brownfield 库产物发布到本地 Maven 仓库(mavenLocal()),供独立宿主工程解析;--all:一次性构建全部所需的 build type(可对应 Debug/Release 等变体);--verbose:输出详细日志,便于在集成失败时定位问题;- iOS 的
--release/--debug是构建 flavor,因为产物以 xcframework/Swift Package 形式输出,而 Swift Package Manager 不支持按构建配置重载.binaryTarget,因此每个 flavor 需要单独构建一次、分发两个包(详见下文)。
消费模式一:Integrated —— 基于 Expo autolinking 的直接集成
integrated/README.md 说明:integrated/下的原生工程直接与 Expo monorepo 集成,其构建/project root 被指向../expo-app,从而在构建期从 monorepo 的node_modules解析并链接 React Native 模块。
需要特别强调的是,该 README 自带一段醒目的> [!WARNING]:
这个应用把构建与 project root 重定向到了
../expo-app,这不是合法的工程配置,需要重构。请不要把这种写法复制到其他测试或 E2E 工程中。
也就是说integrated/的存在意义是验证 autolinking 链路本身能否工作,它的工程结构属于"刻意为之的非法形态",仅限仓库内部测试使用。这提醒我们:真实项目不应通过改 project root 的方式接 monorepo。
Android 集成要点(integrated)
从源码结构看,Android 端工程遵循官方 "Brownfield Integration" 指南的接入方式:宿主工程位于 integrated/android,通过 Android Studio 2025.1.3 的空 Activity 工程起步,最终手工接入 Expo。关键的实施后置步骤包括:
- 由于 React Native 面向Java 17编译,需移除
app/build.gradle.kts中默认的compileOptions; - 需移除
settings.gradle.kts中dependencyResolutionManagement的repositoriesMode,因为react-native插件会自行配置 maven 仓库,二者叠加会冲突。
工程中可以看到承载 Expo 渲染的原生活动类(如ExpoActivity.kt)以及入口MainActivity.kt、应用级MyApplication.kt,即"原生壳 + Expo 承载视图"的典型布局。
iOS 集成要点(integrated)
iOS 端工程位于 integrated/ios,由 Xcode 26 的 SwiftUI 工程起步并同样遵循官方指南接入。由于当前Swift 6 尚未被完全支持,需要在工程设置里把 "Default Actor isolation" 设为nonisolated。源码中包含ExpoView.swift、ExpoViewController.swift等承载层文件,对应在 SwiftUI/UIKit 原生界面中嵌入 Expo 内容的接入方式。
消费模式二:Isolated —— 消费预构建产物(推荐分发方式)
isolated/README.md 描述的isolated/是与 monorepo完全解耦的独立原生 App:
- Android:从Maven Local仓库解析预发布的 brownfield 库;
- iOS:通过Swift Package引入预构建的 xcframework;
- 构建期不使用 Expo autolinking,也不依赖 monorepo 的
node_modules。
这是官方推荐的、面向既有原生 App 分发 brownfield 集成能力的方式——宿主工程可以独立于 Expo 仓库构建、发布与演进。
先决条件:先生成产物
构建 isolated 宿主前,必须先进入expo-app生成产物,isolated/README.md 给出的完整流程为:
cd ../expo-app # Android —— 先 prebuild 生成原生目标,再构建并发布到 MavenLocal npx expo prebuild --clean -p android npx expo-brownfield build:android --repo MavenLocal --all --verbose # iOS —— 先 prebuild 生成原生目标,再构建 xcframework npx expo prebuild --clean -p ios npx expo-brownfield build:ios --release --verbose npx expo-brownfield build:ios --debug --verbose这里npx expo prebuild --clean -p <platform>的作用是让 config plugin(包括expo-brownfield)生效,先生成出供产物构建使用的完整原生工程。产物细节可继续参考 expo-app/README.md。
Android:直接打开、正常构建
产物发布到 Maven Local 后,用 Android Studio 打开 isolated/android 正常构建即可,宿主 App 会从mavenLocal()解析 brownfield 库。从源码结构看,Android 宿主包含MainActivity.kt、ReactNativeActivity.kt以及演示自定义原生模块的BrownfieldTestModule.kt/BrownfieldTestPackage.kt,覆盖"宿主 → 原生模块 → React Native 侧"的往返调用。
iOS:通过本地 Swift Package 引入
iOS 产物(xcframeworks)构建完成后,可通过 E2E 脚本目录中的add_xcframeworks.rb脚本把包含这些 framework 的 Swift Package 加入 Xcode 工程,也可以在 Xcode 里通过Add Package Dependencies手动添加本地 package 路径。
从 expo-brownfield 包源码看 CLI 与插件能力
产物构建与宿主集成背后的实现来自 expo-brownfield,它提供了一套完整的工具链,仓库内可见其cli/(CLI 实现)、android/、ios/、gradle-plugins/、plugin/(config plugin)、scripts/与e2e/等模块,结构上印证了"配置插件 + 双端产物构建 + 宿主集成脚本"的分层设计。
Config plugin 与核心选项
在 app.json/app.config.js 中加入插件即可,expo prebuild会根据插件为 brownfield 场景生成额外的原生 target:
{ "plugins": ["expo-brownfield"] }需要自定义时可传入选项,例如指定 iOS target 名称或 Android 库名:
{ "plugins": [ [ "expo-brownfield", { "ios": { "targetName": "MyBrownfield" }, "android": { "library": "mybrownfield" } } ] ] }不加额外选项时均使用默认值。brownfield-tester 工程本身(见 expo-app/app.json)就是"通过插件选项控制产物行为"的真实范例。
iOS 预编译模块与 SPM 共享依赖
expo-brownfield的 iOS 产物构建支持配合expo-build-properties的ios.usePrecompiledModules,让pod install直接下载各 Expo 模块的预编译.xcframework而非源码编译;随后build:ios会自动识别ios/Pods/下的 xcframework,把它们连同 brownfield framework、React、Hermes 与ReactNativeDependencies一起打包进输出的 Swift Package。
由于 Swift Package Manager 不支持对.binaryTarget(path:)做按构建配置的重载,每个输出包被固定为其构建时的 flavor:--debug与--release需各构建一次,随后把两个包并排分发。
此外,多个 Expo 模块会链接共享的 SPM 依赖(如expo-image→SDWebImage、libavif)。为避免宿主运行时出现Library not loaded: @rpath/...崩溃,build:ios会依次在 Pod 内部、发布到 npm 包的prebuilds目录、或共享的.spm-deps/缓存中查找依赖;找不到时直接以可操作的错误信息终止构建,而不是产出一个运行时必崩的 Swift Package。
同一宿主内嵌多个 brownfield framework(iOS)
当一个宿主 iOS App 需要同时共存多个 Expo brownfield framework 时,可开启ios.multipleFrameworks: true。每个 brownfield framework 会获得唯一的 Swift module 名,其注册的所有 ObjC 类通过@objc(...)加<TargetName>_前缀,pod 依赖图里的 ObjC 符号也在编译期被统一重写前缀,从而避免两个 framework 携带重叠 pod 时的链接期重复符号错误。该符号改写由pod install阶段的小型 Ruby shim 与随包分发的 Node worker 完成,并在依赖图变化时通过校验和跳过重复执行。
如何在仓库中实际跑起来
结合三份 README,一个完整的本地验证路径如下:
- 开发/调试 JS 侧:进入 expo-app,执行
yarn start启动 Metro,宿主 App 需选用Debug/All构建产物(当前仅 Android 支持)。 - 生成产物:在 expo-app 中执行
npx expo prebuild --clean -p android(或-p ios),随后运行对应的npx expo-brownfield build:android --repo MavenLocal --all --verbose或build:ios --debug/--release。 - 以 isolated 方式消费(推荐分发路径):Android 直接打开 isolated/android 用 Android Studio 构建(从
mavenLocal()解析);iOS 通过add_xcframeworks.rb脚本或 Xcode 的 Add Package Dependencies 添加本地 Swift Package。 - 以 integrated 方式回归 autolinking:参考 integrated/ 双端工程,但须注意其 project root 重定向属于仓库内刻意保留的非法形态,仅供内部链路验证,不应在真实工程中复制。
两种消费模式如何选型
| 维度 | integrated(autolinking) | isolated(预构建产物) |
|---|---|---|
| 模块来源 | 构建期从 monoreponode_modules链接 | 构建期从mavenLocal()/ 本地 Swift Package 解析 |
| 依赖关系 | 强依赖 monorepo 与源码 | 完全自包含,独立构建 |
| 产物形态 | 无独立可分发产物 | Android Maven 库、iOS xcframework/Swift Package |
| 用途 | 回归验证 autolinking 链路(仓库内部) | 面向既有原生 App 的推荐分发方式 |
| 注意事项 | project root 指向外部工程是非法形态 | 需按 flavor(debug/release)分别构建并分发 |
小结
brownfield-tester 用一套紧凑的工程组织,把 brownfield 集成的两端——Expo 产物构建端与原生宿主消费端——完整地呈现在同一 monorepo 中:expo-app既是测试面也是构建器,integrated/验证最底层的 autolinking 可行性,isolated/则给出业界真正推荐的预构建产物分发范式。若你要在自己的既有原生工程中接入 Expo,建议对照isolated/的产物构建流程与宿主接入步骤,并结合 expo-brownfield 中的插件选项与 iOS 多 framework、SPM 依赖约束等细节,即可复现一条从 JS 源码到宿主 App 的完整集成链路。
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考