首页指南参考教程

插件和模组

创建配置插件时了解什么是插件和模组。


插件是同步函数,接受 ExpoConfig 并返回修改后的 ExpoConfig

¥Plugins are synchronous functions that accept an ExpoConfig and return a modified ExpoConfig.

  • 插件应使用以下约定命名:with<Plugin Functionality>,例如 withFacebook

    ¥Plugins should be named using the following convention: with<Plugin Functionality>, for example, withFacebook.

  • 插件应该是同步的,并且它们的返回值应该是可序列化的,除了添加的任何 mods 之外。

    ¥Plugins should be synchronous and their return value should be serializable, except for any mods that are added.

  • 或者,可以将第二个参数传递给插件来配置它。

    ¥Optionally, a second argument can be passed to the plugin to configure it.

  • expo/config 方法 getConfig 读取配置时,始终会调用 plugins。然而,mods 仅在 npx expo prebuild 的 "syncing" 阶段被调用。

    ¥plugins are always invoked when the config is read by the expo/config method getConfig. However, the mods are only invoked during the "syncing" phase of npx expo prebuild.

创建一个插件

¥Create a plugin

这是最基本的配置插件的示例:

¥Here is an example of the most basic config plugin:

const withNothing = config => config;

假设你想创建一个向 iOS 项目中的 Info.plist 添加自定义值的插件:

¥Say you wanted to create a plugin that added custom values to Info.plist in an iOS project:

my-plugin.js
const withMySDK = (config, { apiKey }) => {
  if (!config.ios) {
    config.ios = {};
  }
  if (!config.ios.infoPlist) {
    config.ios.infoPlist = {};
  }

  config.ios.infoPlist['MY_CUSTOM_NATIVE_IOS_API_KEY'] = apiKey;

  return config;
};

module.exports.withMySDK = withMySDK;

要使用该插件,请导入它并封装配置:

¥To use the plugin, import it and wrap the config:

app.config.js
const { withMySDK } = require('./my-plugin');

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

module.exports = withMySDK(config, { apiKey: 'X-XXX-XXX' });

导入插件

¥Import a plugin

你可能想在不同的文件中创建插件,方法如下:

¥You may want to create a plugin in a different file, here's how:

  • 根文件可以是任何 JS 文件或 Node 模块根目录中名为 app.plugin.js 的文件。

    ¥The root file can be any JS file or a file named app.plugin.js in the root of a Node module.

  • 该文件应导出满足 ConfigPlugin 类型的函数。

    ¥The file should export a function that satisfies the ConfigPlugin type.

  • 应提前针对 Node 环境转换插件!

    ¥Plugins should be transpiled for Node environments ahead of time!

    • 他们应该支持 Expo 支持 (LTS) 的 Node 版本。

      ¥They should support the versions of Node that Expo supports (LTS).

    • 没有 import/export 关键字,请在附带的插件文件中使用 module.exports

      ¥No import/export keywords, use module.exports in the shipped plugin file.

    • Expo 仅转换用户的初始 app.config 文件,更多内容将需要一个打包器,它会为配置文件添加太多 "opinions"。

      ¥Expo only transpiles the user's initial app.config file, anything more would require a bundler which would add too many "opinions" for a config file.

考虑以下更改配置名称的示例:

¥Consider the following example that changes the config name:

app.config.jsExpo config
my-plugin.jsCustom Config Plugin file
my-plugin.js
module.exports = function withPrefixedName(config, prefix) {
  // Modify the config
  config.name = prefix + '-' + config.name;
  // Return the results
  return config;
};
app.config.js
{
  "name": "my-app",
  "plugins": [["./my-plugin", "custom"]]
}

其计算结果为以下 JSON 配置:

¥It evaluates to the following JSON config:

Evaluated config JSON
{
  "name": "custom-my-app",
  "plugins": [["./my-plugin", "custom"]]
}

链插件

¥Chain plugins

一旦添加了一些插件,你的 app.config.js 代码就会变得难以阅读和操作。为了解决这个问题,expo/config-plugins 提供了 withPlugins 函数,可用于将插件链接在一起并按顺序执行它们。

¥Once you add a few plugins, your app.config.js code can become difficult to read and manipulate. To combat this, expo/config-plugins provides a withPlugins function which can be used to chain plugins together and execute them in order.

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

// ❌ Hard to read
withDelta(withFoo(withBar(config, 'input 1'), 'input 2'), 'input 3');

// ✅ Easy to read
import { withPlugins } from 'expo/config-plugins';

withPlugins(config, [
  [withBar, 'input 1'],
  [withFoo, 'input 2'],
  // When no input is required, you can just pass the method...
  withDelta,
]);

