Expo 预编译体系:将第三方 React Native 包转换为预构建 XCFramework 的完整实操流程
2026/9/10 15:33:05 网站建设 项目流程

Expo 预编译体系:将第三方 React Native 包转换为预构建 XCFramework 的完整实操流程

【免费下载链接】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 仓库内的技能文档 convert-external-package.md,讲解如何把一个外部 React Native 包接入 Expo iOS precompile 系统、以 Swift Package Manager 预构建为 XCFramework。读完后你将掌握五步转换流程(分析包结构、修复头文件导入、包裹 podspec、编写 spm.config.json、生成 patch),以及try_link_with_prebuilt_xcframework在 CocoaPods 集成层的真实实现逻辑,并能在仓库中找到全部可对照的参考配置。

一、这套流程在整个预编译体系中的位置

et prebuild是 Expo 仓库内的 iOS 预编译工具链,其职责是从spm.config.json定义出发构建 XCFramework 产物,并支持可选的校验与签名。核心工具链位于 tools/src/prebuilds 目录,整体构建流程为:

  1. 发现并校验目标包与构建选项;
  2. 解析版本与本地 tarball 输入;
  3. 按 flavor(Debug/Release)解析产物缓存;
  4. 对每个package/product@flavor单元:生成源码与Package.swift→ 构建 framework → 合成 XCFramework → 校验产物;
  5. 打印摘要,必要时写错误日志。

这一流程在 tools/src/prebuilds/README.md 中有完整定义。外部包(第三方 React Native 包)与 Expo 自家包共用同一条构建管线:ExternalPackage类实现了SPMPackageSource接口,与 Expo 的Package类地位对等(见 tools/src/prebuilds/ExternalPackage.ts)。因此“把外部包转换为可预构建形态”本质上就是为它补全三份输入:

  • 一份spm.config.json配置(放在packages/expo-modules-autolinking/external-configs/ios/<PACKAGE>/spm.config.json);
  • 对包 podspec 的修改(包源码编译逻辑被try_link_with_prebuilt_xcframework条件包裹);
  • 对包源码中头文件导入问题的修复。

而 convert-external-package.md 这份文档正是规定“这三份输入如何产出”的标准化作业流程。当前仓库中已落地 7 个外部包配置,可作为对照样本:@react-native-async-storage/async-storage@shopify/react-native-skiareact-native-reanimatedreact-native-safe-area-contextreact-native-screensreact-native-svgreact-native-worklets(完整清单见 external-configs/ios/README.md)。

二、任务规则:允许做什么,禁止做什么

文档为转换流程设定了明确的边界约束,执行时必须遵守:

  • 只能创建/编辑三类文件packages/expo-modules-autolinking/external-configs/ios/<PACKAGE>/spm.config.jsonnode_modules/中的 podspec、以及node_modules/中需要修复头文件的源文件;
  • 永远不要手工编写 patch 文件。所有对node_modules/的修改必须通过npx patch-package <PACKAGE>生成补丁;
  • 永远不要构建或测试产物。不要运行et prebuild-packagespod install
  • 输出保持最小化——只输出简短的 CLI 风格状态信息;
  • npx patch-package成功运行后立即停止,那是最后一步。

这些规则的意图很清晰:转换流程本身只负责“准备好可预构建的输入”,真正构建、验证交给统一的et prebuild管线(其产物输出到packages/precompile/.build/<package-name>/output/<flavor>/xcframeworks/,依赖缓存在packages/precompile/.cache/下),避免转换者各自构建导致的环境不一致。

三、第一步:分析包结构

读取包的 podspec 与package.json,确定以下关键信息:

  • Pod 名称、codegen 名称、源码目录、语言混合情况(Swift/ObjC/ObjC++/C++)、依赖的框架;
  • 检查头文件导入问题:是否存在#import "RCTFabricComponentsPlugins.h"、是否存在相对路径../导入。

这些判断直接决定后续步骤的工作量:

判断项检查方式影响
Pod 名称podspec 中的s.name决定 product 的podName字段
Codegen 名称package.jsoncodegenConfig.name决定是否需要 codegen targets 及moduleName取值
语言混合统计.swift/.m/.mm/.cpp文件决定 SPM 拆分为多少个 target(SPM 要求不同语言使用独立 target)
头文件导入问题搜索RCTFabricComponentsPlugins.h../导入决定是否需要修改node_modules/源码并生成 patch

四、第二步:修复头文件导入(如需要)

直接编辑node_modules/中的源文件。文档给出三类标准修复模式:

模式 A#import "RCTFabricComponentsPlugins.h"改为模块化导入:

#import <React/RCTFabricComponentsPlugins.h>

原因是预构建的 XCFramework 中 React 是独立模块,包内源码不能再以裸文件名方式引用 React 私有头文件。

模式 B— 相对父级导入加__has_include保护:

#if __has_include("../Foo.h") #import "../Foo.h" #else #import "Foo.h" #endif

这样同一份源码在 CocoaPods 源码编译(保留相对目录结构)与 SPM 预构建(头文件被重新组织)两种形态下都能编译。

