使用危险模块

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


Expo 中的危险模块通过字符串操作和正则表达式提供对本地项目文件的直接访问。虽然现有的模块插件是推荐的做法,但危险模块作为一种应急方案,用于实现无法通过现有模块插件完成的修改。

🌐 Dangerous mods in Expo provide direct access to native project files through string manipulation and regular expressions. While existing mod plugins are the recommended approach, dangerous mods serve as an escape hatch for modifications that cannot be achieved through existing mod plugins.

为什么它们被认为很危险?

自动化直接源代码操作通常不易良好组合。例如,如果一个危险的 mod 替换了源文件中的文本,而随后的另一个危险 mod 期望原始文本仍然存在(可能它使用原始文本作为正则表达式的锚点),那么它不太可能产生预期的结果——取决于它的编写方式,它可能会抛出错误或记录日志。其他类型的 mod 不太容易出现这种问题,尽管像 withAndroidManifestwithPodfile 这样直接操作源文件的 mod 也可能发生这种情况。

🌐 Automated direct source code manipulation does not typically compose well. For example, if a dangerous mod replaces text in a source file, and a subsequent dangerous mod expects the original text to be there (perhaps it uses the original text as an anchor for a regular expression) then it is unlikely produce the desired result — depending on how it is written, it may either throw an error or log. Other types of mods are less prone to this type of problem, although it can happen with mods that manipulate source files directly like withAndroidManifest and withPodfile.

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

🌐 Unlike standard mods, which can run multiple times safely, dangerous mods are rarely guaranteed to be idempotent. Running the same dangerous mod multiple times may produce different results, cause duplicate modifications, or break the target file entirely.

何时使用危险模块

🌐 When to use a dangerous mod

在以下情况下,请考虑使用危险模块:

🌐 Consider using a dangerous mod when:

  • 无法使用标准模组进行修改:你需要的修改不被现有的模组插件支持,例如 withAndroidManifestwithPodfile 等,或者某些库需要特定的原生修改,而标准插件无法涵盖这些修改。
  • 旧版 Expo SDK 兼容性: 你正在使用的 Expo SDK 版本较旧,不包含你所需的 mod 插件。
  • 需要使用正则表达式或替换函数修改文本:你需要执行现有修改插件不支持的复杂文本操作。例如,当某个库的名称发生更改时,Expo 会在内部使用危险的修改来进行大型文件系统重构。

如何使用危险模块

🌐 How to use a dangerous mod

在现实场景中,你可以按照创建配置插件部分中的标准配置插件使用模式,直接在你的项目中使用本节描述的示例配置插件。然而,对于现有的名为withPodfile的 mod 插件,你不需要使用危险的 mod。下面的示例仅用于演示如何创建和使用危险的 mod。

🌐 In a real-world scenario, you can use the example config plugin described in this section directly in your project by following the standard config plugin usage pattern from the Creating a config plugin section. However, with the existing mod plugin called withPodfile, you don't have to use the dangerous mod. The example below is just for demonstration of how a dangerous mod can be created and used.

让我们来看一个示例配置插件,用于修改本地目录(ios)中的文件。当你在 Expo 项目中使用持续本地生成时,这非常有用。借助这个配置插件,每当运行 npx expo prebuild 命令时,无论是手动运行还是使用 EAS Build,本地文件(ios/Podfile)都会更新。当现有的 mod 插件无法编辑和更新本地目录中的文件时,这个示例就是一个理想的使用场景。

🌐 Let's take a look at an example config plugin to modify a file inside a native directory (ios). This is useful when you are using Continuous Native Generation in your Expo project. With the help of this config plugin, the native file (ios/Podfile) will update anytime the npx expo prebuild command runs, whether you run it manually or using EAS Build). This example is an ideal use case when an existing mod plugin cannot edit and update a file inside a native directory.

按照目录结构和从 创建配置插件部分 中创建配置插件的步骤(步骤 3、4 和 5),假设该配置插件是在你的 Expo 项目的 plugins 目录中创建的:

🌐 Following the directory structure and steps to create a config plugin (steps 3, 4, and 5) from Creating a config plugin section, let's assume this config plugin is created inside the plugins directory of your Expo project:

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('✅ Successfully added custom pod to Podfile'); } catch (error) { console.warn('⚠️ Podfile not found, skipping modification'); } return config; }, ]); }; function addCustomPod(contents: string, projectName: string): string { if (contents.includes("pod 'Alamofire'")) { console.log('Alamofire pod already exists, skipping'); 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 会在预构建过程中自动向项目的原生 ios/Podfile 添加 CocoaPod 依赖。它使用 withDangerousMod 直接访问原生文件系统,并在原生项目生成后、任何 CocoaPod 依赖安装之前运行。

🌐 In the example above, the plugin withCustomPodfile will add a CocoaPod dependency automatically to your project's native ios/Podfile during the prebuild process. It uses withDangerousMod to provide access to the native file system directly and run after the native project is generated, but before any CocoaPod dependency is installed.

Podfile 需要直接进行文本操作,这通过在 addCustomMod 函数内使用正则表达式模式来完成。这个过程还要求 CocoaPod 依赖被插入到 Podfile 的特定位置,即在 use_expo_modules! 语句之后。

🌐 The Podfile requires direct text manipulation, which is done using a regex pattern inside addCustomMod function. This process also requires that the CocoaPod dependency is inserted into the Podfile at a specific location, which is after the use_expo_modules! statement.

withDangerousMod 语法和要求

🌐 withDangerousMod syntax and requirements

使用 withDangerousMod 需要某些参数:

🌐 Using withDangerousMod requires certain parameters:

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

下面的代码片段提供了所需字段的框架,以及在使用 withDangerousMod 时配置插件的结构示例:

🌐 The code snippet below provides a skeleton of the required field and how the config plugin can be structured when using withDangerousMod:

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. Async modification function // 3. Build file paths const filePath = path.join( config.modRequest.platformProjectRoot, // Native project root 'path/to/file' // Relative path to target file ); try { // 4. Read existing file, modify its contents, and write back to the file let contents = await fs.readFile(filePath, 'utf8'); contents = modifyContents(contents); await fs.writeFile(filePath, contents); // 5. Log success and failure states console.log('✅ Successfully modified file'); } catch (error) { console.warn('⚠️ File modification failed:', error); } return config; }, ]); }; // Helper functions to use regex to modify the contents of the file

配置插件中的可用路径

🌐 Available paths in config plugins

配置插件中可用的不同路径属性:

🌐 Different path properties available in config plugins:

PathTypeDescription
config.modRequest.projectRootstringUniversal app project root directory where package.json is located. Used for resolving assets, reading package.json, and cross-platform operations. Always verify the directory exists and contains package.json.
config.modRequest.platformProjectRootstringPlatform-specific project root (projectRoot/android or projectRoot/ios). Used for platform-specific file operations like modifying native configuration files. Ensure the platform directory exists relative to main projectRoot.
config.modRequest.projectNamestring[iOS only] Project name component for constructing iOS file paths (for example, projectRoot/ios/[projectName]/). Used for iOS-specific file path construction. Only available on iOS platform and should match the actual Xcode project structure.
config.modRequest.introspectbooleanWhether running in introspection mode where no filesystem changes should be made. When true, mods should only read and analyze files without writing. Used during config analysis and validation.
config.modRequest.ignoreExistingNativeFilesbooleanWhether to ignore existing native files. Used in template-based operations, particularly affects entitlements and other native configs to ensure alignment with prebuild expectations.

使用危险模块时的注意事项

🌐 Considerations when using a dangerous mod

使用危险模块时,请考虑以下事项:

🌐 When using a dangerous mod, consider the following:

  • 有限的幂等性保证。 与通常可幂等且可在没有清除标志的情况下工作的标准模组不同,危险模组很少保证幂等性。这意味着多次运行相同的危险模组可能会产生不同的结果或引发问题。
  • 实验性且容易出错。 使用 withDangerousMod 时请小心,因为它将来可能会发生变化。每次 SDK 发布时都要彻底测试你的高风险模组,因为在原生模板发生更改时,它们尤其容易出错。
  • 使用标准的mod插件。Android和iOS都提供像withAndroidManifestwithPodfilewithPodfileProperties等mod插件,用于执行常见的本地文件修改。只有在没有可用的mod插件可以处理你的使用场景时,才使用危险的mod。
  • 不要假设文件存在。在读取或写入文件之前,务必检查本地目录和文件的相对路径。如果你使用 CNG,你可以随时运行 npx expo prebuild 来创建本地的 androidios 目录,并手动验证文件是否存在。
  • 危险的模块先运行。危险模块的执行顺序可能不可靠,因为它们会在其他修改器之前运行。这可能影响构建过程的可预测性,并可能与其他修改产生冲突。