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 目录,整体构建流程为:
- 发现并校验目标包与构建选项;
- 解析版本与本地 tarball 输入;
- 按 flavor(Debug/Release)解析产物缓存;
- 对每个
package/product@flavor单元:生成源码与Package.swift→ 构建 framework → 合成 XCFramework → 校验产物; - 打印摘要,必要时写错误日志。
这一流程在 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-skia、react-native-reanimated、react-native-safe-area-context、react-native-screens、react-native-svg、react-native-worklets(完整清单见 external-configs/ios/README.md)。
二、任务规则:允许做什么,禁止做什么
文档为转换流程设定了明确的边界约束,执行时必须遵守:
- 只能创建/编辑三类文件:
packages/expo-modules-autolinking/external-configs/ios/<PACKAGE>/spm.config.json、node_modules/中的 podspec、以及node_modules/中需要修复头文件的源文件; - 永远不要手工编写 patch 文件。所有对
node_modules/的修改必须通过npx patch-package <PACKAGE>生成补丁; - 永远不要构建或测试产物。不要运行
et prebuild-packages或pod 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.json的codegenConfig.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_files、s.exclude_files、s.pod_target_xcconfig、s.xcconfig、s.dependency、subspec、s.resource_bundles。条件块外部保留:install_modules_dependencies(s)、s.requires_arc、s.swift_version、s.platforms、s.source、s.license、s.author、s.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.json的codegenConfig.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 全部声明了moduleName(rnscreens或rnscreens_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.h、RNScreens-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 头文件路径而非源码路径; - 结构化 compilerFlags:
RNReanimated_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 约束,一份合格的转换产物应满足:
spm.config.json位于packages/expo-modules-autolinking/external-configs/ios/<PACKAGE>/,$schema指向 tools/src/prebuilds/schemas/spm.config.schema.json;- product 的
podName与 podspecs.name一致,codegenName与package.json的codegenConfig.name一致; - 所有 codegen target 的
moduleName等于 codegen 名,target 的type与源文件语言匹配(cpp/objc/swift); path+pattern精确选中目标源文件,exclude过滤掉 xcodeproj 与 macOS 专属文件;- podspec 已用
try_link_with_prebuilt_xcframework条件包裹源码编译属性,且块外保留s.platforms、s.source等元信息; - 头文件问题已按模式 A/B/C 修复,Swift 跨模块类已提升为
open/public; - 补丁由
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),仅供参考