模式 C— 跨模块边界被 ObjC 使用的 Swift 类/方法需要open/public

open class MyView: RCTView { override public func view() -> UIView! {

CocoaPods 中所有源文件编译进同一个静态库,internal即可互见;而 SPM 预构建把包切成多个 target/模块后,跨模块可见性必须显式提升。

五、第三步:用 try_link_with_prebuilt_xcframework 包裹 podspec

编辑node_modules/中的 podspec,把源码编译相关配置包进条件块:

# Expo prebuilt xcframework support if !Expo::PackagesConfig.instance.try_link_with_prebuilt_xcframework(s) # Build from source s.source_files = "..." s.exclude_files = "..." s.pod_target_xcconfig = { ... } s.dependency "..." s.subspec "..." do |ss| ... end end

条件块内部只能放源码编译相关属性:s.source_filess.exclude_filess.pod_target_xcconfigs.xcconfigs.dependency、subspec、s.resource_bundles条件块外部保留:install_modules_dependencies(s)s.requires_arcs.swift_versions.platformss.sources.licenses.authors.homepage

这一模式的运行语义可以从 CocoaPods 集成层源码得到印证。Expo::PackagesConfig.try_link_with_prebuilt_xcframework只是委托调用(见 packages_config.rb),真正实现位于 precompiled_modules.rb:当预构建产物可用时,它会返回true并把spec.source指到本地 tarball URI、设置vendored_frameworks指向预构建产物、为被跳过的依赖补充FRAMEWORK_SEARCH_PATHS、注入prepare_command与构建期 debug/release 切换脚本——此时条件块内部的源码编译配置全部被跳过;当产物不可用时返回false,Pod 回落到块内原有的源码编译路径。这正是“预构建优先、源码兜底”双形态机制的核心。

六、第四步:创建 spm.config.json

packages/expo-modules-autolinking/external-configs/ios/<PACKAGE>/spm.config.json写入配置。该文件的字段级定义见 JSON Schema:spm.config.schema.json。由于配置文件位于ios/<PACKAGE>/两层子目录下,实际落库配置中$schema使用五级相对路径指向 Schema(可对照 react-native-screens 的配置):

"$schema": "../../../../../tools/src/prebuilds/schemas/spm.config.schema.json"

6.1 Product 层字段

  • name:产品名(= pod 名),即最终 XCFramework 名称;
  • podName:必须与 podspec 中s.name完全一致;
  • codegenName:包使用 codegen 时必填,取package.jsoncodegenConfig.name
  • platforms:通常为["iOS(.v15)"](Schema 的枚举值还包括iOS(.v16)iOS("16.4")macOS(.v11)tvOS(.v15)macCatalyst(.v15));
  • externalDependencies:常规取["ReactNativeDependencies", "React", "Hermes"];凡含 Fabric 组件或 TurboModules 的包必须包含Hermes(JSI 符号来自 Hermes)。

6.2 Codegen targets 的路径约定

package.json中存在codegenConfig时,codegen 产物固定放在两个位置:

  • C++ 组件:.build/codegen/build/generated/ios/ReactCodegen/react/renderer/components/<codegenName>,target 的moduleName必须等于codegenName
  • ObjC 模块:.build/codegen/build/generated/ios/ReactCodegen/<codegenName>,同样设置moduleName

所有 codegen 相关 target 必须带moduleName字段,其作用是告知构建系统:当 XCFramework 被使用时,把这部分源码从 ReactCodegen 中排除,避免重复编译。

6.3 target 类型与依赖规则

类型适用源文件标准依赖
"cpp".cpp/.c["React", "ReactNativeDependencies"]
"objc".m/.mm["Hermes", "React", "ReactNativeDependencies"]
"swift".swift["Hermes", "React", "ReactNativeDependencies", "expo-modules-core/ExpoModulesCore"]

配套的两个通用约定:

  • ObjC 的常用编译标志:["-include", "Foundation/Foundation.h"](Schema 中compilerFlags还支持{ "common": [...], "debug": [...], "release": [...] }结构化写法,以及按c/cxx语言分别指定标志);
  • 常用排除项:["**/*.macos.*", "<PodName>.xcodeproj/**"]

6.4 真实配置对照:react-native-screens(Swift + ObjC + C++ + codegen 混合)

react-native-screens/spm.config.json 展示了混合语言包的完整 target 拓扑:RNScreens_codegen_components(cpp)、RNScreens_codegen_modules(objc)、RNScreens_common_cpp(cpp)、RNScreens_cpp(cpp)四个子 target 全部声明了moduleNamernscreensrnscreens_turbo),主 targetRNScreens(objc,pattern: "**/*.{m,mm}")依赖上面全部 target 并声明linkedFrameworks: ["Foundation", "UIKit", "QuartzCore", "CoreGraphics"]。它同时演示了两个进阶字段:

  • fileMapping:把react/renderer/components/rnscreens/*.h等头文件重新映射到rnscreens/目录,并用type: "symlink"建立react/renderer/components/rnscreens目录软链,让源码中按原路径的#import继续可用;
  • moduleMapContent:为 C++ 头文件提供自定义 module map,把全部头标记为textual header(这些头依赖 JSI 宏与包含它的编译单元,不能被独立预编译)。

产品层还通过excludeFromUmbrella排除了Swift-Bridging.hRNScreens-Bridging-Header.h等桥接头——因为 SPM 不支持 CocoaPods 风格的 bridging header,这些文件不能进入自动生成的 umbrella header。

6.5 真实配置对照:react-native-reanimated(跨包依赖与结构化编译标志)

react-native-reanimated/spm.config.json 展示了两个典型场景:

  • 跨包依赖externalDependencies中加入了"RNWorklets"(react-native-worklets 的 product 名,而非 npm 包名),主 target 与 C++ target 的dependencies也引用它。预编译时这类依赖会被解析为对应包的 xcframework 头文件路径而非源码路径;
  • 结构化 compilerFlagsRNReanimated_cpptarget 使用{ "common": { "c": [...], "cxx": [...] }, "debug": [...] }形式,其中 cxx 侧单独加了-fno-cxx-modules(规避 C++ 模块化的system_clock报错),debug 侧加了-DHERMES_ENABLE_DEBUGGER=1;另外REANIMATED_FEATURE_FLAGS这类含引号宏在 JSON 中写作\\\"...\\\",转义后进入生成的Package.swift

七、第五步:运行 patch-package 并停止

node_modules/中 podspec 与源文件的所有修改,最后统一生成补丁:

npx patch-package <PACKAGE>

补丁文件落盘在仓库patches/<package-name>+<version>.patch(仓库已有 react-native-reanimated、@shopify/react-native-skia 等外部包补丁可参考)。此处即为流程终点:不构建、不测试、不继续。构建与验证由et prebuild管线统一承担,例如(见 external-configs/ios/README.md):

# 构建外部包的 XCFramework(--include-external 表示纳入外部包) et prebuild --include-external react-native-screens # 检查产物 ls -la packages/precompile/.build/react-native-screens/output/debug/xcframeworks/

若修改了exclude模式或 target 结构后出现旧符号残留,可用et prebuild --clean --include-external <package-name>清理旧构建输出——SPM 生成器只创建新符号链接,不会移除之前被包含、现已被排除的文件。

八、参考包与关键源码索引

文档建议以已转换包作为目标结构参照,其中 ObjC-only + codegen(react-native-gesture-handler)、ObjC + 自定义 C++ shadow nodes(react-native-svg)、混合 Swift + ObjC + SPM 远程依赖(lottie-react-native)、混合 Swift + ObjC + C++ + codegen(react-native-screens)、ObjC + C++ + codegen(react-native-safe-area-context)五类结构覆盖了常见形态。需要说明的是,从当前仓库结构看,external-configs/ios/下实际落盘的配置为前文列出的 7 个包,文档提到的 lottie-react-native、react-native-gesture-handler 配置未包含在本快照该目录中,研究目标结构时建议以在库配置为准;lottie 场景(Swift/ObjC 拆分 +spmPackages远程依赖)的完整字段说明可在 external-configs/ios/README.md 的示例 3 中找到。

深入理解该流程时,建议按以下顺序阅读源码:

关注点文件
外部包发现与解析tools/src/prebuilds/ExternalPackage.ts
Package.swift生成tools/src/prebuilds/SPMPackage.ts
Codegen 处理tools/src/prebuilds/Codegen.ts
管线入口与模块职责tools/src/prebuilds/README.md
podspec 预构建链接实现packages/expo-modules-autolinking/scripts/ios/precompiled_modules.rb
配置字段级定义tools/src/prebuilds/schemas/spm.config.schema.json

九、转换完成前的自检清单

结合文档规则与 Schema 约束,一份合格的转换产物应满足:

  1. spm.config.json位于packages/expo-modules-autolinking/external-configs/ios/<PACKAGE>/$schema指向 tools/src/prebuilds/schemas/spm.config.schema.json;
  2. product 的podName与 podspecs.name一致,codegenNamepackage.jsoncodegenConfig.name一致;
  3. 所有 codegen target 的moduleName等于 codegen 名,target 的type与源文件语言匹配(cpp/objc/swift);
  4. path+pattern精确选中目标源文件,exclude过滤掉 xcodeproj 与 macOS 专属文件;
  5. podspec 已用try_link_with_prebuilt_xcframework条件包裹源码编译属性,且块外保留s.platformss.source等元信息;
  6. 头文件问题已按模式 A/B/C 修复,Swift 跨模块类已提升为open/public
  7. 补丁由npx patch-package <PACKAGE>生成而非手写,且流程在补丁生成后即终止。

满足以上各点后,该包即可交由et prebuild管线统一预构建,与 Expo 自家模块以完全相同的流程产出 XCFramework 并进入packages/precompile/.cache/的共享依赖缓存体系。

【免费下载链接】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),仅供参考

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

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

立即咨询