Mods

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


本指南解释了什么是 mod 和 mod 插件、它们的工作原理以及如何在为你的 Expo 项目创建配置插件时有效地使用它们。

¥This guide explains what mods and mod plugins are, how they work, and how to use them effectively when creating config plugins for your Expo project.

在本指南中,通过下图,你将了解配置插件层次结构的后两部分:

¥Using the diagram below, in this guide, you will learn the last two parts of the config plugin hierarchy:

模组插件

¥Mod plugins

Mod 插件提供了一种在预构建过程中修改原生项目文件的方法。它们由 expo/config-plugins 库提供,封装了顶层 mod(也称为默认 mods),因为顶层 mod 是平台相关的,并且执行各种一开始可能难以理解的任务。

¥Mod plugins provide a way to modify native project files during the prebuild process. They are made available from expo/config-plugins library and wrap top-level mods (also known as default mods) because top-level mods are platform-specific and perform various tasks that can be difficult to understand at first.

提示:如果你正在开发需要 mod 的功能,则应该使用 mod 插件,而不是直接与顶层 mod 交互。

可用的 mod 插件

¥Available mod plugins

expo/config-plugins 库中提供以下 mod 插件:

¥The following mod plugins are available in the expo/config-plugins library:

安卓

¥Android

默认 Android 版本模组插件危险的描述
mods.android.manifestwithAndroidManifest(示例)*将 android/app/src/main/AndroidManifest.xml 修改为 JSON(使用 xml2js 解析)
mods.android.stringswithStringsXml(示例)*修改 android/app/src/main/res/values/strings.xml 为 JSON(用 xml2js 解析)。
mods.android.colorswithAndroidColors(示例)*将 android/app/src/main/res/values/colors.xml 修改为 JSON(使用 xml2js 解析)。
mods.android.colorsNightwithAndroidColorsNight(示例)*将 android/app/src/main/res/values-night/colors.xml 修改为 JSON(使用 xml2js 解析)。
mods.android.styleswithAndroidStyles(示例)*修改 android/app/src/main/res/values/styles.xml 为 JSON(用 xml2js 解析)。
mods.android.gradlePropertieswithGradleProperties(示例)*修改 android/gradle.properties 为 Properties.PropertiesItem[]
mods.android.mainActivitywithMainActivity(示例)将 android/app/src/main/<package>/MainActivity.java 修改为字符串。
mods.android.mainApplicationwithMainApplication(示例)将 android/app/src/main/<package>/MainApplication.java 修改为字符串。
mods.android.appBuildGradlewithAppBuildGradle(示例)将 android/app/build.gradle 修改为字符串。
mods.android.projectBuildGradlewithProjectBuildGradle(示例)将 android/build.gradle 修改为字符串。
mods.android.settingsGradlewithSettingsGradle(示例)将 android/settings.gradle 修改为字符串。

iOS 系统

¥iOS