为了支持 JSON 配置,我们还添加了 plugins 数组,该数组在底层仅使用 withPlugins。这是与上面相同的配置,但更简单:

¥To support JSON configs, we also added the plugins array which just uses withPlugins under the hood. Here is the same config as above, but even simpler:

app.config.js
export default {
  name: 'my app',
  plugins: [
    [withBar, 'input 1'],
    [withFoo, 'input 2'],
    [withDelta, 'input 3'],
  ],
};

什么是模组

¥What are mods

修饰符(简称 mod)是一个异步函数,它接受配置和数据对象,然后将两者作为对象进行操作和返回。

¥A modifier (mod for short) is an async function that accepts a config and a data object, then manipulates and returns both as an object.

Mod 被添加到应用配置的 mods 对象中。mods 对象与应用配置的其余部分不同,因为它在初始读取后不会被序列化,这意味着你可以使用它在代码生成期间执行操作。如果可能的话,你应该尝试使用基本插件而不是模组,因为它们更容易使用。

¥Mods are added to the mods object of the app config. The mods object is different from the rest of the app config because it doesn't get serialized after the initial reading, which means you can use it to perform actions during code generation. If possible, you should attempt to use basic plugins instead of mods, as they're simpler to work with.

  • mods 从清单中省略,无法通过 Updates.manifest 访问。Mod 存在的唯一目的是在代码生成期间修改原生项目文件!

    ¥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 可用于在 npx expo prebuild 命令期间安全地读写文件。这就是 Expo CLI 修改 Info.plist、权利、xcproj 等的方式。

    ¥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 是特定于平台的,应始终添加到特定于平台的对象中:

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

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

模组工作原理

¥How mods work

  • 使用 getPrebuildConfig@expo/prebuild-config 读取配置。

    ¥The config is read using getPrebuildConfig from @expo/prebuild-config.

  • Expo 支持的所有核心功能都是通过 withIosExpoPlugins 中的插件添加的。这包括名称、版本、图标、区域设置等。

    ¥All of the core functionality supported by Expo is added via plugins in withIosExpoPlugins. This includes name, version, icons, locales, and so on.

  • 配置被传递给编译器 compileModsAsync

    ¥The config is passed to the compiler compileModsAsync

  • 编译器添加了负责读取数据(如 Info.plist)、执行命名 mod(如 mods.ios.infoPlist)的基本 mod,然后将结果写入文件系统。

    ¥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.

  • 编译器迭代所有 mod 并异步评估它们,提供一些基本属性,例如 projectRoot

    ¥The compiler iterates over all the mods and asynchronously evaluates them, providing some base props like the projectRoot.

    • 在每个 mod 之后,错误处理都会断言 mod 链是否被无效 mod 损坏。

      ¥After each mod, error handling asserts if the mod chain was corrupted by an invalid mod.

默认模组

¥Default mods

mod 编译器为常见文件操作提供了以下默认 mod。

¥The following default mods are provided by the mod compiler for common file manipulation.

危险的修改依赖正则表达式(regex)来修改应用代码,这可能会导致构建中断。正则表达式 mod 也很难进行版本控制,因此应谨慎使用。始终选择使用应用代码来修改应用代码,即 Expo 模块 原生 API。

¥Dangerous modifications rely on regular expressions (regex) to modify application code, which may cause the build to break. Regex mods are also difficult to version, and therefore should be used sparingly. Always opt towards using application code to modify application code, that is, Expo Modules native API.

安卓模组危险的描述
mods.android.manifest*修改 android/app/src/main/AndroidManifest.xml 为 JSON(用 xml2js 解析)。
mods.android.strings*修改 android/app/src/main/res/values/strings.xml 为 JSON(用 xml2js 解析)。
mods.android.colors*将 android/app/src/main/res/values/colors.xml 修改为 JSON(使用 xml2js 解析)。
mods.android.colorsNight*将 android/app/src/main/res/values-night/colors.xml 修改为 JSON(使用 xml2js 解析)。
mods.android.styles*修改 android/app/src/main/res/values/styles.xml 为 JSON(用 xml2js 解析)。
mods.android.gradleProperties*修改 android/gradle.properties 为 Properties.PropertiesItem[]
mods.android.mainActivity将 android/app/src/main/<package>/MainActivity.java 修改为字符串。
mods.android.mainApplication将 android/app/src/main/<package>/MainApplication.java 修改为字符串。
mods.android.appBuildGradle将 android/app/build.gradle 修改为字符串。
mods.android.projectBuildGradle将 android/build.gradle 修改为字符串。
mods.android.settingsGradle将 android/settings.gradle 修改为字符串。
iOS 模组危险的描述
mods.ios.infoPlist*修改 ios/<name>/Info.plist 为 JSON(用 @expo/plist 解析)。
mods.ios.entitlements*将 ios/<name>/<product-name>.entitlements 修改为 JSON(用 @expo/plist 解析)。
mods.ios.expoPlist*将 ios/<ame>/Expo.plist 修改为 JSON(Expo 更新 iOS 的配置)(用 @expo/plist 解析)。
mods.ios.xcodeproj*修改 ios/<name>.xcodeproj 为 XcodeProject 对象(用 xcode 解析)。
mods.ios.podfile*将 ios/Podfile 修改为字符串。
mods.ios.podfileProperties*将 ios/Podfile.properties.json 修改为 JSON。
mods.ios.appDelegate将 ios/<name>/AppDelegate.m 修改为字符串。

