教程:使用配置插件创建模块

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


配置插件可让你自定义使用 npx expo prebuild 生成的原生 Android 和 iOS 项目。你可以使用它们将属性添加到原生配置文件、将资源复制到原生项目或应用高级配置,例如添加 应用扩展目标

¥Config plugins let you customize native Android and iOS projects generated with npx expo prebuild. You can use them to add properties to native config files, copy assets to native projects, or apply advanced configurations, such as adding an app extension target.

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

¥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.xml 和 Info.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

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

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

2

Set up workspace

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
  • example/App.tsx
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>
  );
}

3

Run the example project

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
# Run the example app on iOS
npx expo run:ios
# Run the example app on Android
npx expo run:android

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

4

Create a new config plugin

Plugins are synchronous functions that accept an ExpoConfig and return a modified ExpoConfig. By convention, these functions are prefixed with the word with. Name your plugin withMyApiKey or use a different name, as long as it follows this convention.

Here is an example of a basic config plugin function:

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

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:

  • Plugins must be synchronous, and their return value must be serializable, except for any mods that are added.
  • plugins are invoked whenever the getConfig method from expo/config reads the configuration. In contrast, mods are invoked only during the "syncing" phase of npx expo prebuild.

Although optional, use expo-module-scripts to simplify plugin development. It provides a recommended default configuration for TypeScript and Jest. For more information, see the config plugins guide.

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.

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__/*"]
}

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;

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

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": {
    ...
    "plugins": ["../app.plugin.js"]
  }
}

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

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.

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

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;

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": {
    ...
    "plugins": [["../app.plugin.js", { "apiKey": "custom_secret_api" }]]
  }
}

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.

5

Read native values from the module

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

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

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

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 our 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 平台的支持。