首页指南参考教程

开发和调试插件

了解 Expo 配置插件的开发最佳实践和调试技术。


开发插件是扩展 Expo 生态系统的好方法。然而,有时你会想要调试你的插件。本页面提供了一些开发和调试插件的最佳实践。

¥Developing a plugin is a great way to extend the Expo ecosystem. However, there are times you'll want to debug your plugin. This page provides some of the best practices for developing and debugging a plugin.

开发一个插件

¥Develop a plugin

使用 修改器预览 实时调试插件的结果。

¥Use modifier previews to debug the results of your plugin live.

为了使插件开发更容易,我们在 expo-module-scripts 中添加了插件支持。有关使用 TypeScript 和 Jest 构建插件的更多信息,请参阅 配置插件指南

¥To make plugin development easier, we've added plugin support to expo-module-scripts. Refer to the config plugins guide for more info on using TypeScript, and Jest to build plugins.

安装依赖

¥Install dependencies

在提供配置插件的库中使用以下依赖:

¥Use the following dependencies in a library that provides a config plugin:

package.json
{
  "dependencies": {},
  "devDependencies": {
    "expo": "^47.0.0"
  },
  "peerDependencies": {
    "expo": ">=47.0.0"
  },
  "peerDependenciesMeta": {
    "expo": {
      "optional": true
    }
  }
}
  • 你可以更新 expo 的确切版本以针对特定版本进行构建。

    ¥You may update the exact version of expo to build against a specific version.

  • 对于依赖于核心、稳定 API 的简单配置插件(例如仅修改 AndroidManifest.xml 或 Info.plist 的插件),你可以使用松散依赖,如上例所示。

    ¥For simple config plugins that depend on core, stable APIs, such as a plugin that only modifies AndroidManifest.xml or Info.plist, you can use a loose dependency such as in the example above.

  • 你可能还想安装 expo-module-scripts 作为开发依赖,但这不是必需的。

    ¥You may also want to install expo-module-scripts as a development dependency, but it's not required.

导入配置插件包

¥Import the config plugins package

expo/config-pluginsexpo/config 包是从 expo 包重新导出的。

¥The expo/config-plugins and expo/config packages are re-exported from the expo package.

const { %%placeholder-start%%...%%placeholder-end%%/* @end */ } = require('expo/config-plugins');
const { %%placeholder-start%%...%%placeholder-end%%/* @end */ } = require('expo/config');

通过 expo 包导入可确保你使用 expo 包所依赖的 expo/config-pluginsexpo/config 包的版本。

¥Importing through the expo package ensures that you are using the version of the expo/config-plugins and expo/config packages that are depended on by the expo package.

如果你不通过这种方式通过 expo 重新导出导入包,你可能会意外导入不兼容的版本(取决于使用该模块的开发者使用的包管理器中模块提升的实现细节)或者无法 完全导入模块(如果使用包管理器(例如 Yarn Berry 或 pnpm)的 "即插即用" 功能)。

¥If you do not import the package through the expo re-export in this way, you may accidentally be importing an incompatible version (depending on the implementation details of module hoisting in the package manager used by the developer consuming the module) or be unable to import the module at all (if using "plug and play" features of a package manager such as Yarn Berry or pnpm).

配置类型直接从 expo/config 导出,因此无需安装或从 expo/config-types 导入:

¥Config types are exported directly from expo/config, so there is no need to install or import from expo/config-types:

import { ExpoConfig, ConfigContext } from 'expo/config';

Mod 的最佳实践

¥Best practices for mods

  • 避免使用正则表达式:静态修改 是关键。如果你想修改 Android gradle 文件中的值,请考虑使用 gradle.properties。如果你想修改 Podfile 中的某些代码,请考虑写入 JSON 并让 Podfile 读取静态值。

    ¥Avoid regex: static modification is key. If you want to modify a value in an Android gradle file, consider using gradle.properties. If you want to modify some code in the Podfile, consider writing to JSON and having the Podfile read the static values.

  • 避免执行长时间运行的任务,例如发出网络请求或在 mod 中安装 Node 模块。

    ¥Avoid performing long-running tasks like making network requests or installing Node modules in mods.

  • 不要在 mod 中添加交互式终端提示。

    ¥Do not add interactive terminal prompts in mods.

  • 仅在危险模组中生成、移动和删除新文件。否则将会破坏 introspection

    ¥Generate, move, and delete new files in dangerous mods only. Failing to do so will break introspection.

  • 利用 withXcodeProject 等内置配置插件来最大限度地减少文件读取和解析的次数。

    ¥Utilize built-in config plugins like withXcodeProject to minimize the amount of times a file is read and parsed.

  • 坚持使用预构建内部使用的 XML 解析库,这有助于防止不必要地重新排列代码的更改。

    ¥Stick with the XML parsing libraries that prebuild uses internally, this helps prevent changes where code is rearranged needlessly.

工具

¥Tooling

我们强烈建议安装 Expo 工具 VS Code 扩展,因为这将对插件执行自动验证并显示错误信息以及配置插件开发的其他生活质量改进。

¥We highly recommend installing the Expo Tools VS Code extension as this will perform automatic validation on the plugins and surface error information along with other quality of life improvements for Config Plugin development.

设置在线运行环境

¥Setup up a playground environment

你可以使用 JS 轻松开发插件,但如果你想设置 Jest 测试并使用 TypeScript,你将需要一个 monorepo。

¥You can develop plugins easily using JS, but if you want to setup Jest tests and use TypeScript, you will want a monorepo.

