库的插件开发

编辑页面

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


For the complete documentation index, see llms.txt. Use this file to discover all available pages.

React Native 库中的 Expo config plugin 代表了一种自动化原生项目配置的变革性方法。与其要求库用户手动编辑原生文件,例如 AndroidManifest.xmlInfo.plist 等,不如提供一个插件,在 prebuild 过程中自动处理这些配置。这将开发者体验从容易出错的手动设置转变为可靠、自动化的配置,并且可以在不同项目之间稳定一致地工作。

本指南解释了你可以用来在库中实现 config plugin 的关键配置步骤和策略。

config plugin 在库中的战略价值

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

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

除了直接改善用户体验之外,config plugin 还使其能够兼容 Continuous Native Generation,在这种模式下,原生目录会自动生成,而不是检查到版本控制中。没有 config plugin,采用 CNG 的开发者会面临一个艰难的选择:要么放弃 CNG 工作流,手动配置原生文件;要么投入大量精力自行创建自动化方案。这为现代 Expo 开发工作流中的库采用设置了相当大的障碍。

项目结构

目录结构是维护库中 config plugin 的基础。下面是一个示例目录结构:

.
androidAndroid 原生模块代码
  src
   main
    java
     com
      your-awesome-library
  build.gradle
iosiOS 原生模块代码
  YourAwesomeLibrary
  YourAwesomeLibrary.podspec
src
  index.ts库的主入口点
  YourAwesomeLibrary.ts库的核心实现
  types.tsTypeScript 类型定义
plugin
  src
   index.ts插件入口点
   withAndroid.tsAndroid 特定配置
   withIos.tsiOS 特定配置
  build
  __tests__
  tsconfig.json插件专属 TypeScript 配置
example
  app.json示例应用配置
  App.tsx示例应用实现
  package.json示例应用依赖
__tests__
app.plugin.jsExpo CLI 的插件入口点
package.json包配置
tsconfig.json主 TypeScript 配置
jest.config.js测试配置
README.md文档

上面的目录结构示例突出显示了以下组织原则:

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

开发安装与配置

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

  • expo 提供了你的插件将使用的 config plugin API 和类型。
  • expo-module-scripts 提供专为 Expo 模块和 config plugin 设计的构建工具。它还负责 TypeScript 编译。
Terminal
npx expo install package

使用 expo-module-scripts 时,需要以下 package.json 配置。如果已有同名脚本,请将其替换。

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": "^56.0.0" }, "peerDependencies": { "expo": ">=56.0.0" }, "peerDependenciesMeta": { "expo": { "optional": true } } }

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

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

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

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

上述配置非常重要,因为当 Expo CLI 查找插件时,它会在你库的项目根目录中检查这个文件。plugin/build 目录包含由 config plugin 的 TypeScript 源代码生成的 JavaScript 文件。

关键实现模式

成功实现 config plugin 的关键模式包括:

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

插件结构与平台特定实现

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

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 = {}) => { // 应用 Android 配置 config = withAndroidConfiguration(config, props); // 应用 iOS 配置 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; }); };

测试策略

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

对 config plugin 的有效测试可以是以下一种或多种方式的组合:

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

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

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', }); // 验证插件是否被正确应用 expect(result.plugins).toBeDefined(); }); });

应当在你的 config plugin 内优雅地处理错误,以便在配置失败时提供清晰反馈。使用 try-catch 块尽早拦截错误:

plugin/src/index.ts
const withYourLibrary: ConfigPlugin<YourLibraryPluginProps> = (config, props = {}) => { try { // 及早验证配置 validateProps(props); // 应用配置 config = withAndroidConfiguration(config, props); config = withIosConfiguration(config, props); return config; } catch (error) { // 如有需要,重新抛出并附加更多上下文 throw new Error(`Failed to configure YourLibrary plugin: ${error.message}`); } };

其他构建方式

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

为主包添加一个插件

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

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

创建一个独立的插件包

有些库会将其配置插件作为一个独立包,与主库分开发布。这种方式允许你将配置插件与原生模块的其他部分分开维护。你需要在 app.plugin.js 中包含导出,并编译插件中的 build 目录。

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

插件开发最佳实践

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