首页指南参考教程

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

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


配置插件允许你自定义使用 npx expo prebuild 生成的原生 Android 和 iOS 项目。在原生配置文件中添加属性、将资源复制到原生项目以及添加应用扩展目标等高级配置通常很有用。作为应用开发者,应用默认 应用配置 中未公开的自定义设置可能会有所帮助。作为库作者,它允许你自动为使用库的开发者配置原生项目。

¥Config plugins allow you to customize native Android and iOS projects when they are generated with npx expo prebuild. It is often useful to add properties in native config files, to copy assets to native projects, and for advanced configurations such as adding an app extension target. As an app developer, applying customizations not exposed in the default app config can be helpful. As a library author, it allows you to configure native projects for the developers using your library automatically.

本指南将引导你从头开始创建一个新的配置插件,并向你展示如何读取由你的插件从 Expo 模块注入到 AndroidManifest.xml 和 Info.plist 中的自定义值。

¥This guide will walk you through creating a new config plugin from scratch and show you how to read custom values injected into AndroidManifest.xml and Info.plist by your plugin from an Expo module.

1. 初始化模块

¥ Initialize a module

首先使用 create-expo-module 初始化一个新的 Expo 模块项目,该项目将为 Android、iOS 和 TypeScript 提供脚手架。它还将提供一个示例项目,用于从应用内与模块进行交互。运行以下命令对其进行初始化:

¥Start by initializing a new Expo module project using create-expo-module, which will provide scaffolding for Android, iOS, and TypeScript. It will also provide an example project to interact with the module from within an app. Run the following command to initialize it:

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

我们将为该项目使用名称 expo-native-configuration/ExpoNativeConfiguration。你可以将其命名为任何你喜欢的名称。

¥We will use the name expo-native-configuration/ExpoNativeConfiguration for the project. You can name it whatever you like.

2. 设置我们的工作区

¥ Set up our workspace

在我们的示例中,我们不需要 create-expo-module 包含的视图模块。让我们使用以下命令稍微清理一下默认模块:

¥In our example, we won't need the view module included by create-expo-module. Let's clean up the default module a little bit with the following command:

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

我们还需要找到 ExpoNativeConfigurationModule.swift、ExpoNativeConfigurationModule.kt、src/index.ts 和 example/App.tsx 并将它们替换为提供的最小样板:

¥We also need to find ExpoNativeConfigurationModule.swift, ExpoNativeConfigurationModule.kt, src/index.ts and example/App.tsx and replace them with the provided minimal boilerplate:

ios/ExpoNativeConfigurationModule.swift
import ExpoModulesCore

public class ExpoNativeConfigurationModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ExpoNativeConfiguration")

    Function("getApiKey") { () -> String in
      "api-key"
    }
  }
}
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"
    }
  }
}
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

现在让我们运行示例项目以确保一切正常。启动 TypeScript 编译器以观察更改并重建模块 JavaScript。

¥Now let's run the example project to make sure everything is working. Start the TypeScript compiler to watch for changes and rebuild the module 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
cd example
# Run the example app on iOS
npx expo run:ios
# Run the example app on Android
npx expo run:android

我们应该看到一个屏幕,上面有文本“API key: api-key”。现在让我们开发插件来注入我们的自定义 API 密钥。

¥We should see a screen with a text saying API key: api-key. Now let's develop the plugin to inject our custom API key.

4. 创建一个新的配置插件

¥ Creating a new config plugin

让我们开始开发新的配置插件。插件是同步函数,接受 ExpoConfig 并返回修改后的 ExpoConfig。按照惯例,这些函数以单词 with 为前缀。我们将插件命名为 withMyApiKey。只要遵循约定,你可以随意调用它。

¥Let's start developing our new config plugin. Plugins are synchronous functions that accept an ExpoConfig and return a modified ExpoConfig. By convention, these functions are prefixed by the word with. We will name our plugin withMyApiKey. Feel free to call it whatever you like, as long as it follows the convention.

以下是基本配置插件功能的示例:

¥Here is an example of how a basic config plugin function looks:

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

此外,你还可以使用 mods,它们是修改原生项目中的文件(例如源代码或配置(plist、xml)文件)的异步函数。mods 对象与应用配置的其余部分不同,因为它在初始读取后不会被序列化。这意味着你可以使用它在代码生成期间执行操作。

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

但是,在编写配置插件时,我们应该遵循一些注意事项:

¥However, there are a few considerations that we should follow when writing config plugins:

  • 插件应该是同步的,并且它们的返回值应该是可序列化的,除了添加的任何 mods 之外。

    ¥Plugins should be synchronous and their return value should be serializable, except for any mods that are added.

  • expo/config 方法 getConfig 读取配置时,始终会调用 plugins。然而,mods 仅在 npx expo prebuild 的 "syncing" 阶段被调用。

    ¥plugins are always invoked when the config is read by the expo/config method getConfig. However, mods are only invoked during the "syncing" phase of npx expo prebuild.

虽然不是必需的,但我们可以使用 expo-module-scripts 来使插件开发更容易 - 它为 TypeScript 和 Jest 提供了推荐的默认配置。欲了解更多信息,请参阅 配置插件指南