解析 mod 后,每个 mod 的内容将被写入磁盘。可以添加自定义默认 mods 以支持新的原生文件。例如,你可以创建一个 mod 来支持 GoogleServices-Info.plist,并将其传递给其他 mod。

¥After the mods are resolved, the contents of each mod will be written to disk. Custom default 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.

模组插件

¥Mod plugins

Mod 负责很多任务,所以一开始它们可能很难理解。如果你正在开发需要模组的功能,最好不要直接与它们交互。

¥Mods are responsible for a lot of tasks, so they can be pretty difficult to understand at first. If you're developing a feature that requires mods, it's best not to interact with them directly.

相反,你应该使用 expo/config-plugins 提供的辅助模块:

¥Instead you should use the helper mods provided by expo/config-plugins:

安卓

¥Android

安卓模组模组插件危险的
mods.android.manifestwithAndroidManifest*
mods.android.stringswithStringsXml*
mods.android.colorswithAndroidColors*
mods.android.colorsNightwithAndroidColorsNight*
mods.android.styleswithAndroidStyles*
mods.android.gradlePropertieswithGradleProperties*
mods.android.mainActivitywithMainActivity
mods.android.mainApplicationwithMainApplication
mods.android.appBuildGradlewithAppBuildGradle
mods.android.projectBuildGradlewithProjectBuildGradle
mods.android.settingsGradlewithSettingsGradle

iOS 系统

¥iOS

iOS 模组模组插件危险的
mods.ios.infoPlistwithInfoPlist*
mods.ios.entitlementswithEntitlementsPlist*
mods.ios.expoPlistwithExpoPlist*
mods.ios.xcodeprojwithXcodeProject*
mods.ios.podfilewithPodfile*
mods.ios.podfilePropertieswithPodfileProperties*
mods.ios.appDelegatewithAppDelegate

mod 插件传递一个 config 对象,并添加了附加属性 modResultsmodRequest

¥A mod plugin gets passed a config object with additional properties modResults and modRequest added to it.

  • modResults:要修改并返回的对象。类型取决于正在使用的模组。

    ¥modResults: The object to modify and return. The type depends on the mod that's being used.

  • modRequest:mod 编译器提供的附加属性。

    ¥modRequest: Additional properties supplied by the mod compiler.

    • projectRoot: string:通用应用的项目根目录。

      ¥projectRoot: string: Project root directory for the universal app.

    • platformProjectRoot: string:特定平台的项目根。

      ¥platformProjectRoot: string: Project root for the specific platform.

    • modName: string:模组名称。

      ¥modName: string: Name of the mod.

    • platform: ModPlatform:mods 配置中使用的平台名称。

      ¥platform: ModPlatform: Name of the platform used in the mods config.

    • projectName?: string:(仅限 iOS)用于查询项目文件的路径组件。ex.projectRoot/ios/[projectName]/

      ¥projectName?: string: (iOS only) The path component used for querying project files. ex. projectRoot/ios/[projectName]/

创建一个模组

¥Create a mod

假设你想编写一个 mod 来更新 Xcode 项目的 "产品名称":

¥Say you wanted to write a mod to update the Xcode Project's "product name":

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

mod 系统的某些部分尚未完全充实,这些部分使用 withDangerousMod 在没有基础 mod 的情况下读取/写入数据。这些方法本质上充当它们自己的基础 mod 并且无法扩展。例如,图标目前使用危险模式来执行单个生成步骤,无法自定义结果。

¥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. Icons, for example, currently use the dangerous mod to perform a single generation step with no ability to customize the results.

my-config-plugin.js
export const withIcons = config => {
  return withDangerousMod(config, [
    'ios',
    async config => {
      await setIconsAsync(config, config.modRequest.projectRoot);
      return config;
    },
  ]);
};

