教程:创建带配置插件的模块

关于使用 Expo Modules API 创建带配置插件的原生模块的教程。


配置插件 允许你在 持续原生生成 (CNG) 项目中自定义使用 npx expo prebuild 生成的原生 Android 和 iOS 项目。你可以使用它们向原生配置文件添加属性,将资源复制到原生项目,或者应用高级配置,例如添加 应用扩展目标

作为应用开发者,配置插件可以帮助你应用默认 应用配置 中未提供的自定义功能。作为库作者,它们使你能够为使用你库的开发者自动配置原生项目。

🌐 As an app developer, config plugins help you apply customizations not exposed in the default app config. As a library author, they enable you to configure native projects automatically for developers using your library.

本教程解释了如何从头创建一个新的配置插件,并从 Expo 模块中读取你的插件注入到 AndroidManifest.xmlInfo.plist 的自定义值。

🌐 This tutorial explains how to create a new config plugin from scratch and read custom values that your plugin injects into AndroidManifest.xml and Info.plist from an Expo module.

1

初始化模块

🌐 Initialize a module

首先使用 create-expo-module 初始化一个新的 Expo 模块项目。这将为 Android、iOS 和 TypeScript 设置脚手架,并包含一个用于在应用中测试模块的示例项目。运行以下命令以开始:

🌐 Start by initializing a new Expo module project with create-expo-module. This sets up scaffolding for Android, iOS, and TypeScript and includes an example project to test the module within an app. Run the following command to get started:

Terminal
npx create-expo-module expo-native-configuration

本指南使用 expo-native-configuration/ExpoNativeConfiguration 作为模块项目的名称。不过,你可以选择任何你喜欢的名称。

🌐 This guide uses the name expo-native-configuration/ExpoNativeConfiguration for the module project. However, you can choose any name you prefer.

2

设置工作区

🌐 Set up workspace

在这个示例中,你不需要 create-expo-module 包含的 view 模块。使用以下命令清理默认模块:

🌐 In this example, you don't need the view module included by create-expo-module. Clean up the default module with the following command:

Terminal
cd expo-native-configuration
rm android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationView.kt
rm ios/ExpoNativeConfigurationView.swift
rm src/ExpoNativeConfigurationView.tsx src/ExpoNativeConfiguration.types.ts
rm src/ExpoNativeConfigurationView.web.tsx src/ExpoNativeConfigurationModule.web.ts

找到以下文件,并用提供的最小化模板替换它们:

🌐 Locate the following files and replace them with the provided minimal boilerplate:

  • android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt
  • ios/ExpoNativeConfigurationModule.swift
  • src/ExpoNativeConfigurationModule.ts
  • src/index.ts
  • 示例/App.tsx
  • package.json
android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt
package expo.modules.nativeconfiguration import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoNativeConfigurationModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoNativeConfiguration") Function("getApiKey") { return@Function "api-key" } } }
ios/ExpoNativeConfigurationModule.swift
import ExpoModulesCore public class ExpoNativeConfigurationModule: Module { public func definition() -> ModuleDefinition { Name("ExpoNativeConfiguration") Function("getApiKey") { () -> String in "api-key" } } }
src/ExpoNativeConfigurationModule.ts
import { NativeModule, requireNativeModule } from 'expo'; declare class ExpoNativeConfigurationModule extends NativeModule { getApiKey(): string; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoNativeConfigurationModule>('ExpoNativeConfiguration');
src/index.ts
import ExpoNativeConfigurationModule from './ExpoNativeConfigurationModule'; export function getApiKey(): string { return ExpoNativeConfigurationModule.getApiKey(); }
example/App.tsx
import * as ExpoNativeConfiguration from 'expo-native-configuration'; import { Text, View } from 'react-native'; export default function App() { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>API key: {ExpoNativeConfiguration.getApiKey()}</Text> </View> ); }
package.json
{ %%placeholder-start%%... %%placeholder-end%% "dependencies": { "expo-native-configuration": "file:.." %%placeholder-start%%... %%placeholder-end%% } }

3

运行示例项目

🌐 Run the example project

在项目的根目录下,运行 TypeScript 编译器以监视更改并重建模块的 JavaScript:

🌐 In the root of your project, run the TypeScript compiler to watch for changes and rebuild the module's JavaScript:

Terminal
# Run this in the root of the project to start the TypeScript compiler
npm run build

在另一个终端窗口中,编译并运行示例应用:

🌐 In another terminal window, compile and run the example app:

Terminal
# Navigate to the example project
cd example

# Reinstall the dependencies
rm -rf node_modules && npm install

# Run the example app on Android
npx expo run:android

# Run the example app on iOS
npx expo run:ios

你应该会看到一个屏幕,上面显示文本“API key: api-key”。

🌐 You should see a screen with the text "API key: api-key".

4

创建新的配置插件

🌐 Create a new config plugin