monorepo 将使你能够处理节点模块并将其导入你的应用配置中,就像将其发布到 npm 一样。Expo 配置插件内置完整的 monorepo 支持,因此你所需要做的就是设置一个项目。

¥A monorepo will enable you to work on a node module and import it in your app config like you would if it were published to npm. Expo config plugins have full monorepo support built-in so all you need to do is setup a project.

在 monorepo 的 packages/ 文件夹中,创建一个模块,并在其中创建 引导配置插件

¥In your monorepo's packages/ folder, create a module, and bootstrap a config plugin in it.

手动运行插件

¥Manually run a plugin

如果你不习惯设置 monorepo,可以尝试手动运行插件:

¥If you aren't comfortable setting up a monorepo, you can try manually running a plugin:

  • 使用配置插件运行包中的 npm pack

    ¥Run npm pack in the package with the config plugin

  • 在你的测试项目中,运行 npm install path/to/react-native-my-package-1.0.0.tgz,这会将包添加到你的 package.json dependencies 对象中。

    ¥In your test project, run npm install path/to/react-native-my-package-1.0.0.tgz, this will add the package to your package.json dependencies object.

  • 将包添加到 app.json 中的 plugins 数组中:{ "plugins": ["react-native-my-package"] }

    ¥Add the package to the plugins array in your app.json: { "plugins": ["react-native-my-package"] }

    • 如果你安装了 VS Code Expo 工具,自动补齐功能应该适用于该插件。

      ¥If you have VS Code Expo Tools installed, autocomplete should work for the plugin.

  • 如果需要更新包,请更改包的 package.json 中的 version 并重复该过程。

    ¥If you need to update the package, change the version in the package's package.json and repeat the process.

修改 AndroidManifest.xml

¥Modify AndroidManifest.xml

在使用配置插件之前,包应尝试使用内置的 AndroidManifest.xml 合并系统。这可用于静态的、非可选的功能,例如权限。这将确保功能在构建时而不是预构建时合并,从而最大限度地减少用户忘记预构建的可能性。缺点是用户无法使用 introspection 预览更改并调试任何潜在问题。

¥Packages should attempt to use the built-in AndroidManifest.xml merging system before using a config plugin. This can be used for static, non-optional features like permissions. This will ensure features are merged during build-time and not prebuild-time, which minimizes the possibility of users forgetting to prebuild. The drawback is that users cannot use introspection to preview the changes and debug any potential issues.

下面是一个包的 AndroidManifest.xml 示例,它注入了所需的权限:

¥Here is an example of a package's AndroidManifest.xml, which injects a required permission:

AndroidManifest.xml
<manifest package="expo.modules.filesystem" xmlns:android="http://schemas.android.com/apk/res/android">
  <uses-permission android:name="android.permission.INTERNET"/>
</manifest>

如果你正在为本地项目构建插件,或者你的包需要更多控制,那么你应该实现一个插件。

¥If you're building a plugin for your local project, or if your package needs more control, then you should implement a plugin.

你可以使用内置类型和助手来简化处理复杂对象的过程。下面是将 <meta-data android:name="..." android:value="..."/> 添加到默认 <application android:name=".MainApplication" /> 的示例。

¥You can use built-in types and helpers to ease the process of working with complex objects. Here's an example of adding a <meta-data android:name="..." android:value="..."/> to the default <application android:name=".MainApplication" />.

