库的插件开发

了解如何为 Expo 和 React Native 库开发配置插件。


React Native 库中的 Expo 配置插件代表了一种变革性的方式来自动化原生项目配置。与其让库用户手动编辑原生文件,比如 AndroidManifest.xmlInfo.plist 等,你可以提供一个插件,在预构建过程中自动处理这些配置。这将开发者体验从容易出错的手动设置转变为可靠的自动化配置,并且能够在不同项目中保持一致性。

🌐 Expo config plugins in a React Native library represent a transformative approach to automating native project configuration. Rather than requiring library users to manually edit native files, such as AndroidManifest.xml, Info.plist, and so on, you can provide a plugin that handles these configurations automatically during the prebuild process. This changes developer experience from error-prone manual setup to reliable, automated configuration that can work consistently across different projects.

本指南介绍了可用于在库中实现配置插件的关键配置步骤和策略。

🌐 This guide explains key configuration steps and strategies that you can use to implement a config plugin in your library.

库中配置插件的战略价值

🌐 Strategic value of a config plugin in a library

配置插件通常用于解决那些历史上使 React Native 库的采用比应有的更困难的互相关联的问题。有时,当用户安装一个 React Native 库时,他们会面临一系列复杂的原生配置步骤,而这些步骤必须正确执行,库才能正常工作。这些步骤是特定于平台的,有时还需要对原生开发概念有深入了解。

🌐 Config plugins tend to solve interconnected problems that have historically made React Native library adoption more difficult than it should be. At times, when a user installs a React Native library, they face a complex set of native configuration steps that must be performed correctly for the library to function. These steps are platform-specific and sometimes require deep knowledge of native development concepts.

通过在你的库中创建一个配置插件,你可以将这个看起来很复杂的手动过程转变为用户可以在其 Expo 项目的应用配置文件(通常是 app.json)中应用的简单配置声明。这降低了用户采用你库的门槛,同时也使设置过程更加可靠。

🌐 By creating a config plugin within your library, you can transform this complex-looking manual process into a simple configuration declaration that a user can apply in their Expo project's app config file (usually, app.json). This reduces the barrier to adoption for your library and simultaneously makes the setup process reliable.

除了立即改善用户体验之外,配置插件还支持与连续原生生成兼容,其中原生目录会被自动生成,而不是提交到版本控制中。如果没有配置插件,采用CNG的开发者将面临一个艰难的选择:要么放弃CNG工作流以手动配置原生文件,要么投入大量精力创建自己的自动化解决方案。这在现代Expo开发工作流中为库的采用设置了相当大的障碍。

🌐 Beyond immediate user experience improvements, config plugins enable compatibility with Continuous Native Generation, where native directories are generated automatically rather than checked into version control. Without a config plugin, developers who have adopted CNG face a difficult choice: either abandon the CNG workflow to manually configure native files, or invest significant effort in creating their own automation solutions. This creates a substantial barrier to library adoption in modern Expo development workflows.

项目结构

🌐 Project structure

目录结构是维护库中配置插件的基础。下面是一个示例目录结构:

🌐 A directory structure is the foundation for maintaining config plugins within your library. Below is an example directory structure:

.
androidAndroid native module code
  src
   main
    java
     com
      your-awesome-library
  build.gradle
iosiOS native module code
  YourAwesomeLibrary
  YourAwesomeLibrary.podspec
src
  index.tsMain library entry point
  YourAwesomeLibrary.tsCore library implementation
  types.tsTypeScript type definitions
plugin
  src
   index.tsPlugin entry point
   withAndroid.tsAndroid-specific configurations
   withIos.tsiOS-specific configurations
  build
  __tests__
  tsconfig.jsonPlugin-specific TypeScript config
example
  app.jsonExample app configuration
  App.tsxExample app implementation
  package.jsonExample app dependencies
__tests__
app.plugin.jsPlugin entry point for Expo CLI
package.jsonPackage configuration
tsconfig.jsonMain TypeScript configuration
jest.config.jsTesting configuration
README.mdDocumentation

上面的目录结构示例强调了以下组织原则:

🌐 The directory structure example above highlights the following organizational principles:

  • 根级分离:库代码(src)与插件实现(plugin)之间有清晰的边界
  • 插件目录组织:特定平台的文件(withAndroid.tswithIos.ts)便于针对性测试和维护
  • 构建输出管理:已在 plugins/build/ 目录中编译 JavaScript 和 TypeScript 声明
  • 测试:将插件测试与库测试分开,以反映不同的关注点。

开发环境的安装和配置

🌐 Installation and configuration for development

利用 Expo 工具的最直接方法是使用 expoexpo-module-scripts