插件 是同步函数,接受一个 ExpoConfig 并返回一个修改后的 ExpoConfig。按照惯例,这些函数的前缀使用 with。将你的插件命名为 withMyApiKey,或者使用其他名称,只要遵循此约定即可。

这是一个基本配置插件函数的示例:

🌐 Here is an example of a basic config plugin function:

const withMyApiKey = config => { return config; };

你也可以使用 mods,它们是异步函数,用于修改本地项目中的文件,例如源码或配置文件(plist、xml)。mods 对象与其他应用配置不同,因为它在初始读取后不会被序列化。这使你能够在代码生成过程中执行操作。

🌐 You can also use mods, which are async functions that modify files in native projects, such as source code or configuration files (plist, xml). The mods object is different from the rest of the app config because it doesn't serialize after the initial reading. This allows you to perform actions during code generation.

编写配置插件时,请考虑以下因素:

🌐 When writing config plugins, follow these considerations:

  • 插件必须是同步的,并且它们的返回值必须可序列化,添加的任何 mods 除外。
  • plugins 会在 expo/configgetConfig 方法读取配置时被调用。相比之下,mods 仅在 npx expo prebuild 的“同步”阶段被调用。

虽然是可选的,但可以使用 expo-module-scripts 来简化插件开发。它为 TypeScript 和 Jest 提供了推荐的默认配置。更多信息,请参阅 配置插件指南

使用这个最小化的模板开始创建你的插件。创建一个用于用 TypeScript 编写插件的 plugin 目录,并在项目根目录下添加一个 app.plugin.js 文件,这将作为插件的入口文件。

🌐 Start creating your plugin with this minimal boilerplate. Create a plugin directory for writing the plugin in TypeScript and add an app.plugin.js file in the project root, which will be the plugin's entry point.

创建一个 plugin/tsconfig.json 文件

🌐 Create a plugin/tsconfig.json file

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

为你的插件创建一个 plugin/src/index.ts 文件

🌐 Create a plugin/src/index.ts file for your plugin

plugin/src/index.ts
import { ConfigPlugin } from 'expo/config-plugins'; const withMyApiKey: ConfigPlugin = config => { console.log('my custom plugin'); return config; }; export default withMyApiKey;

在根目录下创建一个 app.plugin.js 文件

🌐 Create an app.plugin.js file in the root directory

app.plugin.js
// This file configures the entry file for your plugin. module.exports = require('./plugin/build');

在你的项目根目录下,运行 npm run build plugin 来以监听模式启动 TypeScript 编译器。接下来,通过将以下行添加到 example/app.json 文件中,配置你的示例项目使用你的插件:

🌐 At the root of your project, run npm run build plugin to start the TypeScript compiler in watch mode. Next, configure your example project to use your plugin by adding the following line to the example/app.json file:

example/app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": ["../app.plugin.js"] } }

当你在 example 目录中运行 npx expo prebuild 命令时,终端会通过控制台语句记录“my custom plugin”。

🌐 When you run the npx expo prebuild command inside your example directory, the terminal logs "my custom plugin" through a console statement.

Terminal
cd example
npx expo prebuild --clean

要将自定义 API 密钥注入 AndroidManifest.xmlInfo.plist,请使用 expo/config-plugins 提供的帮助工具 mods。这些工具可以轻松修改原生文件。在本例中,请使用 withAndroidManifestwithInfoPlist

🌐 To inject your custom API keys into AndroidManifest.xml and Info.plist, use helper mods provided by expo/config-plugins. These make it easy to modify native files. For this example, use withAndroidManifest and withInfoPlist.

顾名思义,withAndroidManifest 允许你读取和修改 AndroidManifest.xml 文件。使用 AndroidConfig 辅助工具可以向主应用添加一个元数据项,如下所示:

🌐 As the name suggests, withAndroidManifest allows you to read and modify the AndroidManifest.xml file. Use AndroidConfig helpers to add a metadata item to the main application, as shown below:

const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { config = withAndroidManifest(config, config => { const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); AndroidConfig.Manifest.addMetaDataItemToMainApplication( mainApplication, 'MY_CUSTOM_API_KEY', apiKey ); return config; }); return config; };

同样,你可以使用 withInfoPlist 来修改 Info.plist 的值。使用 modResults 属性,你可以像下面的代码示例中那样添加自定义值:

🌐 Similarly, you can use withInfoPlist to modify the Info.plist values. Using the modResults property, you can add custom values as shown in the code snippet below:

const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { config = withInfoPlist(config, config => { config.modResults['MY_CUSTOM_API_KEY'] = apiKey; return config; }); return config; };

你可以通过将所有内容合并到一个函数中来创建自定义插件:

🌐 You can create a custom plugin by merging everything into a single function:

plugin/src/index.ts
import { withInfoPlist, withAndroidManifest, AndroidConfig, ConfigPlugin, } from 'expo/config-plugins'; const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => { config = withInfoPlist(config, config => { config.modResults['MY_CUSTOM_API_KEY'] = apiKey; return config; }); config = withAndroidManifest(config, config => { const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults); AndroidConfig.Manifest.addMetaDataItemToMainApplication( mainApplication, 'MY_CUSTOM_API_KEY', apiKey ); return config; }); return config; }; export default withMyApiKey;

插件准备就绪后,更新示例应用,将你的 API 密钥作为配置选项传递给插件。按如下所示修改 example/app.json 中的 plugins 字段:

🌐 With the plugin ready to use, update the example app to pass your API key to the plugin as a configuration option. Modify the plugins field in example/app.json as shown below:

example/app.json
{ "expo": { %%placeholder-start%%... %%placeholder-end%% "plugins": [["../app.plugin.js", { "apiKey": "custom_secret_api" }]] } }

在进行此更改后,通过在 example 目录中运行 npx expo prebuild --clean 来测试插件是否正常工作。此命令会执行你的插件并更新本地文件,将 "MY_CUSTOM_API_KEY" 注入到 AndroidManifest.xmlInfo.plist 中。你可以通过检查 example/android/app/src/main/AndroidManifest.xmlexample/ios/exponativeconfigurationexample/Info.plist 的内容来验证这一点。

🌐 After making this change, test that the plugin works correctly by running npx expo prebuild --clean inside the example directory. This command executes your plugin and updates native files, injecting "MY_CUSTOM_API_KEY" into AndroidManifest.xml and Info.plist. You can verify this by checking the contents of example/android/app/src/main/AndroidManifest.xml and example/ios/exponativeconfigurationexample/Info.plist.

5

从模块读取本地值

🌐 Read native values from the module

现在,使用特定平台的方法让你的原生模块读取添加到 AndroidManifest.xmlInfo.plist 的字段,以访问它们的内容。

🌐 Now, make your native module read the fields added to AndroidManifest.xml and Info.plist by using platform-specific methods to access their contents.

在 Android 上,可以使用 packageManager 类从 AndroidManifest.xml 文件中访问元数据信息。要读取 "MY_CUSTOM_API_KEY" 值,请更新 android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt 文件:

🌐 On Android, access metadata information from the AndroidManifest.xml file using the packageManager class. To read the "MY_CUSTOM_API_KEY" value, update the android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt file:

android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt
package expo.modules.nativeconfiguration import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import android.content.pm.PackageManager class ExpoNativeConfigurationModule() : Module() { override fun definition() = ModuleDefinition { Name("ExpoNativeConfiguration") Function("getApiKey") { val applicationInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(appContext?.reactContext?.packageName.toString(), PackageManager.GET_META_DATA) return@Function applicationInfo?.metaData?.getString("MY_CUSTOM_API_KEY") } } }

在 iOS 上,你可以使用 Bundle.main.object(forInfoDictionaryKey: "") 方法读取 Info.plist 属性的内容。要访问之前添加的 "MY_CUSTOM_API_KEY" 值,请按如下方式更新 ios/ExpoNativeConfigurationModule.swift 文件:

🌐 On iOS, you can read the content of an Info.plist property using the Bundle.main.object(forInfoDictionaryKey: "") method. To access the "MY_CUSTOM_API_KEY" value added earlier, update the ios/ExpoNativeConfigurationModule.swift file as shown:

ios/ExpoNativeConfigurationModule.swift
import ExpoModulesCore public class ExpoNativeConfigurationModule: Module { public func definition() -> ModuleDefinition { Name("ExpoNativeConfiguration") Function("getApiKey") { return Bundle.main.object(forInfoDictionaryKey: "MY_CUSTOM_API_KEY") as? String } } }

6

运行你的模块

🌐 Run your module

通过你的原生模块读取添加到原生文件的字段,你现在可以运行示例应用,并使用 ExamplePlugin.getApiKey() 函数访问你的自定义 API 密钥。

🌐 With your native modules reading the fields added to the native files, you can now run the example app and access your custom API key using the ExamplePlugin.getApiKey() function.

Terminal
cd example

# Execute your plugin and update native files
npx expo prebuild

# Run the example app on Android
npx expo run:android

# Run the example app on iOS
npx expo run:ios

下一步

🌐 Next steps

恭喜,你已创建与 Android 和 iOS 的 Expo 模块交互的配置插件!

🌐 Congratulations, you have created a config plugin that interacts with an Expo module for Android and iOS!

如果你想挑战自己并使插件更加多功能,这个练习适合你。修改插件以允许传入任意的配置键和值,并添加从模块读取任意键的功能。

🌐 If you want to challenge yourself and make the plugin more versatile, this exercise is open for you. Modify the plugin to allow any arbitrary set of config keys and values to be passed in, and add functionality to read arbitrary keys from the module.

Expo 模块 API 参考

创建使用 Kotlin 和 Swift 的原生模块的参考资料。

额外的平台支持

了解如何添加对 macOS 和 tvOS 平台的支持。