my-config-plugin.ts
import { AndroidConfig, ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
import { ExpoConfig } from 'expo/config';

// Using helpers keeps error messages unified and helps cut down on XML format changes.
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;

export const withMyCustomConfig: ConfigPlugin = config => {
  return withAndroidManifest(config, async config => {
    // Modifiers can be async, but try to keep them fast.
    config.modResults = await setCustomConfigAsync(config, config.modResults);
    return config;
  });
};

// Splitting this function out of the mod makes it easier to test.
async function setCustomConfigAsync(
  config: Pick<ExpoConfig, 'android'>,
  androidManifest: AndroidConfig.Manifest.AndroidManifest
): Promise<AndroidConfig.Manifest.AndroidManifest> {
  const appId = 'my-app-id';
  // Get the <application /> tag and assert if it doesn't exist.
  const mainApplication = getMainApplicationOrThrow(androidManifest);

  addMetaDataItemToMainApplication(
    mainApplication,
    // value for `android:name`
    'my-app-id-key',
    // value for `android:value`
    appId
  );

  return androidManifest;
}

修改 Info.plist

¥Modify Info.plist

使用 withInfoPlist 比静态修改 app.json 中的 expo.ios.infoPlist 对象更安全,因为它会读取 Info.plist 的内容并将其与 expo.ios.infoPlist 合并,这意味着你可以尝试防止更改被覆盖。

¥Using the withInfoPlist is a bit safer than statically modifying the expo.ios.infoPlist object in the app.json because it reads the contents of the Info.plist and merges it with the expo.ios.infoPlist, this means you can attempt to keep your changes from being overwritten.

下面是将 GADApplicationIdentifier 添加到 Info.plist 的示例:

¥Here's an example of adding a GADApplicationIdentifier to the Info.plist:

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

// Pass `<string>` to specify that this plugin requires a string property.
export const withCustomConfig: ConfigPlugin<string> = (config, id) => {
  return withInfoPlist(config, config => {
    config.modResults.GADApplicationIdentifier = id;
    return config;
  });
};

修改 iOS Podfile

¥Modify iOS Podfile

iOS Podfile 是 CocoaPods(iOS 上的依赖管理器)的配置文件。它类似于 iOS 的 package.json。Podfile 是一个 Ruby 文件,这意味着你无法从 Expo 配置插件安全地修改它,而应该选择另一种方法,例如 Expo 自动链接 钩子。

¥The iOS Podfile is the config file for CocoaPods, the dependency manager on iOS. It is similar to package.json for iOS. The Podfile is a Ruby file, which means you cannot safely modify it from Expo config plugins and should opt for another approach, such as Expo Autolinking hooks.

我们确实公开了一种与 Podfile 安全交互的机制,但它非常有限。版本化的 模板 Podfile 被硬编码为从静态 JSON 文件 Podfile.properties.json 中读取,我们公开了一个 mod (ios.podfileProperties, withPodfileProperties) 来安全地从此文件中读取和写入。这由 expo-build-properties 使用并配置 JavaScript 引擎。

¥We do expose one mechanism for safely interacting with the Podfile, but it's very limited. The versioned template Podfile is hard coded to read from a static JSON file Podfile.properties.json, we expose a mod (ios.podfileProperties, withPodfileProperties) to safely read and write from this file. This is used by expo-build-properties and to configure the JavaScript engine.

添加插件到 pluginHistory

¥Add plugins to pluginHistory

创建 _internal.pluginHistory 是为了防止在从旧版未版本插件迁移到版本化插件时运行重复插件。

¥_internal.pluginHistory was created to prevent duplicate plugins from running while migrating from legacy UNVERSIONED plugins to versioned plugins.

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

// Keeping the name, and version in sync with it's package.
const pkg = require('my-cool-plugin/package.json');

const withMyCoolPlugin: ConfigPlugin = config => config;

// A helper method that wraps `withRunOnce` and appends items to `pluginHistory`.
export default createRunOncePlugin(
  // The plugin to guard.
  withMyCoolPlugin,
  // An identifier used to track if the plugin has already been run.
  pkg.name,
  // Optional version property, if omitted, defaults to UNVERSIONED.
  pkg.version
);

插件开发最佳实践

¥Plugin development best practices

  • 自述文件中的说明:如果插件与 React Native 模块绑定,那么你应该记录该包的手动设置说明。如果插件出现任何问题,用户仍然应该能够手动将包添加到他们的项目中。经常这样做可以帮助你找到减少设置的方法,从而可以简化插件。

    ¥Instructions in your README: If the plugin is tied to a React Native module, then you should document manual setup instructions for the package. If anything goes wrong with the plugin, users should still be able to manually add the package to their project. Doing this often helps you find ways to reduce the setup, which can lead to a simpler plugin.

    • 记录插件的可用属性,并指定插件是否可以在没有属性的情况下工作。

      ¥Document the available properties for the plugin, and specify if the plugin works without props.

    • 如果在多次运行预构建后可以使插件工作,那将是一个很大的优势!它可以改善开发者的体验,能够在没有 --clean 标志的情况下运行 npx expo prebuild 来同步更改。

      ¥If you can make your plugin work after running prebuild multiple times, that's a big plus! It can improve the developer experience to be able to run npx expo prebuild without the --clean flag to sync changes.

  • 命名约定:如果跨平台,请使用 withFeatureName。如果插件是平台特定的,请使用驼峰式命名法,在 "with" 后紧跟平台。例如,withAndroidSplashwithIosSplash。在驼峰式标识符中,对于 iOS 的大小写没有普遍认可,我们更喜欢这种风格,并建议你的配置插件也使用它。

    ¥Naming conventions: Use withFeatureName if cross-platform. If the plugin is platform specific, use a camel case naming with the platform right after "with". For example, withAndroidSplash, withIosSplash. There is no universally agreed upon casing for iOS in camel cased identifiers, we prefer this style and suggest using it for your config plugins too.

  • 利用内置插件:考虑 预构建配置 的内置插件。出于历史原因包含一些功能,例如自动复制和链接应用配置中定义的 谷歌服务文件 的功能。如果存在重叠,那么也许建议用户使用内置类型以使你的插件尽可能简单。

    ¥Leverage built-in plugins: Account for built-in plugins from the prebuild config. Some features are included for historical reasons, like the ability to automatically copy and link Google services files defined in the app config. If there is overlap, then maybe recommend the user uses the built-in types to keep your plugin as simple as possible.

  • 按平台拆分插件:例如 — withIosSplashwithAndroidSplash。这使得在 npx expo prebuild 中使用 --platform 标志在 EXPO_DEBUG 模式下更容易遵循。

    ¥Split up plugins by platform: For example — withIosSplash, withAndroidSplash. This makes using the --platform flag in npx expo prebuild a bit easier to follow in EXPO_DEBUG mode.

  • 对你的插件进行单元测试:为复杂的修改编写 Jest 测试。如果你的插件需要访问文件系统,请使用模拟系统(我们强烈推荐 memfs),你可以在 expo-notifications 插件测试中看到这方面的示例。

    ¥Unit test your plugin: Write Jest tests for complex modifications. If your plugin requires access to the filesystem, use a mock system (we strongly recommend memfs), you can see examples of this in the expo-notifications plugin tests.

  • TypeScript 插件总是比 JavaScript 插件更好。查看 expo-module-script 插件 工具了解更多信息。

    ¥A TypeScript plugin is always better than a JavaScript plugin. Check out the expo-module-script plugin tooling for more info.

  • 不要通过配置插件修改 sdkVersion,这可能会破坏 expo install 等命令并导致其他意外问题。

    ¥Do not modify the sdkVersion via a config plugin, this can break commands like expo install and cause other unexpected issues.

版本控制

¥Versioning

默认情况下,npx expo prebuild 在与项目正在使用的 Expo SDK 版本关联的 源模板 上运行转换。SDK 版本在 app.json 中定义或从项目安装的 expo 版本推断。

¥By default, npx expo prebuild runs transformations on a source template associated with the Expo SDK version that a project is using. The SDK version is defined in the app.json or inferred from the installed version of expo that the project has.

例如,当 Expo SDK 升级到 React Native 的新版本时,模板可能会发生重大变化,以适应 React Native 或 Android 或 iOS 新版本的变化。

¥When Expo SDK upgrades to a new version of React Native for instance, the template may change significantly to account for changes in React Native or new releases of Android or iOS.

如果你的插件主要使用 静态修改,那么它可以跨版本正常运行。如果它使用正则表达式来转换应用代码,那么你肯定需要记录你的插件适用于哪个 Expo SDK 版本。Expo 每季度(每 3 个月)发布一个新版本,并且有一个 测试期,你可以在发布之前测试你的插件是否适用于新版本。

¥If your plugin is mostly using static modifications then it will work well across versions. If it's using a regular expression to transform application code, then you'll definitely want to document which Expo SDK version your plugin is intended for. Expo releases a new version quarterly (every 3 months), and there is a beta period where you can test if your plugin works with the new version before it's released.

插件属性

¥Plugin properties

属性用于自定义插件在预构建期间的工作方式。

¥Properties are used to customize the way a plugin works during prebuild.

属性必须始终是静态值(没有函数或 promise)。考虑以下类型:

¥Properties MUST always be static values (no functions, or promises). Consider the following types:

type StaticValue = boolean | number | string | null | StaticArray | StaticObject;

type StaticArray = StaticValue[];

interface StaticObject {
  [key: string]: StaticValue | undefined;
}

需要静态属性,因为应用配置必须可序列化为 JSON 才能用作应用清单。静态属性还可以启用为自动补齐和 IntelliSense 生成 JSON 架构类型检查的工具。

¥Static properties are required because the app config must be serializable to JSON for use as the app manifest. Static properties can also enable tooling that generates JSON schema type checking for autocomplete and IntelliSense.

如果可能的话,尝试让你的插件在没有 props 的情况下工作,这将有助于像 expo installVS Code Expo 工具 这样的解析工具更好地工作。请记住,你添加的每个属性都会增加复杂性,从而使将来更难以更改并增加需要测试的功能数量。在可行的情况下,良好的默认值优于强制配置。

¥If possible, attempt to make your plugin work without props, this will help resolution tooling like expo install or VS Code Expo Tools work better. Remember that every property you add increases complexity, making it harder to change in the future and increase the amount of features you'll need to test. Good default values are preferred over mandatory configuration when feasible.

配置 Android 应用启动

¥Configure Android app startup

你可能会发现你的项目需要在 JS 引擎启动之前进行配置。例如,在 Android 上的 expo-splash-screen 中,我们需要在 MainActivity.java 的 onCreate 方法中指定调整大小模式。我们没有尝试通过危险的 mod 将这些更改危险地正则表达式到 MainActivity 中,而是使用生命周期钩子和静态设置系统来安全地确保该功能在所有受支持的 Android 语言(Java、Kotlin)、Expo 版本以及 配置插件。

¥You may find that your project requires configuration to be setup before the JS engine has started. For example, in expo-splash-screen on Android, we need to specify the resize mode in the MainActivity.java's onCreate method. Instead of attempting to dangerously regex these changes into the MainActivity via a dangerous mod, we use a system of lifecycle hooks and static settings to safely ensure the feature works across all supported Android languages (Java, Kotlin), versions of Expo, and combination of config plugins.

该系统由三个组件组成:

¥This system is made up of three components:

  • ReactActivityLifecycleListenersexpo-modules-core 公开的接口,用于在调用项目 ReactActivityonCreate 方法时获取原生回调。

    ¥ReactActivityLifecycleListeners: An interface exposed by expo-modules-core to get a native callback when the project ReactActivity's onCreate method is invoked.

  • withStringsXmlexpo/config-plugins 公开的 mod,它将属性写入 Android strings.xml 文件,该库可以安全地读取 strings.xml 值并进行初始设置。字符串 XML 值遵循指定的格式以保持一致性。

    ¥withStringsXml: A mod exposed by expo/config-plugins which writes a property to the Android strings.xml file, the library can safely read the strings.xml value and do initial setup. The string XML values follow a designated format for consistency.

  • SingletonModule(可选):expo-modules-core 公开的接口,用于在原生模块和 ReactActivityLifecycleListeners 之间创建共享接口。

    ¥SingletonModule (optional): An interface exposed by expo-modules-core to create a shared interface between native modules and ReactActivityLifecycleListeners.

考虑这个例子:我们希望在调用 onCreate 方法后立即将自定义 "value" 字符串设置为 Android Activity 上的属性。我们可以通过创建节点模块 expo-custom、实现 expo-modules-core 和 Expo 配置插件来安全地做到这一点:

¥Consider this example: We want to set a custom "value" string to a property on the Android Activity, directly after the onCreate method was invoked. We can do this safely by creating a node module expo-custom, implementing expo-modules-core, and Expo config plugins:

首先,我们在 Android 原生模块中注册 ReactActivity 监听器,只有当用户在其项目中设置了 expo-modules-core 支持时才会调用此监听器(默认在使用 Expo CLI、Create React Native App、Ignite CLI 和 Expo 预构建引导的项目中) 。

¥First, we register the ReactActivity listener in our Android native module, this will only be invoked if the user has expo-modules-core support, setup in their project (default in projects bootstrapped with Expo CLI, Create React Native App, Ignite CLI, and Expo prebuilding).

expo-custom/android/src/main/java/expo/modules/custom/CustomPackage.kt
package expo.modules.custom

import android.content.Context
import expo.modules.core.BasePackage
import expo.modules.core.interfaces.ReactActivityLifecycleListener

class CustomPackage : BasePackage() {
  override fun createReactActivityLifecycleListeners(activityContext: Context): List<ReactActivityLifecycleListener> {
    return listOf(CustomReactActivityLifecycleListener(activityContext))
  }

  // ...
}

接下来我们实现 ReactActivity 监听器,它传递给 Context 并且能够从项目 strings.xml 文件中读取。

¥Next we implement the ReactActivity listener, this is passed the Context and is capable of reading from the project strings.xml file.

expo-custom/android/src/main/java/expo/modules/custom/CustomReactActivityLifecycleListener.kt
package expo.modules.custom

import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.Log
import expo.modules.core.interfaces.ReactActivityLifecycleListener

class CustomReactActivityLifecycleListener(activityContext: Context) : ReactActivityLifecycleListener {
  override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
    // Execute static tasks before the JS engine starts.
    // These values are defined via config plugins.

    var value = getValue(activity)
    if (value != "") {
      // Do something to the Activity that requires the static value...
    }
  }

  // Naming is node module name (`expo-custom`) plus value name (`value`) using underscores as a delimiter
  // i.e. `expo_custom_value`
  // `@expo/vector-icons` + `iconName` -> `expo__vector_icons_icon_name`
  private fun getValue(context: Context): String = context.getString(R.string.expo_custom_value).toLowerCase()
}

