该技能面向使用 Expo 的项目,核心做法是持续原生生成(CNG):ios/ 与 android/ 目录视为 app.config.ts、config plugins 和已安装原生包所产生的构建输出,所有原生改动都通过配置键或插件列表中的一行完成,避免直接编辑生成的原生工程。
用途与主要能力
- 统一配置:要求 app.config.ts 是唯一应用配置,且为纯函数,同步返回对象;同一环境下结果一致,不含 null,且不在代码中应用插件。
- 生成目录管理:ios/ 与 android/ 为生成输出,需在 .gitignore(以及在 EAS 场景下替代它的 .easignore)中锚定忽略 /ios 和 /android;git 不跟踪其中内容,任何 npm 脚本、scripts 文件或 CI 步骤不得写入这些目录。
- 配置键优先:ios.infoPlist、ios.entitlements、locales、android.permissions/blockedPermissions、ios.deploymentTarget 以及 expo-build-properties 可覆盖多数需求,只有内置键无法覆盖时才编写插件。
- 插件显式化:所有会自行应用插件的已安装包都需在插件列表中显式列出并写明选项;不允许重复列出同一插件;插件自动添加的权限(如存储相关权限)仅通过 android.blockedPermissions 按精确大小写移除。
- 本地插件规范:放在 plugins/with-<capability>.ts,按路径列出;默认导出带类型的 ConfigPlugin,SDK 57 从 'expo/config-plugins.js' 导入辅助函数(SDK 58 起为裸导入),禁用 @expo/config-plugins。
- mod 使用规则:优先类型化 mod;文本 mod 需通过 CodeGenerator.mergeContents 或 includes 检查保证幂等;危险 mod 必须注明原因,并限定写入范围。
- 插件质量要求:每个插件幂等、校验选项,并有配套测试,通过 runMod 走 Expo 自身的 mod 链执行;同一原生键只允许一个所有者,并对 mod 顺序做明确约定。
- 快照与校验:通过内省生成 contracts/native-config.json 快照并随改动提交,使任何原生改动在评审中可见;提供 check-native-generation.mjs 与 check-native-snapshot.mjs 两个检查脚本,要求输出 RESULT: PASS。
使用方式(工作流要点)
- 判断改动能否由内置键完成;能则改 app.config.ts 并进入验证。
- 新原生包先安装依赖,再在插件列表登记其插件。
- 无键可覆盖时编写本地插件,参考模板 with-android-optional-features.ts 及其测试。
- 用 jest 按路径运行插件测试,并确认测试被 Jest 发现。
- 在 app.config.ts 中按路径列出插件并写选项与归属注释。
- 对每个变体运行 expo config --type introspect --json,运行快照检查并阅读每条 native-drift;确认符合预期后再用 --write 重新生成。
- 文本或危险 mod 需先执行 clean prebuild,再执行 --no-clean,确认改动只出现一次。
- 运行 typecheck、eslint、jest 等门禁,再运行两个检查脚本直至全部通过。
- 离开或回到 CNG 需先与开发者确认,并在 contracts/native-generation.json 中记录模式、原因与日期。
适用场景
- 需要以声明式方式维护 iOS/Android 原生配置的 Expo 项目。
- 需要添加 Info.plist 或 AndroidManifest 条目、增加权限或接入原生包插件的场景。
- 需要在代码评审中展示原生配置变化、防止原生改动随机器漂移的团队。
限制与注意事项
- 该技能是一套规范与脚本,具体要求以资料所列规则为准;未提供的收费、价格、平台支持范围等信息暂未提供。
- 反模式包括:在 Xcode 中直接改设置、脚本中 sed 修改 AndroidManifest、为内置键能完成的事写插件、漏登记已安装包的插件、在 app.config.ts 中 import 并调用插件、使用错误的插件导入路径、无检查地追加 manifest/Gradle/Podfile、在测试中共享可变配置常量、改配置后不 clean prebuild 直接 run、CI 中使用 --no-clean、以 ios/ 代替 /ios 忽略、.easignore 漏写目录、为让检查通过而滥用 --write、未记录理由就提交原生目录。