¥Although not required, we can use expo-module-scripts to make plugin development easier — it provides a recommended default configuration for TypeScript and Jest. For more information, see config plugins guide.

让我们首先使用这个最小的样板文件创建我们的插件。这将创建一个插件文件夹,我们将在其中用 TypeScript 编写插件,并在项目根目录中添加 app.plugin.js 文件,这将是插件的入口文件。

¥Let's start by creating our plugin with this minimal boilerplate. This will create a plugin folder where we will write the plugin in TypeScript and add an app.plugin.js file in the project root, which will be the entry file for the plugin.

  1. 创建一个 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__/*"]
    }
    
  2. 为我们的插件创建一个 plugin/src/index.ts 文件:

    ¥Create a plugin/src/index.ts file for our 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;
    
  3. 最后,在根目录下创建 app.plugin.js 文件。这将为我们的插件配置入口文件:

    ¥Finally, create an app.plugin.js file in the root directory. That will configure the entry file for our plugin:

    app.plugin.js
    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. The only thing left to configure is our example project to use our plugin. We can achieve this by adding the following line to the example/app.json file.

example/app.json
{
  "expo": {
    ...
    "plugins": ["../app.plugin.js"]
  }
}

现在,当在示例文件夹中运行 npx expo prebuild 时,我们应该在终端中看到 '我的自定义插件' console.log 语句。

¥Now when running npx expo prebuild inside our example folder we should see our 'my custom plugin' console.log statement in the terminal.

Terminal
cd example
npx expo prebuild --clean

要将自定义 API 密钥注入 AndroidManifest.xml 和 Info.plist,我们可以使用一些辅助程序 modsexpo/config-plugins 提供,这使得修改原生文件变得很容易。在我们的示例中,我们将使用其中两个,withAndroidManifestwithInfoPlist

¥To inject our custom API keys into AndroidManifest.xml and Info.plist we can use a few helper mods provided by expo/config-plugins, which makes it easy to modify native files. In our example, we will use two of them, withAndroidManifest and withInfoPlist.

顾名思义,withInfoPlist 允许我们读取和修改 Info.plist 值。使用 modResults 属性,我们可以添加自定义值,如下面的代码片段所示:

¥As the name suggests, withInfoPlist allows us to read and modify Info.plist values. Using the modResults property, we can add custom values as demonstrated 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;
};

同样,我们可以使用 withAndroidManifest 来修改 AndroidManifest.xml 文件。在这种情况下,我们将利用 AndroidConfig 助手将元数据项添加到主应用:

¥Similarly, we can use withAndroidManifest to modify the AndroidManifest.xml file. In this case, we will utilize AndroidConfig helpers to add a metadata item to the main application:

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

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

¥We can create our 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" 字段,如下所示:

¥Now with the plugin ready to be used, let's update the example app to pass our 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" }]]
  }
}

进行此更改后,我们可以通过在示例文件夹中运行命令 npx expo prebuild --clean 来测试插件是否正常工作。这将执行我们的插件并更新原生文件,将 "MY_CUSTOM_API_KEY" 注入 AndroidManifest.xml 和 Info.plist。你可以通过检查 example/android/app/src/main/AndroidManifest.xml 的内容来验证这一点。

¥After making this change, we can test that the plugin is working correctly by running the command npx expo prebuild --clean inside the example folder. This will execute our plugin and update 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. 从模块读取原生值

¥ Reading native values from the module

现在让我们的原生模块读取我们添加到 AndroidManifest.xml 和 Info.plist 中的字段。这可以通过使用特定于平台的方法来访问这些文件的内容来完成。

¥Now let's make our native module read the fields we added to AndroidManifest.xml and Info.plist. This can be done by using platform-specific methods to access the contents of these files.

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

¥On iOS, we can read the content of an Info.plist property by using the Bundle.main.object(forInfoDictionaryKey: "") instance Method. To read the "MY_CUSTOM_API_KEY" value that we added earlier, update the ios/ExpoNativeConfigurationModule.swift file:

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

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

¥On Android, we can 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")
    }
  }
}

6. 运行你的模块

¥ Running your module

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

¥With our native modules reading the fields we added to the native files, we can now run the example app and access our custom API key through the ExamplePlugin.getApiKey() function.

Terminal
cd example
# execute our plugin and update native files
npx expo prebuild
# Run the example app on iOS
npx expo run:ios
# Run the example app on Android
npx expo run:android

下一步

¥Next steps

恭喜,你已经创建了一个简单但不平凡的配置插件,可以与 Android 和 iOS 的 Expo 模块进行交互!

¥Congratulations, you have created a simple yet non-trivial config plugin that interacts with an Expo module for Android and iOS!

如果你想挑战自己并使插件更加通用,我们将这个练习留给你。尝试修改插件以允许传入任意一组配置键/值,并添加功能以允许从模块读取任意键。

¥If you want to challenge yourself and make the plugin more versatile we leave this exercise open to you. Try modifying the plugin to allow for any arbitrary set of config keys/values to be passed in and adding the functionality to allow for the reading of arbitrary keys from the module.

Expo 中文网 - 粤ICP备13048890号