请小心使用 withDangerousMod,因为它将来可能会发生变化。它的执行顺序也不可靠。目前,危险模组先于所有其他修饰符运行,这是因为我们在内部使用危险模组进行大型文件系统重构,例如当包名称更改时。

¥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, this is because we use dangerous mods internally for large file system refactoring like when the package name changes.

插件模块解析

¥Plugin module resolution

传递给 plugins 数组的字符串可以通过几种不同的方式解析。

¥The strings passed to the plugins array can be resolved in a few different ways.

下面未指定的任何解决模式都是意外行为,并且可能会发生重大更改。

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

项目文件

¥Project file

你可以在项目中快速创建插件并在配置中使用它。

¥You can quickly create a plugin in your project and use it in your config.

app.config.jsimport "./my-config-plugin"
my-config-plugin.js Imported from config

在此示例中,配置插件文件包含最低限度的功能:

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

my-config-plugin.js
module.exports = config => config;

app.plugin.js

有时你希望你的包导出 React 组件并支持插件。为此,需要使用多个入口点,因为转译(Babel 预设)可能不同。如果 Node 模块文件夹的根目录中存在 app.plugin.js 文件,则将使用该文件而不是包的 main 文件。

¥Sometimes you want your package to export React components and also support a plugin. To do this, multiple entry points need to be used because the transpilation (Babel preset) may be different. If an app.plugin.js file is present in the root of a Node module's folder, it'll be used instead of the package's main file.

app.config.jsimport "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
node_modules/expo-splash-screen/app.plugin.js
module.exports = config => config;

节点模块默认文件

¥Node module default file

节点模块中的配置插件(没有 app.plugin.js)将使用 package.json 中定义的 main 文件。

¥A config plugin in a node module (without an app.plugin.js) will use the main file defined in the package.json.

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

项目文件夹

¥Project folder

这与 Node 模块中的配置插件的工作方式不同,因为默认情况下不会在目录中解析 app.plugin.js。你必须手动指定 ./my-config-plugin/app.plugin.js 才能使用它,否则将使用目录中的 index.js。

¥This is different to how Config Plugins in Node modules work because app.plugin.js won't be resolved by default in a directory. You'll have to manually specify ./my-config-plugin/app.plugin.js to use it, otherwise index.js in the directory will be used.

app.config.jsimport "./my-config-plugin"
my-config-plugin
index.js Config Plugin
app.plugin.js Skipped outside of a node module

模块内部结构

¥Module internals

避免导入模块内部结构。

如果直接导入 Node 模块内的文件,则将跳过该模块的根 app.plugin.js 解析。这称为 "到达包内部",被认为是不好的形式。我们支持这一点是为了使测试和插件创作更容易,但我们不希望库作者将他们的插件像这样公开为公共 API。

¥If a file inside a Node module is directly imported, then the module's root app.plugin.js resolution will be skipped. This is referred to as "reaching inside a package" and is considered bad form. We support this to make testing, and plugin authoring easier, but we don't expect library authors to expose their plugins like this as a public API.

app.config.jsimport "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

原始函数

¥Raw functions

Expo 配置对象还支持将函数按原样传递到 plugins 数组。这对于测试很有用,或者如果你想在不创建文件的情况下使用插件。

¥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.js
const withCustom = (config, props) => config;

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

使用函数而不是字符串的一个警告是序列化会将函数替换为函数的名称。这可以使清单(有点像应用的 index.html)按预期工作。

¥One caveat to using functions instead of strings is that serialization will replace the function with the function's name. This keeps manifests (kinda like the index.html for your app) working as expected.

序列化配置如下所示:

¥Here is what the serialized config would look like:

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

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

¥Why app.plugin.js for plugins

当 Node 模块 ID 作为插件提供时,配置解析首先搜索名为 app.plugin.js 的文件。这是因为 Node 环境通常与 Android、iOS 或 Web JS 环境不同,因此需要不同的转译预设(例如,module.exports 而不是 import/export)。

¥Config resolution searches for a file named app.plugin.js first when a Node module ID is provided as a plugin. This is because Node environments are often different to Android, iOS, or web JS environments and therefore require different transpilation presets (for example, module.exports instead of import/export).

由于这个原因,我们会搜索 Node 模块的根目录,而不是紧挨着 index.js。想象一下,你有一个 TypeScript Node 模块,其中转译的主文件位于 build/index.js,如果应用配置插件解析搜索 build/app.plugin.js,你将失去以不同方式转译文件的能力。

¥Because of this reasoning, the root of a Node module is searched instead of right next to the index.js. Imagine you had a TypeScript Node module where the transpiled main file was located at build/index.js, if app config plugin resolution searched for build/app.plugin.js you'd lose the ability to transpile the file differently.

Expo 中文网 - 粤ICP备13048890号