使用危险的 mod

编辑页面

了解危险的 mod,以及在创建配置插件时如何使用它们。


For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.

Expo 中的危险 mod 通过字符串操作和正则表达式直接访问原生项目文件。虽然现有 mod 插件是推荐的方式,但危险 mod 作为一种逃生口,可用于那些无法通过现有 mod 插件实现的修改。

为什么它们被认为是危险的?

自动化的直接源代码修改通常并不能很好地组合使用。比如,如果一个危险 mod 替换了源文件中的文本,而后续另一个危险 mod 预期原始文本仍然存在(也许它会把原始文本作为正则表达式的锚点),那么它很可能无法产生预期结果——取决于它的实现方式,它可能会抛出错误或记录日志。其他类型的 mod 较少出现这类问题,不过像 withAndroidManifestwithPodfile 这种直接操作源文件的 mod 也可能发生这种情况。

与可安全多次运行的标准 mod 不同,危险 mod 很少能保证幂等。多次运行同一个危险 mod 可能会产生不同结果、导致重复修改,甚至彻底破坏目标文件。

何时使用危险 mod

在以下情况下可以考虑使用危险 mod:

  • 无法使用标准 mod 完成修改:你需要的修改不受现有 mod 插件支持,例如 withAndroidManifestwithPodfile 等,或者某个库需要标准插件未覆盖的特定原生修改。
  • 旧版 Expo SDK 兼容性: 你正在目标 Expo SDK 的较旧版本,而该版本不包含你需要的 mod 插件。
  • 需要使用正则或替换函数修改文本:你需要执行现有 mod 插件不支持的复杂文本处理。Expo 内部会在大规模文件系统重构时使用危险 mod,例如某个库名称发生变化时。

如何使用危险 mod

在实际场景中,你可以按照创建 config plugin 章节中的标准 config plugin 使用方式,直接在项目中使用本节描述的示例 config plugin。不过,对于名为 withPodfile 的现有 mod 插件来说,你并不需要使用危险 mod。下面的示例只是为了演示如何创建和使用危险 mod。

让我们看一个示例 config plugin,它用于修改原生目录(ios)中的文件。当你在 Expo 项目中使用 Continuous Native Generation 时,这很有用。在这个 config plugin 的帮助下,原生文件(ios/Podfile)会在每次运行 npx expo prebuild 命令时更新,无论你是手动运行还是通过 EAS Build 运行)。当现有 mod 插件无法编辑和更新原生目录中的文件时,这个示例就是一个理想用例。

按照创建 config plugin 章节中的目录结构和创建步骤(步骤 3、4 和 5),我们假设这个 config plugin 创建在 Expo 项目的 plugins 目录中:

withCustomPodfile.ts
import { ConfigPlugin, IOSConfig, withDangerousMod } from 'expo/config-plugins'; import fs from 'fs/promises'; import path from 'path'; const withCustomPodfile: ConfigPlugin = config => { return withDangerousMod(config, [ 'ios', async config => { const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile'); try { let contents = await fs.readFile(podfilePath, 'utf8'); const projectName = IOSConfig.XcodeUtils.getProjectName(config.modRequest.projectRoot); contents = addCustomPod(contents, projectName); await fs.writeFile(podfilePath, contents); console.log('✅ 成功将自定义 pod 添加到 Podfile'); } catch (error) { console.warn('⚠️ 未找到 Podfile,跳过修改'); } return config; }, ]); }; function addCustomPod(contents: string, projectName: string): string { if (contents.includes("pod 'Alamofire'")) { console.log('Alamofire pod 已存在,跳过'); return contents; } const targetRegex = new RegExp( `(target ['"]${projectName}['"] do[\\s\\S]*?use_expo_modules!)`, 'm' ); return contents.replace(targetRegex, `$1\n pod 'Alamofire', '~> 5.6'`); } export default withCustomPodfile;

在上面的示例中,插件 withCustomPodfile 会在 prebuild 过程中,自动向你项目的原生 ios/Podfile 中添加一个 CocoaPod 依赖。它使用 withDangerousMod 直接访问原生文件系统,并在原生项目生成之后、安装任何 CocoaPod 依赖之前运行。

Podfile 需要直接进行文本修改,这通过 addCustomMod 函数中的正则表达式模式完成。这个过程还要求将 CocoaPod 依赖插入到 Podfile 的特定位置,也就是在 use_expo_modules! 语句之后。

withDangerousMod 语法和要求

使用 withDangerousMod 需要以下参数:

  1. 一个原生平台(androidios
  2. 一个异步函数,接收具有文件系统访问权限的 config 对象
  3. 用于访问原生目录中内容的相对文件名/路径
  4. 读取现有文件、修改其内容,并将其写回文件
  5. (可选)在插件于 prebuild 过程中执行时,为成功和失败状态记录自定义消息

下面的代码片段提供了所需字段的骨架,以及在使用 withDangerousMod 时 config plugin 可以如何组织:

import { ConfigPlugin, withDangerousMod } from 'expo/config-plugins'; import fs from 'fs/promises'; import path from 'path'; const myPlugin: ConfigPlugin = config => { return withDangerousMod(config, [ 'platform', // 1. "ios" | "android" async config => { // 2. 异步修改函数 // 3. 构建文件路径 const filePath = path.join( config.modRequest.platformProjectRoot, // 原生项目根目录 'path/to/file' // 目标文件的相对路径 ); try { // 4. 读取现有文件,修改其内容,并将其写回文件 let contents = await fs.readFile(filePath, 'utf8'); contents = modifyContents(contents); await fs.writeFile(filePath, contents); // 5. 记录成功和失败状态 console.log('✅ 成功修改文件'); } catch (error) { console.warn('⚠️ 文件修改失败:', error); } return config; }, ]); }; // 使用正则表达式修改文件内容的辅助函数

config plugin 中可用的路径

config plugin 中可用的不同路径属性:

路径类型描述
config.modRequest.projectRootstring通用应用项目根目录,即 package.json 所在目录。用于解析资源、读取 package.json 以及跨平台操作。请始终确认该目录存在且包含 package.json
config.modRequest.platformProjectRootstring平台特定的项目根目录(projectRoot/androidprojectRoot/ios)。用于平台特定的文件操作,例如修改原生配置文件。请确保平台目录相对于主 projectRoot 存在。
config.modRequest.projectNamestring[仅 iOS] 用于构建 iOS 文件路径的项目名组件(例如 projectRoot/ios/[projectName]/)。用于 iOS 特定的文件路径构建。仅在 iOS 平台可用,并且应与实际的 Xcode 项目结构一致。
config.modRequest.introspectboolean是否处于不应修改文件系统的 introspection 模式。为 true 时,mod 只能读取和分析文件,不能写入。用于配置分析和验证期间。
config.modRequest.ignoreExistingNativeFilesboolean是否忽略现有的原生文件。用于基于模板的操作,尤其会影响 entitlements 和其他原生配置,以确保与 prebuild 预期保持一致。

使用危险 mod 时的注意事项

使用危险 mod 时,请考虑以下几点:

  • 幂等性保证有限。 与通常幂等、且在不使用 clean 标志时也能工作的标准 mod 不同,危险 mod 很少能保证幂等。这意味着多次运行同一个危险 mod 可能会产生不同结果或引发问题。
  • 实验性且容易出问题。 使用 withDangerousMod 时要谨慎,因为它未来可能会变更。请在每个 SDK 版本中都对你的危险 mod 进行充分测试,因为当原生模板发生变化时,它们尤其容易出问题。
  • 使用标准 mod 插件。Android 和 iOS 都提供了诸如 withAndroidManifestwithPodfilewithPodfileProperties 等 mod 插件,用于执行常见的原生文件修改。只有在没有现有 mod 插件可用来处理你的用例时,才使用危险 mod。
  • 不要假设文件一定存在。在读写之前,始终检查原生目录以及文件的相对路径。如果你使用 CNG,可以随时运行 npx expo prebuild 来创建原生 androidios 目录,并手动验证文件是否存在。
  • 危险 mod 会先运行。危险 mod 执行的顺序可能不可靠,因为危险 mod 会在其他修改器之前运行。这会影响构建过程的可预测性,并可能与其他修改发生冲突。