Expo patch-project 深度解析:基于补丁的 CNG 工作流与源码级实践指南
Expo patch-project 深度解析基于补丁的 CNG 工作流与源码级实践指南【免费下载链接】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/expoExpo 生态中的patch-project包是为CNGContinuous Native Generation持续原生生成工作流设计的 config-plugin 与配套工具它允许开发者在expo prebuild生成的原生工程android/ios 目录上手动修改后把改动固化为补丁文件并在后续每次 prebuild 时自动重放从而在不放弃托管工作流的前提下持久化自定义原生代码。本文将以 packages/patch-project/CHANGELOG.md 记录的功能演进为主线结合仓库内patch-project的完整源码讲解补丁的生成、命名、校验、应用全流程并深入分析补丁自动跳过、monorepo 路径处理等关键实现的底层原理让读者既能直接上手使用也能理解其内部机制为排查与二次开发打下基础。一、为什么需要 patch-projectCNG 工作流的痛点Expo 的 CNG 工作流下原生工程android/、ios/由npx expo prebuild从模板与 app config 动态生成因此对原生目录的手动修改会在下次 prebuild 时被覆盖。官方推荐用 config-plugins 把定制逻辑固化到配置中但总存在一些难以用插件表达的改动例如针对某第三方原生库的源码级修复。patch-project正是为此提供了一条托管式打补丁路径它由 CLI 与 config-plugin 两部分组成见 package.json 中bin.patch-project与main: build/withPatchPlugin.js的定义CLI 负责把当前原生工程相对干净的 prebuild 产物的差异导出为.patch文件config-pluginwithPatchPlugin在 prebuild 过程中自动发现并应用这些补丁保证原生工程每次都能复现同样的改动。二、快速上手安装与生成补丁2.1 安装在 Expo 项目中安装该包npx expo install patch-project2.2 生成补丁手动修改android/或ios/目录中的文件之后运行npx patch-project该命令会把当前原生工程与重新 prebuild 出的干净工程进行 diff并将差异保存到项目根目录的cng-patches目录下生成形如androidtemplateChecksum.patch、iostemplateChecksum.patch的文件。此后每次运行npx expo prebuildwithPatchPlugin都会自动把这些补丁应用到新生成的原生工程中。2.3 补丁文件命名的奥秘补丁文件名的后缀templateChecksum并非随机值。从 patchProjectAsync.ts 的源码可见const patchFilePath path.join(projectRoot, patchRoot, ${platform}${templateChecksum}.patch);templateChecksum由 generateNativeProjects.ts 中调用的cloneTemplateAndCopyToProjectAsync来自expo/cli的 prebuild 内部实现返回是生成原生工程所用模板内容的校验和。在应用侧withPatchPlugin.ts插件同样按cng-patches/${platform}${templateChecksum}.patch查找补丁const patchFilePath path.join(patchRoot, ${platform}${templateChecksum}.patch);这种模板校验和绑定机制保证了只有当本次 prebuild 使用的模板与生成补丁时一致时补丁才会被匹配并应用若模板升级如 Expo SDK 升级导致校验和变化旧补丁不会被误用到不兼容的工程上插件会通过WarningAggregator给出存在补丁文件但没有匹配项的警告。三、CLI 完整参数详解patch-project的 CLI 入口位于 cli/index.ts使用arg解析参数。完整的命令行选项如下源码中以printHelp输出参数说明dirExpo 项目目录默认为当前工作目录--clean生成补丁后删除原生目录用于裸工程转回托管工程的转换场景--template template指定 prebuild 使用的模板可为本地 tar 文件路径或 GitHub 仓库-p, --platform all\|android\|ios要同步的平台默认all-h, --help查看用法其中--platform的解析逻辑cli/index.ts有一个值得注意的细节function resolvePlatformOption(platform all, { loose } {}): ModPlatform[] { switch (platform) { case ios: return [ios]; case android: return [android]; case all: return loose || process.platform ! win32 ? [android, ios] : [android]; default: return [platform as ModPlatform]; } }在 Windows 上默认的all会被收窄为仅android——这与 prebuild 本身不支持在 Windows 上构建 iOS 的限制保持一致。随后 patchProjectAsync.ts 还会调用ensureValidPlatforms做二次校验。四、补丁生成流程的源码级拆解patchProjectAsyncpatchProjectAsync.ts是整套流程的主入口其核心思路是用 Git 仓库作为 diff 引擎把用户的原始工程与重新 prebuild 的干净工程做一次版本对比。4.1 前置准备加载环境变量调用expo/env的consumeConfigEnvMode()与loadProjectEnv(projectRoot, { mode: development })加载 Expo config 与.env文件使用开发模式读取 app configgetConfig(projectRoot)得到exp配置对象平台健全性检查generateNativeProjects.ts平台目录必须存在且非空android.package/ios.bundleIdentifier必须在 app config 中定义本机必须安装 Git否则报错提示安装创建临时工作目录在项目根下建立.patch-project-tmp/platform/内含template、diff、origin、tmp四个子目录见 workingDirectories.ts放在项目根内是为了让文件移动操作保持在同一文件系统上、速度更快。4.2 归一化原生工程normalizediff 质量的关键在于消除非用户改动的噪声。对 iOS 工程normalizeNativeProjects.ts 会在生成 diff 前对project.pbxproj做大量清理移除 prebuild 与pod install过程中产生的动态内容包括noop-file.swift、Swift bridging header、PrivacyInfo.xcprivacy等生成文件Pods、Supporting、ExpoModulesProviders等自动生成的 PBXGroup[Expo] Configure project、[CP] Embed Pods Frameworks等 shell script build phaselibPods-name.a框架引用、Pods 的baseConfigurationReference等 CocoaPods 相关属性。这些条目在两次 prebuild 之间的 UUID 或内容会动态变化若不剔除会让补丁文件充满无效噪音甚至无法稳定应用。normalizeNativeProjectsAsync在备份模式下会先把原始 pbxproj 拷贝到临时目录生成补丁后再由revertNormalizeNativeProjectsAsync恢复若未使用--clean。4.3 核心 diff 流程patchProjectForPlatformAsyncpatchProjectAsync.ts按以下步骤执行将用户的原始原生目录移动到origin目录调用 generateNativeProjectsAsync 从模板重新生成干净工程并执行 config-plugins对 iOS 还会运行pod install注释明确指出因为pod install阶段会发生一些变化安装 CocoaPods 是必须的这样才能最小化 diff对新生成工程做同样的归一化处理在diff目录初始化临时 Git 仓库gitPatch.ts把干净工程加入索引并提交为Base commit from prebuild template清空干净工程把用户原始工程移入 diff 仓库执行git diff生成补丁文件diff 参数固定包含--no-color --ignore-space-at-eol --no-ext-diff --src-prefixa/ --dst-prefixb/gitPatch.ts保证补丁格式统一、可跨机器复现空补丁自动清理如果 diff 结果大小为 0说明用户工程与干净工程完全一致会删除生成的补丁文件并提示 No changes detected除非使用--clean否则把原始原生工程移回项目根目录并恢复归一化前的备份文件。值得注意的是临时 Git 仓库的提交作者被硬编码为expo-cng noreplyexpo.devgitPatch.ts避免污染用户的 Git 配置。4.4 默认 .gitignore 保证补丁纯净初始化临时仓库时gitPatch.ts会写入一个固定的.gitignore# These files are generated by pod install and should not be included in patch files. Podfile.lock contents.xcworkspacedataPodfile.lock与contents.xcworkspacedata是pod install的产物若被纳入 diff 会产生大量与用户改动无关的噪声因此被显式排除。五、自动应用补丁withPatchPlugin 的实现withPatchPluginwithPatchPlugin.ts是注册在app.plugin.js中的 config-plugin入口见 app.plugin.js。它通过withFinalizedMod挂在 prebuild 流程的收尾阶段对 android 与 ios 分别创建withAndroidPatchPlugin/withIosPatchPlugin并用withRunOnce保证每个平台只执行一次。插件属性PatchPluginProps支持两个配置项属性类型默认值说明patchRootstringcng-patches存放补丁文件的目录changedLinesLimitnumber300补丁允许的最大变更行数超过则告警5.1 补丁匹配与多重补丁告警determinePatchFilePathAsyncwithPatchPlugin.ts负责定位补丁文件并对两种异常情况给出警告目录中存在补丁文件但没有与当前templateChecksum匹配的文件时会回退使用第一个匹配平台前缀的补丁并警告Having patch files in patchRoot but none matching ...目录中存在多个同平台补丁时只应用与 checksum 匹配的那个并警告Having multiple patch files in patchRoot is not supported。5.2 应用前的变更行数守护在应用补丁前插件用git apply --numstat统计补丁的增删行数gitPatch.ts若超过changedLinesLimit默认 300 行会通过WarningAggregator对该平台发出警告。这是一个安全网一个超过数百行的补丁往往意味着把整个原生文件整体替换了进去一旦模板升级这类补丁极易冲突警告可以及时提醒开发者审视。5.3 已应用补丁的自动跳过幂等保护这正是 CHANGELOG.md 中 Unpublished 版本记录的重要修复Skip applying a CNG patch that is already applied to the native project, e.g. when runningnpx expo prebuild --no-cleanmore than once。当使用npx expo prebuild --no-clean时原生工程会保留上次生成的内容包括已应用的补丁。若此时再次运行 prebuild插件若盲目重新git apply同一补丁会因为改动已经存在而失败。解决方案是 gitPatch.ts 中的isPatchAppliedAsyncawait runGitAsync( [apply, --reverse, --check, --ignore-whitespace, ...pathArgs, patchFilePath], { cwd: projectRoot } );它使用git apply --reverse --check检测补丁是否已处于反向可干净撤销的状态——若反向应用能通过检查说明补丁已被应用此时插件直接跳过调试模式下会打印[withPatchPlugin] Patch is already applied, skipping: path实现真正的幂等。六、monorepo 场景下的补丁路径修复CHANGELOG.md 记录的另一项关键修复是Apply CNG patches relative to the project directory, so that patches are no longer silently skipped when the project lives in a monorepo subdirectory并特别提示手工为补丁路径加上项目目录前缀的补丁必须重新生成。该问题的根因在 gitPatch.ts 的注释中有详细阐述补丁是在一个以原生工程为根的独立仓库中生成的所以其内部路径是相对原生工程的而git apply默认从所在仓库的根目录解析路径并会忽略落在当前目录之外的路径。在 monorepo 中项目位于子目录时这种组合会让每个 hunk 都指向错误位置补丁被无声跳过。--directory恢复了缺失的前缀。修复后的getPatchPathArgsAsync通过git rev-parse --show-prefix获取项目相对仓库根的前缀const prefix await runGitAsync([rev-parse, --show-prefix], { cwd: projectRoot }); return prefix ? [--directory${prefix}] : [];项目位于仓库根时prefix为空字符串不追加参数项目位于apps/mobile/等子目录时自动追加--directoryapps/mobile/让补丁路径重新指向正确位置若项目根本不在任何 Git 仓库中rev-parse失败且非ENOENT则不追加参数因为此时git apply会从当前目录即projectRoot解析路径。这一修复同样作用于isPatchAppliedAsync与getPatchChangedLinesAsync使幂等检测与行数统计在 monorepo 下保持行为一致。七、变更日志中的其他真实修复与演进纵观 packages/patch-project/CHANGELOG.md除上述两项外还可梳理出以下值得开发者知晓的行为细节开发模式加载配置与 .envUnpublishedCLI 现以开发模式mode: development加载 Expo config 与.env文件对应 patchProjectAsync.ts 中的consumeConfigEnvMode()、loadProjectEnv(projectRoot, { mode: development })与globalThis.__DEV__ true。这意味着补丁生成时会依据开发环境变量解析配置若补丁内容依赖环境分支需注意该前提。diff.noprefixtrue兼容0.1.0修复了用户全局 Git 配置了diff.noprefix时补丁生成错误的问题。生成补丁时显式传入--src-prefixa/ --dst-prefixb/见 gitPatch.ts强制补丁使用标准a/、b/前缀不受用户 Git 配置影响。空白变化导致的应用失败0.2.11修复了changed spaces空格/空白差异导致的补丁应用错误配合--ignore-space-at-eol生成侧与--ignore-whitespace应用侧见 gitPatch.ts共同降低空白差异带来的干扰。依赖演进fs-extra被原生fs取代0.2.0getenv2.0.0支持大写布尔环境变量0.2.8glob从 v7 升级到 v10 再到 v130.1.0、0.3.11expo/spawn-async^1.8.056.0.13expopeerDependencies 放宽为*0.1.24。当前版本为 57.0.9package.json。无用户可见变更的版本段57.0.x / 56.0.x 等大量版本标注为 This version does not introduce any user-facing changes说明这些是跟随主 SDK 版本线的内部同步版本而非功能性迭代。八、高级用法裸工程转回托管工程README 中提到的进阶用法是将裸工程bare project转换回托管工程managed project。核心是--clean参数生成补丁后直接删除原生目录让项目回到纯托管状态。此后只要 cng-patches 目录存在每次expo prebuild都会自动重建原生工程并应用补丁从流程上实现了修改 → 固化为补丁 → 回归托管的闭环。执行npx patch-project --help可查看全部参数细节。九、实践建议与注意事项不要把整个原生文件写进补丁changedLinesLimit默认 300 行既是守护也是信号超限补丁应优先考虑改写为 config-plugin升级 SDK 后重新生成补丁模板 checksum 变化会导致旧补丁不再匹配此时应基于新模板重新运行npx patch-projectmonorepo 中保持补丁为自动生成版本新版本已按项目目录相对路径应用补丁切勿手改补丁内部路径曾手工添加目录前缀的旧补丁需重新生成使用prebuild --no-clean无需担心重复应用isPatchAppliedAsync的幂等检测会自动跳过已应用的补丁调试信息设置环境变量EXPO_DEBUG1见 env.ts可查看补丁匹配、跳过、git 命令输出等详细日志是排查补丁为何没生效的第一手段。综上patch-project 用一套模板快照 Git diff 幂等应用的精巧设计把 CNG 工作流中易丢失的手动改动变成了可版本化、可复现、可校验的工程资产。理解其内部机制后无论是日常使用、monorepo 适配还是二次定制都能做到心中有数。【免费下载链接】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),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →