库的插件开发
编辑页面
了解如何为 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.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": "^56.0.0" }, "peerDependencies": { "expo": ">=56.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之类的命令,并导致其他意外问题。