☰
在 Expo Monorepo 中做 Brownfield 集成测试:brownfield-tester 应用的结构、产物构建与两种消费模式
2026/11/3 23:11:56 网站建设 项目流程

在 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 集成的示例工程集。它主要有两个职责:

  1. 作为expo-brownfield包及其与expo-router、expo-updates、expo-dev-menu等包集成效果的测试载体;
  2. 作为expo-brownfieldE2E 测试的基础应用(base app),供自动化测试反复构建与装载。

因此它不是一个玩具 Demo,而是一套"能真正跑通产物构建 → 宿主集成"的最小可复现基线。

三目录总体结构:一份 JS 源、两种原生宿主

仓库根目录为apps/brownfield-tester,内部按职责划分三个子工程:

目录定位与 monorepo 的关系
expo-appReact 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,一个完整的本地验证路径如下:

  1. 开发/调试 JS 侧:进入 expo-app,执行yarn start启动 Metro,宿主 App 需选用Debug/All构建产物(当前仅 Android 支持)。
  2. 生成产物:在 expo-app 中执行npx expo prebuild --clean -p android(或-p ios),随后运行对应的npx expo-brownfield build:android --repo MavenLocal --all --verbose或build:ios --debug/--release。
  3. 以 isolated 方式消费(推荐分发路径):Android 直接打开 isolated/android 用 Android Studio 构建(从mavenLocal()解析);iOS 通过add_xcframeworks.rb脚本或 Xcode 的 Add Package Dependencies 添加本地 Swift Package。
  4. 以 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询