🌐 The most straightforward approach to leverage Expo's tooling is to use expo and expo-module-scripts.

  • expo 提供了一个配置插件 API 以及你的插件将使用的类型。
  • expo-module-scripts 提供专门为 Expo 模块和配置插件设计的构建工具。它还处理 TypeScript 的编译。
Terminal
npx expo install package

在使用 expo-module-scripts 时,需要以下 package.json 配置。对于任何已有的同名脚本,请将其替换。

🌐 When using expo-module-scripts, it requires the following package.json configuration. For any already existing script with the same script name, replace it.

package.json
{ "scripts": { "build": "expo-module build", "build:plugin": "expo-module build plugin", "clean": "expo-module clean", "test": "expo-module test", "prepare": "expo-module prepare", "prepublishOnly": "expo-module prepublishOnly" }, "devDependencies": { "expo": "^54.0.0" }, "peerDependencies": { "expo": ">=54.0.0" }, "peerDependenciesMeta": { "expo": { "optional": true } } }

下一步是在 plugins 目录中添加 TypeScript 支持。打开 plugins/tsconfig.json 文件并添加以下内容:

🌐 The next step is to add TypeScript support within the plugins directory. Open plugins/tsconfig.json file and add the following:

plugins/tsconfig.json
{ "extends": "expo-module-scripts/tsconfig.plugin", "compilerOptions": { "outDir": "build", "rootDir": "src" }, "include": ["./src"], "exclude": ["**/__mocks__/*", "**/__tests__/*"] }

你还需要在 app.plugin.js 文件中定义配置插件的主入口点,该文件从 plugin/build 目录导出编译后的插件代码:

🌐 You also need to define the main entry point for your config plugin in the app.plugin.js file, which exports the compiled plugin code from the plugin/build directory:

app.plugin.js
module.exports = require('./plugin/build');

上述配置是必不可少的,因为当 Expo CLI 寻找插件时,会在你的库的项目根目录中检查此文件。plugin/build 目录包含从你的配置插件的 TypeScript 源代码生成的 JavaScript 文件。

🌐 The above configuration is essential because when the Expo CLI looks for a plugin, it checks for this file in the project root of your library. The plugin/build directory contains the JavaScript files generated from your config plugin's TypeScript source code.

关键实现模式

🌐 Key implementation patterns

成功实现配置插件的基本模式包括:

🌐 Essential patterns for a successful config plugin implementation include:

  • 插件结构:每个插件都应遵循的核心模式
  • 特定平台的实现:有效处理 Android 和 iOS 配置
  • 测试策略: 通过测试验证你的插件代码

插件结构和特定于平台的实现

🌐 Plugin structure and platform-specific implementation

每个配置插件都遵循相同的模式:接收配置和参数,通过修改应用变换,并返回修改后的配置。考虑以下核心插件结构如下所示:

🌐 Every config plugin follows the same pattern: receives configuration and parameters, applies transformations through mods, and returns the modified configuration. Consider the following core plugin structure looks like:

plugin/src/index.ts
import { type ConfigPlugin, withAndroidManifest, withInfoPlist } from 'expo/config-plugins'; export interface YourLibraryPluginProps { customProperty?: string; enableFeature?: boolean; } const withYourLibrary: ConfigPlugin<YourLibraryPluginProps> = (config, props = {}) => { // Apply Android configurations config = withAndroidConfiguration(config, props); // Apply iOS configurations config = withIosConfiguration(config, props); return config; }; export default withYourLibrary;
plugin/src/withAndroid.ts
import { type ConfigPlugin, withAndroidManifest, AndroidConfig } from 'expo/config-plugins'; export const withAndroidConfiguration: ConfigPlugin<YourLibraryPluginProps> = (config, props) => { return withAndroidManifest(config, config => { const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); AndroidConfig.Manifest.addMetaDataItemToMainApplication( mainApplication, 'your_library_config_key', props.customProperty || 'default_value' ); return config; }); };
plugin/src/withIos.ts
import { type ConfigPlugin, withInfoPlist } from 'expo/config-plugins'; export const withIosConfiguration: ConfigPlugin<YourLibraryPluginProps> = (config, props) => { return withInfoPlist(config, config => { config.modResults.YourLibraryCustomProperty = props.customProperty || 'default_value'; if (props.enableFeature) { config.modResults.YourLibraryFeatureEnabled = true; } return config; }); };

测试策略

🌐 Testing strategies

配置插件测试与常规库测试不同,因为你测试的是配置转换而不是运行时行为。你的插件接收配置对象并返回修改后的配置对象。

🌐 Config plugin testing differs from regular library testing because you are testing configuration transformations rather than runtime behavior. Your plugin receives configuration objects and returns modified configuration objects.

配置插件的有效测试可以结合以下一项或多项:

🌐 Effective testing for a config plugin can be a combination of one or more of the following:

  • 单元测试: 使用模拟的 Expo 配置对象测试配置转换逻辑
  • 跨平台验证:使用示例应用来验证实际预构建输出
  • 错误情况测试:使用错误处理

