库的插件开发

编辑

学习如何为 Expo 和 React Native 库开发配置插件。


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

本指南解释了可以在库中实现配置插件的关键配置步骤和策略。

库中配置插件的战略价值

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

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

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

项目结构

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

.
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 提供配置插件 API 及您插件将使用的类型。
  • expo-module-scripts 提供专门为 Expo 模块和配置插件设计的构建工具。它还处理 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": "^54.0.0" }, "peerDependencies": { "expo": ">=54.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 文件中定义您配置插件的主要入口点,该文件会从 plugin/build 目录导出编译后的插件代码:

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

上述配置至关重要,因为当 Expo CLI 查找插件时,它会在您库的项目根目录中检查此文件。 plugin/build 目录包含从您配置插件的 TypeScript 源代码生成的 JavaScript 文件。

关键实现模式

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

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

插件结构和平台特定实现

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

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; }); };

测试策略

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

有效的配置插件测试可以是以下一种或多种组合:

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

由于单元测试关注插件的转换逻辑而不涉及文件系统,您可以使用 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(); }); });

错误应该在您的配置插件中优雅地处理,以提供清晰的反馈,当配置失败时。使用 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” 后直接使用 camel case 命名平台名称。例如,withAndroidSplashwithIosSplash
  • 利用内置插件:如果在 应用配置预构建配置 中已经有可用配置,则无需为其编写配置插件。
  • 按平台拆分插件:在使用配置插件中的函数时,按平台拆分它们。例如,withAndroidSplashwithIosSplash。这使得在 npx expo prebuild 中使用 --platform 标志更容易跟随,因为日志将显示正在执行的特定于平台的函数。
  • 单元测试您的插件:为复杂的修改编写 Jest 测试。如果您的插件需要访问文件系统,使用模拟系统(我们强烈推荐 memfs),您可以在 expo-notifications 插件测试中看到此类示例。
  • TypeScript 插件总是优于 JavaScript 插件,因为它具有更高的类型安全性。有关更多信息,请查看 expo-module-scripts 插件 工具。
  • 请勿通过配置插件修改 sdkVersion,这可能会破坏 expo install 等命令并导致其他意外问题。