Expo 棕地改造:如何在不重写的前提下,把 Expo 接入现有的原生应用

把 Expo 加到现有的原生 iOS/Android 应用里,不用重写。了解 Expo SDK 55 全新的隔离式 brownfield 工作流。

中文
复制
Expo brownfield: How to add Expo to your existing native app without a rewrite

假设你已有一个上线的原生应用。它是多年平台特定决策的产物,你并不打算把它整个替换掉,也不打算一次性替换。

与此同时,你可能想在这个应用里用 React Native 构建一个新功能(比如跨平台共享逻辑),而不必为了做到这一点重写应用的其余部分。

这就是我们所说的 brownfield:把 React Native 和 Expo 加进一个现有原生应用,而这个应用的主入口并不是 React Native(完整定义见此)。

问题不在于能不能加 React Native,而在于怎么加才能做到增量、自包含、不打扰团队其他成员,而不是把它变成一次全有或全无的架构决策。

本文梳理 Expo 的 brownfield 方案如何演变,以及今天能做到什么程度。在 SDK 55 中,我们引入了隔离式 brownfield 工作流,让你可以把 Expo 屏幕和组件集成进一个非 React Native 的原生应用!做法是把一个预编译好的 Expo 应用作为原生依赖嵌入,体验上更接近添加一个库,而不是引入一套新框架。

Expo 在现有原生应用中的位置

团队考虑把 React Native 引入现有原生应用,原因各不相同,例如:

  • 如果目标是渐进采用,可以先在应用的一个部分使用 Expo,而不必一次性押上整个代码库,同时保留日后扩大使用范围的可能。

  • 有选择地用 React Native 构建产品的某个特定部分,比如一个自包含的功能区域或子应用(例如 Facebook 应用里的 Marketplace),其余部分保持不变。

  • 出于业务需要而非技术需要,比如收购之后把一个已有的 React Native 应用整合进原生产品。

这些场景的要求是一样的:React Native 要能嵌进一个已经存在的应用,既不需要痛苦的改写,也不需要让现有应用做出别扭的迁就,而不是成为新的入口或取代现有结构。

Expo 的 brownfield 方案,目标是让团队能在尽量不打扰现有代码库、也不打扰维护它的团队的前提下用上 React Native。Expo 提供了一套明确的 API 和模式,把 React Native 以受支持、可维护的方式嵌入原生应用。这些集成方式可以和 Expo 的工具链、库以及 SDK 升级流程配合使用。

这对整个工程团队也有价值。很多公司里已经有工程师能熟练写 React,但对原生平台、或者对原生开发本身并不熟悉。嵌入 Expo 就划出了一块清晰的界面,这部分经验可以直接用上,而不必去改动原生应用的构建方式或归属关系。

这里的价值不在于重新定义应用的架构,而在于让 Expo 可以被有选择地引入,同时应用的其他部分以及背后的决策都保持不变。

Expo 中的 brownfield 方案

Expo 目前记录了两种把 React Native 加入现有原生应用的方式。

集成式方案把 React Native 和 Expo 直接装进原生应用。但把 React Native 嵌入原生应用,比引入一个普通库要复杂:它会带来第二套运行时、构建系统和开发环境,整个团队都得去适应(详见《如何用集成式方案把 Expo 加入原生应用》)。

作为替代,我们在 SDK 55 中引入了隔离式方案:把你的 React Native 代码打包成一个原生库(Android 上是 AAR,iOS 上是 XCFramework),像其他依赖一样集成进原生应用。原生开发者不需要搭建 Node.js 环境,也不用处理 React Native 的构建依赖——他们只消费预先构建好的产物,复杂度被限制在 React Native 团队内部,组织里的其他人完全感知不到。

隔离式方案的实际形态

采用隔离式方案时,Expo 应用会提前构建好,以原生二进制产物的形式分发。从 Expo 的角度看,这只是一个普通的 Expo 项目:你用常规的 Expo 和 React Native 工具开发页面和组件,开发期间用 Expo CLI 运行项目,等想把改动集成进宿主应用时,再产出编译好的 framework 或 AAR。

隔离方案通过新的 expo-brownfield 包实现:

# Install the package
npx expo install expo-brownfield
# Build the iOS XCFramework
npx expo-brownfield build:ios
# Build the Android AAR
npx expo-brownfield build:android

底层依赖的是 Continuous Native Generation (CNG):用于构建产物的原生 iOS 和 Android 工程由应用配置和 Expo 模块生成,而非手工维护。expo-brownfield 配置插件在此基础上扩展了这一流程,添加生成原生 framework 所需的目标。嵌入的 app 不需要你管理 Xcode 工程或 Gradle 文件——这些都由工具链在构建步骤中处理。

构建完成后,产物因平台而异。

在 iOS 上,这会在项目中生成一个 artifacts 目录,里面是 app 编译后的原生 framework 产物。

├── app/
├── artifacts/
   ├── hermesvm.xcframework/
   └── expohelloworldbrownfield.xcframework/
├── assets/
├── ios/
├── node_modules/
├── .gitignore
└── app.json

artifacts 目录包含两个 .xcframework

  • 一个用于 JavaScript 引擎及运行时依赖

  • 一个用于编译后的 Expo app 本身

把生成的 .xcframework 拖进你的 Xcode 工程即可。它们会和其他依赖一起出现并链接进 app,嵌入的 Expo app 便能从原生代码中初始化并渲染。

将 Expo 添加到原生应用

接下来,原生应用会显式初始化 React Native 宿主,并在合适的位置渲染内嵌的 React Native 视图。

在 Android 上,产物会打包成一个 .aar,但分发方式略有不同。构建过程不会把文件复制到你的原生项目里,而是把产物发布到本地 Maven 目录(通常是 ~/.m2)。

