开发和调试插件
了解 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.
插件开发
🌐 Plugin development
使用 modifier previews 实时调试你的插件结果。
为了使插件开发更容易,我们在 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:
{ "dependencies": {}, "devDependencies": { "expo": "^54.0.0" }, "peerDependencies": { "expo": ">=54.0.0" }, "peerDependenciesMeta": { "expo": { "optional": true } } }
- 你可以更新
expo的确切版本,以针对特定版本进行构建。 - 对于依赖核心稳定 API 的简单配置插件,例如仅修改 AndroidManifest.xml 或 Info.plist 的插件,你可以使用像上面示例中的松散依赖。
- 你也可以将
expo-module-scripts安装为开发依赖,但这不是必需的。
导入配置插件包
🌐 Import the config plugins package
expo/config-plugins 和 expo/config 包是从 expo 包重新导出的。
🌐 The expo/config-plugins and expo/config packages are re-exported from the expo package.
const { ... } = require('expo/config-plugins'); const { ... } = require('expo/config');
通过 expo 包导入可以确保你使用的是 expo 包所依赖的 expo/config-plugins 和 expo/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 读取这些静态值。 - 避免执行长时间运行的任务,例如发出网络请求或在 mod 中安装 Node 模块。
- 不要在 mod 中添加交互式终端提示。
- 仅在危险的模组中生成、移动和删除新文件。未遵守此操作将破坏introspection。
- 使用内置的配置插件,如
withXcodeProject,以尽量减少文件被读取和解析的次数。 - 坚持使用预构建内部使用的 XML 解析库,这有助于防止不必要地重新排列代码的更改。
插件结构和搭建
🌐 Plugin structure and scaffolding
版本控制
🌐 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.
如果你的插件主要使用静态修改,那么通常在不同的 SDK 版本中都能很好地工作。如果它使用正则表达式来转换应用代码,那么你肯定需要记录你的插件针对的是哪个 Expo SDK 版本。在 SDK 发布周期中,有一个测试版期间,你可以在新版本发布前测试你的插件是否正常工作。
🌐 If your plugin is mostly using static modifications then it will usually work well across SDK 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. During the SDK release cycle, there is a beta period where you can test if your plugin works with the new version before it's released.
插件属性
🌐 Plugin properties
属性用于自定义插件在预构建期间的工作方式。它们必须始终是静态值(不能是函数或 Promise)。请考虑以下类型:
🌐 Properties are used to customize the way a plugin works during prebuild. They 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 才能用作应用清单。
🌐 Static properties are required because the app config must be serializable to JSON for use as the app manifest.
如果可能,尝试让你的插件在不使用属性的情况下也能工作,这将有助于像 expo install 或 VS Code Expo Tools 这样的解析工具更好地工作。记住,每增加一个属性都会增加复杂性,使将来修改变得更困难,同时也增加了你需要测试的功能数量。在可行的情况下,优先使用好的默认值,而不是强制配置。
🌐 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 increases the amount of features you'll need to test. Good default values are preferred over mandatory configuration when feasible.
开发环境
🌐 Development environment
工具
🌐 Tooling
我们强烈推荐安装 Expo Tools 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.
设置 Playground 环境
🌐 Set up a playground environment
你可以使用 JS 轻松开发插件,但如果你想设置 Jest 测试并使用 TypeScript,则需要一个 monorepo。
🌐 You can develop plugins easily using JS, but if you want to set up Jest tests and use TypeScript, you will want a monorepo.
使用 monorepo,你可以在一个 node 模块上工作,并像它已经发布到 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 set up a project.
在你的 monorepo 的 packages/ 目录中,创建一个模块,并在其中 引导一个配置插件。
🌐 In your monorepo's packages/ directory, 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 - 在你的测试项目中,运行
npm install path/to/react-native-my-package-1.0.0.tgz,这将把该包添加到你的 package.json 的dependencies对象中。 - 将该包添加到你的 app.json 中的
plugins数组:{ "plugins": ["react-native-my-package"] }- 如果你已经安装了 VS Code Expo 工具,插件的自动补齐功能应该可以正常使用。
- 如果你需要更新该软件包,请在软件包的 package.json 中更改
version并重复此过程。
使用插件修改原生文件
🌐 Modifying native files with plugins
修改 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 the configuration being missed due to 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:
<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" />.
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.
下面是向 Info.plist 添加 GADApplicationIdentifier 的示例:
🌐 Here's an example of adding a GADApplicationIdentifier to the Info.plist:
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 中读取,我们提供了一个模块(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 的创建是为了在从旧的无版本插件迁移到有版本插件时防止重复插件运行。
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 );
配置 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:
ReactActivityLifecycleListeners:expo-modules-core提供的接口,用于在项目ReactActivity的onCreate方法被调用时获取原生回调。withStringsXml:expo/config-plugins暴露的一个模块,它会将属性写入 Android 的 strings.xml 文件,库可以安全地读取 strings.xml 的值并进行初始设置。字符串 XML 值遵循指定的格式以保证一致性。SingletonModule(可选):由expo-modules-core提供的一个接口,用于在原生模块和ReactActivityLifecycleListeners之间创建共享接口。
考虑这个例子:我们想在调用 onCreate 方法之后,直接在 Android 的 Activity 上设置一个自定义的 “value” 字符串。我们可以通过创建一个节点模块 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).
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.
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.
<?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):
<?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:
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:
{ "expo": { "plugins": [["expo-custom", "I Love Expo"]] } }
通过重新运行 npx expo prebuild -p(eas build -p android 或 npx 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!
调试配置插件
🌐 Debugging config plugins
你可以通过运行 EXPO_DEBUG=1 expo prebuild 来调试配置插件。如果启用了 EXPO_DEBUG,将会打印插件堆栈日志,这对于查看哪些插件运行过以及它们的运行顺序非常有用。要查看所有静态插件解析错误,请启用 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 will remove the generated native directories 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.manifestandroid.gradlePropertiesandroid.stringsandroid.colorsandroid.colorsNightandroid.stylesios.infoPlistios.entitlementsios.expoPlistios.podfileProperties
内省仅适用于安全的修饰符(如 JSON、XML、plist、properties 等静态文件),
ios.xcodeproj除外,它通常需要对文件系统进行更改,因此不是幂等的。
内省的工作方式是创建自定义基础模组,这些模组的工作方式类似于默认基础模组,只是它们不会在最后将 modResults 写入磁盘。
它们不是持久化保存,而是将结果保存到应用配置中的 _internal.modResults 下,后跟模组名称,例如 ios.infoPlist 模组保存到 _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 开发者门户同步。自省也可以作为一个方便的调试和开发工具。
🌐 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 数组中来自定义该插件,并且手动定义的插件会优先于自动添加的插件。
🌐 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-location和react-native-maps都已安装在项目中。expo-location正在使用项目node_modules/expo-location/app.plugin.js的插件- 项目中安装的
react-native-maps版本没有插件,因此它退回使用与expo-cli一起提供的无版本插件以支持旧版本。
{ _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 应用中的依赖、版本控制和其他设置。不要直接使用 withProjectBuildGradle、withAppBuildGradle 或 withSettingsGradle 模组修改它们,应使用静态的 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:
expo.react.jsEngine=hermes
然后在 Gradle 文件中:
🌐 Then later in a Gradle file:
project.ext.react = [enableHermes: findProperty('expo.react.jsEngine') ?: 'jsc']
- 对于
gradle.properties中的键,使用由.分隔的驼峰命名,并且通常以expo前缀开头,以表示该属性由 prebuild 管理。 - 要访问该属性,请使用两种全局方法之一:
property:获取一个属性,如果该属性未定义则抛出错误。findProperty:获取属性时,如果属性缺失不会抛出错误。这通常可以与?:运算符一起使用来提供默认值。
通常,你应该仅通过 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 添加代理方法。这可以通过使用 AppDelegate 订阅者 安全地完成,也可以通过 withAppDelegate 模块危险地完成(强烈不建议)。
使用 AppDelegate 订阅者可以让原生 Expo 模块以安全可靠的方式对重要事件作出响应。
🌐 Some modules may need to add delegate methods to the project AppDelegate. This can be done safely by using AppDelegate subscribers or dangerously via the withAppDelegate mod (strongly discouraged).
Using AppDelegate subscribers allows native Expo modules to react to important events in a safe and reliable way.
下面是一些 AppDelegate 订阅者实际使用的示例。此外,你还可以在 GitHub 的社区仓库中找到许多示例(其中一个示例)。
🌐 Below are some examples of the AppDelegate subscribers in action. Additionally, you will find many examples in community repositories on GitHub (one such example).
expo-linking:LinkingAppDelegateSubscriber.swift(openURL)expo-notifications:NotificationsAppDelegateSubscriber.swift(didRegisterForRemoteNotificationsWithDeviceToken,didFailToRegisterForRemoteNotificationsWithError,didReceiveRemoteNotification)
iOS CocoaPods Podfile
Podfile 可以用正则表达式进行自定义(这被认为是危险的,因为这类更改不易组合,并且多重更改很可能会冲突),但更可靠的方法是直接在名为 Podfile.properties.json 的 JSON 文件中设置配置值。下面看看 podfile_properties 是如何用来定制 Podfile 的:
🌐 The Podfile can be customized with a regular expression (this is considered dangerous because these types of changes do not compose well and multiple changes are likely to collide), but it's more reliable to instead set configuration values in JSON file called Podfile.properties.json. See how podfile_properties is used to customize the Podfile below:
require 'json' podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' target 'yolo27' do use_expo_modules! # ... # podfile_properties['your_property'] 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.
这个示例使用
tsx提供简单的本地 TypeScript 支持,这并不是绝对必要的。了解更多。
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; }); };
要使用这个新的基础模组,请将其添加到插件数组中。基础模组必须在所有使用该模组的其他插件之后最后添加,这是因为它必须在过程结束时将结果写入磁盘。
🌐 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.
// Required for external files using TS require('tsx/cjs'); 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
当使用 npx expo install 命令安装一个节点模块时,如果它包含配置插件,它将自动添加到项目的应用配置中。这使得设置更加容易,并有助于防止用户忘记添加插件。不过,这确实有一些注意事项:
🌐 When a node module is installed with the npx expo install command, if it includes a config plugin, it will be added to the project's app config automatically. This makes setup easier and helps prevent users from forgetting to add a plugin. However, this does come with a couple of caveats:
npx expo install仅会使用根目录的 app.config.js 文件自动将配置插件添加到应用清单中。添加此规则是为了防止像lodash这样流行的包被误认为是配置插件,从而导致预构建失败。- 目前没有机制来检测配置插件是否有必需的属性。因此,
expo install只会添加插件,而不会尝试添加任何额外属性。例如,expo-camera有可选的额外属性,所以plugins: ['expo-camera']是有效的,但如果它有必需的属性,那么expo-camera就会报错。 - 只有当用户的项目使用静态应用配置(app.json 和 app.config.json)时,插件才能被自动添加。如果用户在包含 app.config.js 的项目中运行
expo install expo-camera,他们会看到如下警告:
Cannot automatically write to dynamic config at: app.config.js Please add the following to your app config { "plugins": [ "expo-camera" ] }