由于单元测试关注的是插件的转换逻辑,而不涉及文件系统,因此你可以使用 Jest 创建并运行模拟配置对象,将它们传入你的插件,并验证预期的修改是否正确执行。例如:

🌐 Since unit tests focus on a plugin's transformation logic without involving the file system, you can use Jest to create and run mock configuration objects, pass them through your plugin, and verify expected modifications are made correctly. For example:

plugin/__tests__/withYourLibrary.test.ts
import { withYourLibrary } from '../src'; describe('withYourLibrary', () => { it('should configure Android with custom property', () => { const config = { name: 'test-app', slug: 'test-app', platforms: ['android', 'ios'], }; const result = withYourLibrary(config, { customProperty: 'test-value', }); // Verify the plugin was applied correctly expect(result.plugins).toBeDefined(); }); });

错误应该在你的配置插件中被优雅地处理,以在配置失败时提供清晰的反馈。使用 try-catch 块来提前拦截错误:

🌐 Errors should be handled gracefully inside your config plugin to provide clear feedback when a configuration fails. Use try-catch blocks to intercept errors early:

plugin/src/index.ts
const withYourLibrary: ConfigPlugin<YourLibraryPluginProps> = (config, props = {}) => { try { // Validate configuration early validateProps(props); // Apply configurations config = withAndroidConfiguration(config, props); config = withIosConfiguration(config, props); return config; } catch (error) { // Re-throw with more context if needed throw new Error(`Failed to configure YourLibrary plugin: ${error.message}`); } };

其他构建方法

🌐 Alternative build approaches

如果你的库不使用 expo-module-scripts,你有两个选择:

🌐 If your library doesn't use expo-module-scripts, you have two options:

在主软件包中添加插件

🌐 Add a plugin to your main package

对于使用不同构建工具的库(例如使用 create-react-native-library 创建的库),添加一个 app.plugin.js 文件,并将其与主包一起构建:

🌐 For libraries using different build tools (like those created with create-react-native-library), add an app.plugin.js file and build it along with your main package:

app.plugin.js
module.exports = require('./lib/plugin');

创建单独的插件包

🌐 Create a separate plugin package

有些库将它们的配置插件作为与主库分开的独立包分发。这种做法可以让你将配置插件与其余的原生模块分开维护。你需要在 app.plugin.js 中包含 export,并编译来自插件的 build 目录。

🌐 Some libraries distribute their config plugin as a separate package from their main library. This approach allows you to maintain your config plugin separately from the rest of your native module. You need to include export in app.plugin.js and compile the build directory from your plugin.

app.plugin.js
{ "name": "your-library-expo-plugin", "main": "app.plugin.js", "files": ["app.plugin.js", "build/"], "peerDependencies": { "expo": "*", "your-library": "*" } }

插件开发最佳实践

🌐 Plugin development best practices

  • 在你的 README 中的说明:如果插件与 React Native 模块绑定,那么你应该为该包记录手动设置说明。如果插件出现任何问题,开发者应该能够手动添加插件自动化的项目修改。这也使你能够支持未使用 CNG 的项目。
    • 记录插件的可用属性,并指定是否有任何必需的属性。
    • 如果可能,插件应该是幂等的,这意味着无论是在一个新的原生项目模板上运行,还是在一个其更改已存在的项目模板上再次运行,它们所做的更改都是相同的。这允许开发者在同步配置更改时运行 npx expo prebuild 而无需 --clean 标志,而不必完全重新创建原生项目。这对于危险的修改可能会更困难。
  • 命名规范:如果适用于所有平台,请使用 withFeatureName 作为插件函数名称。如果插件是特定平台的,请使用驼峰命名法,并在 with 后面加上平台名称。例如,withAndroidSplashwithIosSplash
  • 利用内置插件:如果在 app configprebuild config 中已有可用的配置,则无需为其编写配置插件。
  • 按平台拆分插件:在配置插件中使用函数时,按平台拆分它们。例如,withAndroidSplashwithIosSplash。这样在 EXPO_DEBUG 模式下使用 --platform 标志在 npx expo prebuild 中时,会更容易跟踪,因为日志会显示哪些平台特定的函数正在执行。
  • 对你的插件进行单元测试:为复杂的修改编写 Jest 测试。如果你的插件需要访问文件系统,请使用模拟系统(我们强烈推荐 memfs),你可以在 expo-notifications 插件测试中看到示例。
  • 由于增加了类型安全,TypeScript 插件总是优于 JavaScript 插件。更多信息请查看 expo-module-scripts 插件 工具。
  • 不要通过配置插件修改 sdkVersion,这可能会导致 expo install 等命令失效,并引发其他意外问题。