我们必须定义默认的 string.xml 值,用户将通过在 strings.xml 文件中使用相同的 name 属性在本地覆盖这些值。

¥We must define default string.xml values which the user will overwrite locally by using the same name property in their strings.xml file.

expo-custom/android/src/main/res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="expo_custom_value" translatable="false"></string>
</resources>

此时,裸用户可以通过在本地 strings.xml 文件中创建字符串来配置此值(假设他们也有 expo-modules-core 支持设置):

¥At this point, bare users can configure this value by creating a string in their local strings.xml file (assuming they also have expo-modules-core support setup):

./android/app/src/main/res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="expo_custom_value" translatable="false">I Love Expo</string>
</resources>

对于托管用户,我们可以通过 Expo 配置插件公开此功能(安全!):

¥For managed users, we can expose this functionality (safely!) via an Expo config plugin:

expo-custom/app.plugin.js
const { AndroidConfig, withStringsXml } = require('expo/config-plugins');

function withCustom(config, value) {
  return withStringsXml(config, config => {
    config.modResults = setStrings(config.modResults, value);
    return config;
  });
}

function setStrings(strings, value) {
  // Helper to add string.xml JSON items or overwrite existing items with the same name.
  return AndroidConfig.Strings.setStringItem(
    [
      // XML represented as JSON
      // <string name="expo_custom_value" translatable="false">value</string>
      { $: { name: 'expo_custom_value', translatable: 'false' }, _: value },
    ],
    strings
  );
}

托管 Expo 用户现在可以与此 API 进行交互,如下所示:

¥Managed Expo users can now interact with this API like so:

app.json
{
  "expo": {
    "plugins": [["expo-custom", "I Love Expo"]]
  }
}

通过重新运行 npx expo prebuild -peas build -p androidnpx expo run:ios),用户现在可以看到更改,并安全地应用到他们的托管项目中!

¥By re-running npx expo prebuild -p (eas build -p android, or npx expo run:ios) the user can now see the changes, safely applied in their managed project!

正如你从示例中看到的,我们严重依赖应用代码 (expo-modules-core) 与应用代码(原生项目)进行交互。这确保了我们的配置插件安全可靠,希望在很长一段时间内都是安全可靠的!

¥As you can see from the example, we rely heavily on application code (expo-modules-core) to interact with application code (the native project). This ensures that our config plugins are safe and reliable, hopefully for a very long time!

调试配置插件

¥Debug config plugins

你可以通过运行 EXPO_DEBUG=1 expo prebuild 来调试配置插件。如果启用了 EXPO_DEBUG,将打印插件堆栈日志,这些日志对于查看运行了哪些 mod 以及它们运行的顺序非常有用。要查看所有静态插件解析错误,请启用 EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS,只有插件作者需要这样做。默认情况下,一些自动插件错误是隐藏的,因为它们通常与版本控制问题相关并且不是很有帮助(也就是说,旧版包还没有配置插件)。

¥You can debug config plugins by running EXPO_DEBUG=1 expo prebuild. If EXPO_DEBUG is enabled, the plugin stack logs will be printed, these are useful for viewing which mods ran, and in what order they ran in. To view all static plugin resolution errors, enable EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS, this should only be needed for plugin authors. By default, some automatic plugin errors are hidden because they're usually related to versioning issues and aren't very helpful (that is, legacy package doesn't have a config plugin yet).

运行 npx expo prebuild --clean 并在编译前删除生成的原生文件夹。

¥Running npx expo prebuild --clean with remove the generated native folders before compiling.

你还可以运行 npx expo config --type prebuild 来打印未评估 mod 的插件结果(不生成代码)。

¥You can also run npx expo config --type prebuild to print the results of the plugins with the mods unevaluated (no code is generated).

可以使用 EXPO_PROFILE=1 来分析 Expo CLI 命令。

¥Expo CLI commands can be profiled using EXPO_PROFILE=1.

内省

¥Introspection

内省是一种高级的技术,用于读取修饰符的评估结果,而无需在项目中生成任何代码。这可用于快速调试 静态修改 的结果,而无需运行预构建。你可以使用 预览功能vscode-expo 与内省进行实时交互。

¥Introspection is an advanced technique used to read the evaluated results of modifiers without generating any code in the project. This can be used to quickly debug the results of static modifications without needing to run prebuild. You can interact with introspection live, by using the preview feature of vscode-expo.

你可以通过在项目中运行 expo config --type introspect 来尝试内省。

¥You can try introspection by running expo config --type introspect in a project.

内省仅支持修饰符的子集:

¥Introspection only supports a subset of modifiers:

  • android.manifest

  • android.gradleProperties

  • android.strings

  • android.colors

  • android.colorsNight

  • android.styles

  • ios.infoPlist

  • ios.entitlements

  • ios.expoPlist

  • ios.podfileProperties

自省仅适用于安全修饰符(静态文件,如 JSON、XML、plist、属性),但 ios.xcodeproj 除外,它通常需要更改文件系统,因此不具有幂等性。

¥Introspection only works on safe modifiers (static files like JSON, XML, plist, properties), except ios.xcodeproj which often requires file system changes, making it non idempotent.

内省通过创建自定义基本 mod 来工作,这些自定义基本 mod 的工作方式与默认基本 mod 类似,只是它们最后不会将 modResults 写入磁盘。他们没有保留结果,而是将结果保存到 _internal.modResults 下的应用配置中,后跟 mod 的名称,例如 ios.infoPlist mod 保存到 _internal.modResults.ios.infoPlist: {}

¥Introspection works by creating custom base mods that work like the default base mods, except they don't write the modResults to disk at the end. Instead of persisting, they save the results to the app config under _internal.modResults, followed by the name of the mod such as the ios.infoPlist mod saves to _internal.modResults.ios.infoPlist: {}.

作为一个现实世界的示例,eas-cli 使用内省来确定托管应用中的最终 iOS 权利,因此它可以在构建之前将它们与 Apple 开发者门户同步。Introspection 还可以用作方便的调试和开发工具。

¥As a real-world example, introspection is used by eas-cli to determine what the final iOS entitlements will be in a managed app, so it can sync them with the Apple Developer Portal before building. Introspection can also be used as a handy debugging and development tool.

旧版插件

¥Legacy plugins

为了使 eas build 与经典 expo build 服务一样工作,我们添加了对 "旧版插件" 的支持,当它们安装在项目中时,它们会自动应用于项目。

¥To make eas build work the same as the classic expo build service, we added support for "legacy plugins" which are applied automatically to a project when they're installed in the project.

例如,假设一个项目安装了 expo-camera,但其 app.json 中没有 plugins: ['expo-camera']。Expo CLI 会自动将 expo-camera 添加到插件中,以确保将所需的摄像头和麦克风权限添加到项目中。用户仍然可以通过手动将 expo-camera 插件添加到 plugins 数组来自定义 expo-camera 插件,并且手动定义的插件将优先于自动插件。

¥For instance, say a project has expo-camera installed but doesn't have plugins: ['expo-camera'] in their app.json. Expo CLI would automatically add expo-camera to the plugins to ensure that the required camera and microphone permissions are added to the project. The user can still customize the expo-camera plugin by adding it to the plugins array manually, and the manually defined plugins will take precedence over the automatic plugins.

你可以通过运行 expo config --type prebuild 并查看 _internal.pluginHistory 属性来调试添加了哪些插件。

¥You can debug which plugins were added by running expo config --type prebuild and seeing the _internal.pluginHistory property.

这将显示一个对象,其中包含使用 expo/config-plugins 中的 withRunOnce 插件添加的所有插件。

¥This will show an object with all plugins that were added using withRunOnce plugin from expo/config-plugins.

请注意,expo-location 使用 version: '11.0.0'react-native-maps 使用 version: 'UNVERSIONED'。这意味着以下内容:

¥Notice that expo-location uses version: '11.0.0', and react-native-maps uses version: 'UNVERSIONED'. This means the following:

  • expo-locationreact-native-maps 都安装在项目中。

    ¥expo-location and react-native-maps are both installed in the project.

  • expo-location 正在使用项目 node_modules/expo-location/app.plugin.js 中的插件

    ¥expo-location is using the plugin from the project's node_modules/expo-location/app.plugin.js

  • 项目中安装的 react-native-maps 版本没有插件,因此它依靠 expo-cli 附带的未版本化插件来提供旧版支持。

    ¥The version of react-native-maps installed in the project doesn't have a plugin, so it's falling back on the unversioned plugin that is shipped with expo-cli for legacy support.

{
  _internal: {
    pluginHistory: {
      'expo-location': {
        name: 'expo-location',
        version: '11.0.0',
      },
      'react-native-maps': {
        name: 'react-native-maps',
        version: 'UNVERSIONED',
      },
    },
  },
};

为了获得最稳定的体验,你应该尝试在项目中没有 UNVERSIONED 插件。这是因为 UNVERSIONED 插件可能不支持你项目中的原生代码。例如,假设你的项目中有一个 UNVERSIONED Facebook 插件,如果 Facebook 原生代码或插件有重大更改,这将破坏你的项目预构建方式并导致构建时出错。

¥For the most stable experience, you should try to have no UNVERSIONED plugins in your project. This is because the UNVERSIONED plugin may not support the native code in your project. For instance, say you have an UNVERSIONED Facebook plugin in your project, if the Facebook native code or plugin has a breaking change, that will break the way your project prebuilds and cause it to error on build.

静态修改

¥Static modification

插件可以使用正则表达式转换应用代码,但如果模板随着时间的推移而发生变化,那么这些修改就会很危险,那么正则表达式将变得难以预测(同样,如果用户手动修改文件或使用自定义模板)。以下是一些不应手动修改的文件示例以及替代方案。

¥Plugins can transform application code with regular expressions, but these modifications are dangerous if the template changes over time then the regex becomes hard to predict (similarly if the user modifies a file manually or uses a custom template). Here are some examples of files you shouldn't modify manually, and alternatives.

Android Gradle 文件

¥Android Gradle Files

Gradle 文件是用 Groovy 或 Kotlin 编写的。它们用于管理 Android 应用中的依赖、版本控制和其他设置。不要直接使用 withProjectBuildGradlewithAppBuildGradlewithSettingsGradle mods 修改它们,而是使用静态 gradle.properties 文件。

¥Gradle files are written in either Groovy or Kotlin. They are used to manage dependencies, versioning, and other settings in the Android app. Instead of modifying them directly with the withProjectBuildGradle, withAppBuildGradle, or withSettingsGradle mods, utilize the static gradle.properties file.

gradle.properties 是一个静态键/值对,groovy 文件可以从中读取。例如,假设你想控制 Groovy 中的某些切换:

¥The gradle.properties is a static key/value pair that groovy files can read from. For example, say you wanted to control some toggle in Groovy:

gradle.properties
expo.react.jsEngine=hermes

然后在 Gradle 文件中:

¥Then later in a Gradle file:

app/build.gradle
project.ext.react = [enableHermes: findProperty('expo.react.jsEngine') ?: 'jsc']
  • 对于 gradle.properties 中的键,使用驼峰式大小写,以 . 分隔,并且通常以 expo 前缀开头,表示该属性由预构建管理。

    ¥For keys in the gradle.properties, use camel case separated by .s, and usually starting with the expo prefix to denote that the property is managed by prebuild.

  • 要访问该属性,请使用两种全局方法之一:

    ¥To access the property, use one of two global methods:

    • property:获取一个属性,如果该属性未定义则抛出错误。

      ¥property: Get a property, throw an error if the property is not defined.

    • findProperty:如果属性丢失,则获取属性而不引发错误。这通常可以与 ?: 运算符一起使用来提供默认值。

      ¥findProperty: Get a property without throwing an error if the property is missing. This can often be used with the ?: operator to provide a default value.

一般来说,你应该只通过 Expo 自动链接 与 Gradle 文件交互,这提供了与项目文件的编程接口。

¥Generally, you should only interact with the Gradle file via Expo Autolinking, this provides a programmatic interface with the project files.

iOS 应用代理

¥iOS AppDelegate

某些模块可能需要向项目 AppDelegate 添加委托方法,这可以通过 withAppDelegate mod 危险地完成,也可以通过向原生模块添加对 unimodules AppDelegate 代理的支持来安全地完成。unimodules AppDelegate 代理可以以安全可靠的方式混合对原生模块的函数调用。如果 AppDelegate 项目的语言从 Objective-C 更改为 Swift,swizzler 将继续工作,而正则表达式可能会失败。

¥Some modules may need to add delegate methods to the project AppDelegate, this can be done dangerously via the withAppDelegate mod, or it can be done safely by adding support for unimodules AppDelegate proxy to the native module. The unimodules AppDelegate proxy can swizzle function calls to native modules in a safe and reliable way. If the language of the project AppDelegate changes from Objective-C to Swift, the swizzler will continue to work, whereas a regex would possibly fail.

以下是 AppDelegate 代理实际运行的一些示例:

¥Here are some examples of the AppDelegate proxy in action:

  • expo-app-auth -- EXAppAuthAppDelegate.m(开放网址)

    ¥expo-app-auth -- EXAppAuthAppDelegate.m (openURL)

  • expo-notifications -- EXPushTokenManager.m(didRegisterForRemoteNotificationsWithDeviceToken、didFailToRegisterForRemoteNotificationsWithError)

  • expo-facebook -- EXFacebookAppDelegate.m(开放网址)

    ¥expo-facebook -- EXFacebookAppDelegate.m (openURL)

  • expo-file-system -- EXSessionHandler.m(handleEventsForBackgroundURLSession)

目前,将 AppDelegate 代理支持添加到原生模块而不将该模块转换为 unimodule 的唯一已知方法是创建一个封装器包:example

¥Currently, the only known way to add support for the AppDelegate proxy to a native module, without converting that module to a unimodule, is to create a wrapper package: example.

我们计划在未来改进这一点。

¥We plan to improve this in the future.

iOS CocoaPods Podfile

ios/Podfile 可以使用正则表达式进行危险的自定义,或者通过 JSON 进行静态自定义:

¥The ios/Podfile can be customized dangerously with regex, or statically via JSON:

Podfile
require 'json'

podfileConfig = JSON.parse(File.read(File.join(__dir__, 'podfile.config.json')))

platform :ios, '11.0'

target 'yolo27' do
  use_unimodules!
  config = use_native_modules!
  use_react_native!(:path => config["reactNativePath"])

  # podfileConfig['version']
end

一般来说,你应该只通过 Expo 自动链接 与 Podfile 交互,这提供了与项目文件的编程接口。

¥Generally, you should only interact with the Podfile via Expo Autolinking, this provides a programmatic interface with the project files.

自定义基础修饰符

¥Custom base modifiers

Expo CLI npx expo prebuild 命令使用 @expo/prebuild-config 来获取默认的基本修饰符。这些默认值仅管理公共文件的子集,如果你想管理自定义文件,你可以通过添加新的基本修饰符在本地执行此操作。

¥The Expo CLI npx expo prebuild command uses @expo/prebuild-config to get the default base modifiers. These defaults only manage a subset of common files, if you want to manage custom files you can do that locally by adding new base modifiers.

例如,假设你想要添加对管理 ios/*/AppDelegate.h 文件的支持,你可以通过添加 ios.appDelegateHeader 修饰符来实现。

¥For example, say you wanted to add support for managing the ios/*/AppDelegate.h file, you could do this by adding a ios.appDelegateHeader modifier.

此示例使用 ts-node 来实现简单的本地 TypeScript 支持,这并不是绝对必要的。了解更多

¥This example uses ts-node for simple local TypeScript support, this isn't strictly necessary. Learn more.

withAppDelegateHeaderBaseMod.ts
import { ConfigPlugin, IOSConfig, Mod, withMod, BaseMods } from 'expo/config-plugins';
import fs from 'fs';

/**

 * A plugin which adds new base modifiers to the prebuild config.
 */
export function withAppDelegateHeaderBaseMod(config) {
  return BaseMods.withGeneratedBaseMods<'appDelegateHeader'>(config, {
    platform: 'ios',
    providers: {
      // Append a custom rule to supply AppDelegate header data to mods on `mods.ios.appDelegateHeader`
      appDelegateHeader: BaseMods.provider<IOSConfig.Paths.AppDelegateProjectFile>({
        // Get the local filepath that should be passed to the `read` method.
        getFilePath({ modRequest: { projectRoot } }) {
          const filePath = IOSConfig.Paths.getAppDelegateFilePath(projectRoot);
          // Replace the .m with a .h
          if (filePath.endsWith('.m')) {
            return filePath.substr(0, filePath.lastIndexOf('.')) + '.h';
          }
          // Possibly a Swift project...
          throw new Error(`Could not locate a valid AppDelegate.h at root: "${projectRoot}"`);
        },
        // Read the input file from the filesystem.
        async read(filePath) {
          return IOSConfig.Paths.getFileInfo(filePath);
        },
        // Write the resulting output to the filesystem.
        async write(filePath: string, { modResults: { contents } }) {
          await fs.promises.writeFile(filePath, contents);
        },
      }),
    },
  });
}

/**

 * (Utility) Provides the AppDelegate header file for modification.
 */
export const withAppDelegateHeader: ConfigPlugin<Mod<IOSConfig.Paths.AppDelegateProjectFile>> = (
  config,
  action
) => {
  return withMod(config, {
    platform: 'ios',
    mod: 'appDelegateHeader',
    action,
  });
};

// (Example) Log the contents of the modifier.
export const withSimpleAppDelegateHeaderMod = config => {
  return withAppDelegateHeader(config, config => {
    console.log('modify header:', config.modResults);
    return config;
  });
};

要使用这个新的基本模组,请将其添加到插件数组中。基本 mod 必须在使用该 mod 的所有其他插件之后最后添加,这是因为它必须在进程结束时将结果写入磁盘。

¥To use this new base mod, add it to the plugins array. The base mod MUST be added last after all other plugins that use the mod, this is because it must write the results to disk at the end of the process.

app.config.js
// Required for external files using TS
require('ts-node/register');

import {
  withAppDelegateHeaderBaseMod,
  withSimpleAppDelegateHeaderMod,
} from './withAppDelegateHeaderBaseMod.ts';

export default ({ config }) => {
  if (!config.plugins) config.plugins = [];
  config.plugins.push(
    withSimpleAppDelegateHeaderMod,

    // Base mods MUST be last
    withAppDelegateHeaderBaseMod
  );
  return config;
};

有关详细信息,请参阅 增加支持的 PR 了解此功能。

¥For more info, see the PR that adds support for this feature.

expo 安装

¥expo install

可以使用 expo install 命令将带有配置插件的节点模块自动添加到项目的应用配置中。相关 PR

¥Node modules with config plugins can be added to the project's app config automatically by using the expo install command. Related PR.

这使得设置变得更加容易,并有助于防止用户忘记添加插件。

¥This makes setup a bit easier and helps prevent users from forgetting to add a plugin.

这确实有一些警告:

¥This does come with a couple of caveats:

  1. 包必须通过 app.plugin.js 导出插件,添加此规则是为了防止像 lodash 这样的流行包被误认为是配置插件并破坏预构建。

    ¥Packages must export a plugin via app.plugin.js, this rule was added to prevent popular packages like lodash from being mistaken for a config plugin and breaking the prebuild.

  2. 目前没有机制可以检测配置插件是否具有强制属性。因此,expo install 只会添加插件,而不会尝试添加任何额外的属性。例如,expo-camera 有可选的额外 props,所以 plugins: ['expo-camera'] 是有效的,但如果它有强制 props,那么 expo-camera 就会抛出错误。

    ¥There is currently no mechanism for detecting if a config plugin has mandatory props. Because of this, expo install will only add the plugin, and not attempt to add any extra props. For example, expo-camera has optional extra props, so plugins: ['expo-camera'] is valid, but if it had mandatory props then expo-camera would throw an error.

  3. 仅当用户的项目使用静态应用配置(app.json 和 app.config.json)时,才能自动添加插件。如果用户在带有 app.config.js 的项目中运行 expo install expo-camera,他们会看到如下警告:

    ¥Plugins can only be automatically added when the user's project uses a static app config (app.json and app.config.json). If the user runs expo install expo-camera in a project with an app.config.js, they'll see a warning like:

    Cannot automatically write to dynamic config at: app.config.js
    Please add the following to your app config
    
      {
        "plugins": [
        "expo-camera"
        ]
      }
    
Expo 中文网 - 粤ICP备13048890号