这意味着原生应用必须能够从产物所发布到的 Maven 仓库解析依赖,比如 mavenLocal() 或某个远程 Maven 仓库。

// settings.gradle or build.gradle (depending on your setup)
dependencyResolutionManagement {
  repositories {
    mavenLocal()
    google()
    mavenCentral()
  }
}

配置好之后,就可以在将要承载它的模块中把内嵌应用作为普通依赖添加进来:

dependencies {
  implementation("com.example.helloworld:brownfield:1.0.0")
}

一个实际细节:宿主 Activity 需要使用一个能提供 React Native 所需属性的主题。基于 AppCompat 的主题在这里是稳妥的默认选择。测试时,任意 AppCompat 主题(例如 Theme.AppCompat.Light.NoActionBar)都可以正常工作。

<activity
  android:name=".MainActivity"
  android:exported="true"
  android:label="@string/app_name"
- android:theme="@style/Theme.Helloworld">
+ android:theme="@style/Theme.AppCompat.Light.NoActionBar">
  <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      
      <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>

构建或运行宿主应用时,确保 build variant 与 Expo 应用的打包方式一致。例如,如果你用 --release 构建了 .aar,那么运行宿主应用时也要使用 release variant(比如通过 Android Studio 的 “Active Build Variant”)。

把 expo 加进原生应用——隔离式方案

嵌入的 Expo 应用可以从原生 Android 代码中显式实例化。

原生应用与嵌入式应用之间的通信

隔离方案内置了一套通信 API,用于在原生宿主与嵌入式 Expo 应用之间传递消息。

这套 API 提供了一条结构化的双向通道来交换事件和数据,无需直接访问嵌入式应用的内部模块。跨越隔离边界协调导航、状态变更以及原生驱动的事件,主要靠的就是它。

从概念上讲,这套 API 类似于 Web 的 postMessage 模型:消息通过一个消息层传递,而不是直接访问内部对象或模块。

例如,下面是从外层应用向 React Native 发送消息的写法:

// iOS
import ExpoBrownfield

BrownfieldMessaging.sendMessage([
    "type": "MyIOSMessage",
    "timestamp": Date().timeIntervalSince1970,
    "data": [
        "platform": "ios"
    ]
])

在 React Native 中接收来自外层应用的消息:

import * as Brownfield, { type MessageEvent } from 'expo-brownfield';
import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      console.log('Received message:', event);
    };

    Brownfield.addMessageListener(handleMessage);

    return () => {
      Brownfield.removeMessageListener(handleMessage);
    };
  }, []);
}

限制与取舍

Brownfield 支持涉及不少实际问题,下面逐一说明。

隔离方案的限制

  • 一个原生应用只能包含一个嵌入式应用。该应用以单个 XCFramework(iOS 上)或 AAR(Android 上)的形式打包分发。之所以有这个限制,是因为每个嵌入式应用都自带一份 React Native 运行时。在同一个原生应用里放入多个这样的框架,构建时会出现类名冲突。支持嵌入多个隔离应用已在计划中,但目前尚不可用。

  • 多个逻辑体验必须共用一个嵌入式运行时。嵌入式应用可以加载多个 JavaScript bundle,但它们都运行在同一个打包好的 Expo 应用内,必须共用同一套 React Native 运行时和原生依赖。

  • 嵌入式应用也被有意设计为自包含的。嵌入式应用之外的代码无法直接访问 Expo 模块或其他内部实现细节。原生应用与嵌入式应用之间的交互必须通过嵌入式应用自身暴露的明确接口进行,比如消息 API

  • 构建时也存在实际的取舍。构建 framework 或 AAR 可能很慢,而且在这种情况下无法复用预编译的 React Native 二进制文件。在 debug 和 release 配置之间切换需要重新生成嵌入的产物,这会在开发过程中增加摩擦。

  • 生成的 XCFramework 或 AAR 必须作为二进制产物存储和分发。由于这些文件可能很大,通常不会直接提交到 Git。虽然可以使用 Git LFS,但许多团队会将这些产物发布到产物仓库(例如 Artifactory 或其他兼容 Maven 的 registry),以实现带版本的分发。

库兼容性方面的考量

在 brownfield 方案下,一些库可能无法按预期工作,或者文档有限。

这包括某些 Expo 库。例如,expo-updates 包在集成式和隔离式 brownfield 方案中均可使用,但它目前假设每个原生应用只有一个 Expo 项目、一个更新 URL。

这一限制对两种方案都适用,原因在于 Expo 项目 ID 在原生应用 bundle 中的存储方式。

许多社区库,无论是 Expo 的还是第三方的,也可能假设 React Native 是应用的入口点。那些期望 React Native 视图作为根窗口、或 React Native activity 作为主 activity 的库,在 React Native 通过 fragment 或 view controller 嵌入时,可能需要额外配置,或者根本无法兼容。

总结

Expo 的 brownfield 方案现在包含两种:集成式方案,即把 React Native 和 Expo 直接安装到原生项目中并与应用一起构建;以及隔离式方案,即把预编译的 Expo 应用作为依赖嵌入原生应用。

这两种方案目前服务于不同的需求。集成式方案提供更大的灵活性和与原生应用的深度集成,而隔离式方案则通过让 Expo 应用保持自包含来降低接入摩擦。

Expo 在棕地场景下的工具方向,是让这种集成越来越熟悉、越来越可预期,大部分复杂度都由工具来处理。

棕地支持不把 React Native 当成一个全有或全无的选择,而是让 Expo 可以逐步引入现有的原生应用:从小处着手,在合适的地方再扩展。

随着 Expo 越来越容易接入团队已有的应用,我们很期待看到他们会做出什么。

来源: Expo Blog← 返回首页