库的插件开发
编辑页面
了解如何为 Expo 和 React Native 库开发配置插件。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
React Native 库中的 Expo config plugin 代表了一种自动化原生项目配置的变革性方法。与其要求库用户手动编辑原生文件,例如 AndroidManifest.xml、Info.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 原生模块代码srcmainjavacomyour-awesome-librarybuild.gradleiosiOS 原生模块代码YourAwesomeLibraryYourAwesomeLibrary.podspecsrcindex.ts库的主入口点YourAwesomeLibrary.ts库的核心实现types.tsTypeScript 类型定义pluginsrcindex.ts插件入口点withAndroid.tsAndroid 特定配置withIos.tsiOS 特定配置build__tests__tsconfig.json插件专属 TypeScript 配置exampleapp.json示例应用配置App.tsx示例应用实现package.json示例应用依赖__tests__app.plugin.jsExpo CLI 的插件入口点package.json包配置tsconfig.json主 TypeScript 配置jest.config.js测试配置README.md文档上面的目录结构示例突出显示了以下组织原则:
- 根级分离:库代码(src)与插件实现(plugin)之间边界清晰
- 插件目录组织:平台特定文件(withAndroid.ts、withIos.ts)便于专注测试和维护
- 构建输出管理:编译后的 JavaScript 和 TypeScript 声明位于 plugins/build/ 目录
- 测试:将插件测试与库测试分开,以反映不同关注点。
开发安装与配置
利用 Expo 工具链最直接的方法是使用 expo 和 expo-module-scripts。
expo提供了你的插件将使用的 config plugin API 和类型。expo-module-scripts提供专为 Expo 模块和 config plugin 设计的构建工具。它还负责 TypeScript 编译。
- npx expo install package使用 expo-module-scripts 时,需要以下 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": "^55.0.0" }, "peerDependencies": { "expo": ">=55.0.0" }, "peerDependenciesMeta": { "expo": { "optional": true } } }
下一步是在 plugins 目录中添加 TypeScript 支持。打开 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 目录导出已编译的插件代码:
module.exports = require('./plugin/build');
上述配置非常重要,因为当 Expo CLI 查找插件时,它会在你库的项目根目录中检查这个文件。plugin/build 目录包含由 config plugin 的 TypeScript 源代码生成的 JavaScript 文件。
关键实现模式
成功实现 config plugin 的关键模式包括:
- 插件结构:每个插件都应遵循的核心模式
- 平台特定实现:有效处理 Android 和 iOS 配置
- 测试策略:通过测试验证你的插件代码
插件结构与平台特定实现
每个 config plugin 都遵循相同的模式:接收配置和参数,通过 mods 应用转换,并返回修改后的配置。请考虑如下核心插件结构:
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;
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; }); };
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 创建并运行模拟配置对象,将它们传入你的插件,并验证是否正确地进行了预期修改。例如:
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 块尽早拦截错误:
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 文件,并将其与你的主包一起构建:
module.exports = require('./lib/plugin');
创建一个独立的插件包
有些库会将其配置插件作为一个独立包,与主库分开发布。这种方式允许你将配置插件与原生模块的其他部分分开维护。你需要在 app.plugin.js 中包含导出,并编译插件中的 build 目录。
{ "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" 之后。例如,withAndroidSplash、withIosSplash。 - 利用内置插件:如果在 app config 和 prebuild config 中已经有可用的配置,那么你就不需要为它编写配置插件。
- 按平台拆分插件:在配置插件中使用函数时,按平台拆分它们。例如,
withAndroidSplash、withIosSplash。这使得在EXPO_DEBUG模式下使用npx expo prebuild的--platform标志更容易理解,因为日志会显示正在执行哪些平台特定的函数。 - 为插件编写单元测试:为复杂修改编写 Jest 测试。如果你的插件需要访问文件系统,
请使用 mock 系统(我们强烈推荐
memfs),你可以在expo-notifications插件测试中看到示例。- 注意根目录下的 /__mocks__/**/* 目录以及 plugin/jest.config.js。
- 与 JavaScript 相比,TypeScript 插件通常更可取,因为它增加了类型安全。有关更多信息,请查看
expo-module-scripts插件 工具。 - 不要通过配置插件修改
sdkVersion,这可能会破坏诸如expo install之类的命令,并导致其他意外问题。