默认 iOS 版本模组插件危险的描述
mods.ios.infoPlistwithInfoPlist(示例)*修改 ios/<name>/Info.plist 为 JSON(用 @expo/plist 解析)。
mods.ios.entitlementswithEntitlementsPlist(示例)*将 ios/<name>/<product-name>.entitlements 修改为 JSON(用 @expo/plist 解析)。
mods.ios.expoPlistwithExpoPlist(示例)*将 ios/<name>/Expo.plist 修改为 JSON(Expo 更新 iOS 配置)(使用 @expo/plist 解析)。
mods.ios.xcodeprojwithXcodeProject(示例)*修改 ios/<name>.xcodeproj 为 XcodeProject 对象(用 xcode 解析)。
mods.ios.podfilewithPodfile (示例*将 ios/Podfile 修改为字符串。
mods.ios.podfilePropertieswithPodfileProperties(示例)*将 ios/Podfile.properties.json 修改为 JSON。
mods.ios.appDelegatewithAppDelegate(示例)将 ios/<name>/AppDelegate.m 修改为字符串。
关于默认 Android 和 iOS 模块的说明: 默认模块由模块编译器提供,用于常见的文件操作。危险的修改依赖正则表达式(regex)来修改应用代码,这可能会导致构建中断。正则表达式 mod 也很难进行版本控制,因此应谨慎使用。始终选择使用应用代码来修改应用代码,即 Expo 模块 原生 API。

Mods

配置插件使用 mods(修改器的缩写)在预构建过程中修改原生项目文件。模块是异步函数,允许你更改平台特定文件(例如 AndroidManifest.xml 和 Info.plist)以及其他原生配置文件,而无需手动编辑它们。它们仅在 npx expo prebuild 的同步阶段(预构建过程)执行。

¥Config plugins use mods (short for modifiers) to modify native project files during the prebuild process. Mods are asynchronous functions that allow you to make changes to platform-specific files such as AndroidManifest.xml and Info.plist, and other native configuration files without having to manually edit them. They execute only during the syncing phase of npx expo prebuild (prebuild process).

它们接受一个配置和一个数据对象,然后修改它们并将其作为单个对象返回。例如,在原生项目中,mods.android.manifest 修改 AndroidManifest.xml,mods.ios.plist 修改 Info.plist。

¥They accept a config and a data object, then modify and return both of them as a single object. For example, in native projects, mods.android.manifest modifies AndroidManifest.xml and mods.ios.plist modifies Info.plist.

你不能在配置插件中直接将 mods 用作顶层函数(例如 with.android.manifest)。当你需要使用模组时,你可以在配置插件中使用模组插件。这些 mod 插件由 expo/config-plugins 库提供,封装了顶层 mod 函数,并在后台执行各种任务。要查看可用模组列表,请查看 修改 expo/config-plugins 提供的插件

¥You don't use mods as top-level functions (for example with.android.manifest) directly in your config plugin. When you need to use a mod, you use mod plugins in your config plugins. These mod plugins are provided by the expo/config-plugins library and wrap top-level mod functions and behind the scenes they perform various tasks. To see a list of available mods, check out the mod plugins provided by expo/config-plugins.

How default mods work and their key characteristics

When a default mod resolves, it is added to the mods object of the app config. This mods object is different from the rest of the app config because it doesn't get serialized, which means you can use it to perform actions during code generation. Whenever possible, you should use available mod plugins instead of default mods since they are easier to work with.

Here is a high-level overview of how default mods work:

  • The config is read using getPrebuildConfig from @expo/prebuild-config
  • All of the core functionality supported by Expo is added via plugins in withIosExpoPlugins. This includes name, version, icons, locales, and so on.
  • The config is passed to the compiler compileModsAsync
  • The compiler adds base mods that are responsible for reading data (like Info.plist), executing a named mod (like mods.ios.infoPlist), then writing the results to the file system
  • The compiler iterates over all the mods and asynchronously evaluates them, providing some base props like the projectRoot
    • After each mod, error handling asserts if the mod chain was corrupted by an invalid mod

Here are some key characteristics of default mods:

  • mods are omitted from the manifest and cannot be accessed via Updates.manifest. Mods exist for the sole purpose of modifying native project files during code generation!

  • mods can be used to read and write files safely during the npx expo prebuild command. This is how Expo CLI modifies the Info.plist, entitlements, xcproj, and so on.

  • mods are platform-specific and should always be added to a platform-specific object:

app.config.ts
  module.exports = {
    name: 'my-app',
    mods: {
      ios: {
        /* iOS mods... */
      },
      android: {
        /* Android mods... */
      },
    },
  };

After mods are resolved, the contents of each mod will be written to disk. Custom mods can be added to support new native files. For example, you can create a mod to support the GoogleServices-Info.plist, and pass it to other mods.

How mod plugins work

When a mod plugin is executed, it gets passed a config object with additional properties: modResults and modRequest.

modResults

The modResults object contains the data to modify and return. Its type depends on the mod that's being used.

modRequest

The modRequest object contains the following additional properties supplied by the mod compiler.

PropertyTypeDescription
projectRootstringProject root directory for the universal app.
platformProjectRootstringProject root for the specific platform.
modNamestringName of the mod.
platformModPlatformName of the platform used in the mods config.
projectNamestring(iOS only) The path component used for querying project files. For example, projectRoot/ios/[projectName]/.

Create your own mod

For example, if you want to write a mod to update the Xcode Project's "product name", you'll create a config plugin file that uses the withXcodeProject mod plugin.

my-config-plugin.ts
import { ConfigPlugin, withXcodeProject, IOSConfig } from 'expo/config-plugins';

const withCustomProductName: ConfigPlugin<string> = (config, customName) => {
  return withXcodeProject(
    config,
    async (
      config
    ) => {
      config.modResults = IOSConfig.Name.setProductName({ name: customName }, config.modResults);
      return config;
    }
  );
};

// Usage:

/// Create a config
const config = {
  name: 'my app',
};

/// Use the plugin
export default withCustomProductName(config, 'new_name');

Experimental functionality

Some parts of the mod system aren't fully fleshed out. These parts use withDangerousMod to read/write data without a base mod. These methods essentially act as their own base mod and cannot be extended. For example, Icons currently use the dangerous mod to perform a single generation step with no ability to customize the results.

my-config-plugin.ts
import { ExpoConfig } from 'expo/config';

export const withIcons = (config: ExpoConfig): ExpoConfig => {
  return withDangerousMod(config, [
    'ios',
    async (config: ExpoConfig): Promise<ExpoConfig> => {
      await setIconsAsync(config, config.modRequest.projectRoot);
      return config;
    },
  ]);
};

Be careful using withDangerousMod as it is subject to change in the future. The order with which it gets executed is not reliable either. Currently, dangerous mods run first before all other modifiers because Expo uses dangerous mods internally for large file system refactoring like when a library's name changes.

Plugin module resolution

When implementing plugins, there are two fundamental approaches to consider:

  1. Plugins defined within your app's project: These plugins live locally within your project, making them easy to customize and maintain alongside your app's code. They are ideal for project-specific customizations.

  2. Standalone package plugins: These plugins exist as separate packages and are published to npm. This approach is ideal for reusable plugins that can be shared across multiple projects.

Both approaches provide the same capabilities for modifying your native configuration, but differ in how they're structured and imported. The sections below explain how module resolution works for each approach.

Any resolution pattern that isn't specified below is unexpected behavior, and subject to breaking changes.

Plugins defined within your app's project

With plugins defined within your app's project, you can implement plugins directly in your project in several ways:

File import

You can quickly create a plugin in your project by creating a JavaScript/TypeScript file and use it in your config like any other JS/TS file.

app.config.tsimport "./my-config-plugin"
my-config-plugin.ts Imported from config

In the above example, the config plugin file contains a bare minimum function:

my-config-plugin.ts
module.exports = ({ config }: { config: ExpoConfig }) => {};

Inline function inside of dynamic app config

Expo config objects also support passing functions as-is to the plugins array. This is useful for testing, or if you want to use a plugin without creating a file.

app.config.ts
const withCustom = (config, props) => config;

const config = {
  plugins: [
    [
      withCustom,
      {
        /* props */
      },
    ],
    withCustom,
  ],
};

One caveat to using functions instead of strings is that serialization will replace the function with the function's name. This keeps manifests (kind of like the index.html for your app) working as expected. Here is what the serialized config would look like:

{
  "plugins": [["withCustom", {}], "withCustom"]
}

Standalone package plugins

See Create a module with a config plugin for a step-by-step guide on how to create a standalone package plugin.

Standalone package plugins can be implemented in two ways:

1. Dedicated config plugin packages

These are npm packages whose sole purpose is to provide a config plugin. For a dedicated config plugin package, you can export your plugin using app.plugin.js:

app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  app.plugin.js Entry file for custom plugins
  build
   index.js Skipped in favor of app.plugin.js

2. Config plugins with companion packages

When a config plugin is part of a Node module without an app.plugin.js, it uses the package's main entry point:

app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  package.json"main": "./build/index.js"
  build
   index.js Node resolve to this file

Plugin resolution order

When you import a plugin package, files are resolved in this specific order:

  1. app.plugin.js in package root
app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  package.json"main": "./build/index.js"
  app.plugin.js Entry file for custom plugins
  build
   index.js Skipped in favor of app.plugin.js
  1. Package's main entry (from package.json)
app.config.tsimport "expo-splash-screen"
node_modules
expo-splash-screenNode module
  package.json"main": "./build/index.js"
  build
   index.js Node resolve to this file
  1. Direct internal imports (not recommended)

错误:避免直接导入模块内部代码,因为它会绕过标准解析顺序,并且可能会在将来的更新中中断。

¥error Avoid importing module internals directly as it bypasses the standard resolution order and may break in future updates.

app.config.tsimport "expo-splash-screen/build/index.js"
node_modules
expo-splash-screen
  package.json"main": "./build/index.js"
  app.plugin.js Ignored due to direct import
  build
   index.js expo-splash-screen/build/index.js

为什么使用 app.plugin.js 插件

¥Why use app.plugin.js for plugins

app.plugin.js 方法是配置插件的首选,因为它允许与主包代码使用不同的转译设置。这一点尤为重要,因为 Node 环境通常需要与 Android、iOS 或 Web JS 环境不同的转译预设(例如,module.exports 而不是 import/export)。

¥The app.plugin.js approach is preferred for config plugins as it allows different transpilation settings from the main package code. This is particularly important because Node environments often require different transpilation presets compared to Android, iOS, or web JS environments (for example, module.exports instead of import/export).