创建配置插件时了解什么是插件和模组。
插件是同步函数,接受 ExpoConfig
并返回修改后的 ExpoConfig
。
¥Plugins are synchronous functions that accept an ExpoConfig
and return a modified ExpoConfig
.
插件应使用以下约定命名:with<Plugin Functionality>
,例如 withFacebook
。
¥Plugins should be named using the following convention: with<Plugin Functionality>
, for example, withFacebook
.
插件应该是同步的,并且它们的返回值应该是可序列化的,除了添加的任何 mods
之外。
¥Plugins should be synchronous and their return value should be serializable, except for any mods
that are added.
或者,可以将第二个参数传递给插件来配置它。
¥Optionally, a second argument can be passed to the plugin to configure it.
当 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, the mods
are only invoked during the "syncing" phase of npx expo prebuild
.
¥Create a plugin
这是最基本的配置插件的示例:
¥Here is an example of the most basic config plugin:
const withNothing = config => config;
假设你想创建一个向 iOS 项目中的 Info.plist 添加自定义值的插件:
¥Say you wanted to create a plugin that added custom values to Info.plist in an iOS project:
const withMySDK = (config, { apiKey }) => {
if (!config.ios) {
config.ios = {};
}
if (!config.ios.infoPlist) {
config.ios.infoPlist = {};
}
config.ios.infoPlist['MY_CUSTOM_NATIVE_IOS_API_KEY'] = apiKey;
return config;
};
module.exports.withMySDK = withMySDK;
要使用该插件,请导入它并封装配置:
¥To use the plugin, import it and wrap the config:
const { withMySDK } = require('./my-plugin');
const config = {
name: 'my app',
};
module.exports = withMySDK(config, { apiKey: 'X-XXX-XXX' });
¥Import a plugin
你可能想在不同的文件中创建插件,方法如下:
¥You may want to create a plugin in a different file, here's how:
根文件可以是任何 JS 文件或 Node 模块根目录中名为 app.plugin.js 的文件。
¥The root file can be any JS file or a file named app.plugin.js in the root of a Node module.
该文件应导出满足 ConfigPlugin
类型的函数。
¥The file should export a function that satisfies the ConfigPlugin
type.
应提前针对 Node 环境转换插件!
¥Plugins should be transpiled for Node environments ahead of time!
他们应该支持 Expo 支持 (LTS) 的 Node 版本。
¥They should support the versions of Node that Expo supports (LTS).
没有 import/export
关键字,请在附带的插件文件中使用 module.exports
。
¥No import/export
keywords, use module.exports
in the shipped plugin file.
Expo 仅转换用户的初始 app.config
文件,更多内容将需要一个打包器,它会为配置文件添加太多 "opinions"。
¥Expo only transpiles the user's initial app.config
file, anything more would require a bundler which would add too many "opinions" for a config file.
考虑以下更改配置名称的示例:
¥Consider the following example that changes the config name:
app.config.js
Expo config
my-plugin.js
Custom Config Plugin file
module.exports = function withPrefixedName(config, prefix) {
// Modify the config
config.name = prefix + '-' + config.name;
// Return the results
return config;
};
{
"name": "my-app",
"plugins": [["./my-plugin", "custom"]]
}
其计算结果为以下 JSON 配置:
¥It evaluates to the following JSON config:
{
"name": "custom-my-app",
"plugins": [["./my-plugin", "custom"]]
}
¥Chain plugins
一旦添加了一些插件,你的 app.config.js 代码就会变得难以阅读和操作。为了解决这个问题,expo/config-plugins
提供了 withPlugins
函数,可用于将插件链接在一起并按顺序执行它们。
¥Once you add a few plugins, your app.config.js code can become difficult to read and manipulate. To combat this, expo/config-plugins
provides a withPlugins
function which can be used to chain plugins together and execute them in order.
/// Create a config
const config = {
name: 'my app',
};
// ❌ Hard to read
withDelta(withFoo(withBar(config, 'input 1'), 'input 2'), 'input 3');
// ✅ Easy to read
import { withPlugins } from 'expo/config-plugins';
withPlugins(config, [
[withBar, 'input 1'],
[withFoo, 'input 2'],
// When no input is required, you can just pass the method...
withDelta,
]);
为了支持 JSON 配置,我们还添加了 plugins
数组,该数组在底层仅使用 withPlugins
。这是与上面相同的配置,但更简单:
¥To support JSON configs, we also added the plugins
array which just uses withPlugins
under the hood.
Here is the same config as above, but even simpler:
export default {
name: 'my app',
plugins: [
[withBar, 'input 1'],
[withFoo, 'input 2'],
[withDelta, 'input 3'],
],
};
¥What are mods
修饰符(简称 mod)是一个异步函数,它接受配置和数据对象,然后将两者作为对象进行操作和返回。
¥A modifier (mod for short) is an async function that accepts a config and a data object, then manipulates and returns both as an object.
Mod 被添加到应用配置的 mods
对象中。mods
对象与应用配置的其余部分不同,因为它在初始读取后不会被序列化,这意味着你可以使用它在代码生成期间执行操作。如果可能的话,你应该尝试使用基本插件而不是模组,因为它们更容易使用。
¥Mods are added to the mods
object of the app config. The mods
object is different from the rest of the app config because it doesn't get serialized
after the initial reading, which means you can use it to perform actions during code generation.
If possible, you should attempt to use basic plugins instead of mods, as they're simpler to work with.
mods
从清单中省略,无法通过 Updates.manifest
访问。Mod 存在的唯一目的是在代码生成期间修改原生项目文件!
¥mods
are omitted from the manifest and cannot be accessed via Updates.manifest
. Mods exist for the sole purpose of modifying native project files during code generation!
mods
可用于在 npx expo prebuild
命令期间安全地读写文件。这就是 Expo CLI 修改 Info.plist、权利、xcproj 等的方式。
¥mods
can be used to read and write files safely during the npx expo prebuild
command. This is how Expo CLI modifies the Info.plist, entitlements, xcproj, and so on.
mods
是特定于平台的,应始终添加到特定于平台的对象中:
¥mods
are platform-specific and should always be added to a platform-specific object:
module.exports = {
name: 'my-app',
mods: {
ios: {
/* iOS mods... */
},
android: {
/* Android mods... */
},
},
};
¥How mods work
使用 getPrebuildConfig
从 @expo/prebuild-config
读取配置。
¥The config is read using getPrebuildConfig
from @expo/prebuild-config
.
Expo 支持的所有核心功能都是通过 withIosExpoPlugins
中的插件添加的。这包括名称、版本、图标、区域设置等。
¥All of the core functionality supported by Expo is added via plugins in withIosExpoPlugins
. This includes name, version, icons, locales, and so on.
配置被传递给编译器 compileModsAsync
¥The config is passed to the compiler compileModsAsync
编译器添加了负责读取数据(如 Info.plist)、执行命名 mod(如 mods.ios.infoPlist
)的基本 mod,然后将结果写入文件系统。
¥The compiler adds base mods that are responsible for reading data (like Info.plist), executing a named mod (like mods.ios.infoPlist
), then writing the results to the file system.
编译器迭代所有 mod 并异步评估它们,提供一些基本属性,例如 projectRoot
。
¥The compiler iterates over all the mods and asynchronously evaluates them, providing some base props like the projectRoot
.
在每个 mod 之后,错误处理都会断言 mod 链是否被无效 mod 损坏。
¥After each mod, error handling asserts if the mod chain was corrupted by an invalid mod.
¥Default mods
mod 编译器为常见文件操作提供了以下默认 mod。
¥The following default mods are provided by the mod compiler for common file manipulation.
危险的修改依赖正则表达式(regex)来修改应用代码,这可能会导致构建中断。正则表达式 mod 也很难进行版本控制,因此应谨慎使用。始终选择使用应用代码来修改应用代码,即 Expo 模块 原生 API。
¥Dangerous modifications rely on regular expressions (regex) to modify application code, which may cause the build to break. Regex mods are also difficult to version, and therefore should be used sparingly. Always opt towards using application code to modify application code, that is, Expo Modules native API.
安卓模组 | 危险的 | 描述 |
---|---|---|
mods.android.manifest | * | 修改 android/app/src/main/AndroidManifest.xml 为 JSON(用 xml2js 解析)。 |
mods.android.strings | * | 修改 android/app/src/main/res/values/strings.xml 为 JSON(用 xml2js 解析)。 |
mods.android.colors | * | 将 android/app/src/main/res/values/colors.xml 修改为 JSON(使用 xml2js 解析)。 |
mods.android.colorsNight | * | 将 android/app/src/main/res/values-night/colors.xml 修改为 JSON(使用 xml2js 解析)。 |
mods.android.styles | * | 修改 android/app/src/main/res/values/styles.xml 为 JSON(用 xml2js 解析)。 |
mods.android.gradleProperties | * | 修改 android/gradle.properties 为 Properties.PropertiesItem[] 。 |
mods.android.mainActivity | 将 android/app/src/main/<package>/MainActivity.java 修改为字符串。 | |
mods.android.mainApplication | 将 android/app/src/main/<package>/MainApplication.java 修改为字符串。 | |
mods.android.appBuildGradle | 将 android/app/build.gradle 修改为字符串。 | |
mods.android.projectBuildGradle | 将 android/build.gradle 修改为字符串。 | |
mods.android.settingsGradle | 将 android/settings.gradle 修改为字符串。 |
iOS 模组 | 危险的 | 描述 |
---|---|---|
mods.ios.infoPlist | * | 修改 ios/<name>/Info.plist 为 JSON(用 @expo/plist 解析)。 |
mods.ios.entitlements | * | 将 ios/<name>/<product-name>.entitlements 修改为 JSON(用 @expo/plist 解析)。 |
mods.ios.expoPlist | * | 将 ios/<ame>/Expo.plist 修改为 JSON(Expo 更新 iOS 的配置)(用 @expo/plist 解析)。 |
mods.ios.xcodeproj | * | 修改 ios/<name>.xcodeproj 为 XcodeProject 对象(用 xcode 解析)。 |
mods.ios.podfile | * | 将 ios/Podfile 修改为字符串。 |
mods.ios.podfileProperties | * | 将 ios/Podfile.properties.json 修改为 JSON。 |
mods.ios.appDelegate | 将 ios/<name>/AppDelegate.m 修改为字符串。 |
解析 mod 后,每个 mod 的内容将被写入磁盘。可以添加自定义默认 mods 以支持新的原生文件。例如,你可以创建一个 mod 来支持 GoogleServices-Info.plist
,并将其传递给其他 mod。
¥After the mods are resolved, the contents of each mod will be written to disk. Custom default mods can be added to support new native files.
For example, you can create a mod to support the GoogleServices-Info.plist
, and pass it to other mods.
¥Mod plugins
Mod 负责很多任务,所以一开始它们可能很难理解。如果你正在开发需要模组的功能,最好不要直接与它们交互。
¥Mods are responsible for a lot of tasks, so they can be pretty difficult to understand at first. If you're developing a feature that requires mods, it's best not to interact with them directly.
相反,你应该使用 expo/config-plugins
提供的辅助模块:
¥Instead you should use the helper mods provided by expo/config-plugins
:
¥Android
安卓模组 | 模组插件 | 危险的 |
---|---|---|
mods.android.manifest | withAndroidManifest | * |
mods.android.strings | withStringsXml | * |
mods.android.colors | withAndroidColors | * |
mods.android.colorsNight | withAndroidColorsNight | * |
mods.android.styles | withAndroidStyles | * |
mods.android.gradleProperties | withGradleProperties | * |
mods.android.mainActivity | withMainActivity | |
mods.android.mainApplication | withMainApplication | |
mods.android.appBuildGradle | withAppBuildGradle | |
mods.android.projectBuildGradle | withProjectBuildGradle | |
mods.android.settingsGradle | withSettingsGradle |
¥iOS
iOS 模组 | 模组插件 | 危险的 |
---|---|---|
mods.ios.infoPlist | withInfoPlist | * |
mods.ios.entitlements | withEntitlementsPlist | * |
mods.ios.expoPlist | withExpoPlist | * |
mods.ios.xcodeproj | withXcodeProject | * |
mods.ios.podfile | withPodfile | * |
mods.ios.podfileProperties | withPodfileProperties | * |
mods.ios.appDelegate | withAppDelegate |
mod 插件传递一个 config
对象,并添加了附加属性 modResults
和 modRequest
。
¥A mod plugin gets passed a config
object with additional properties modResults
and modRequest
added to it.
modResults
:要修改并返回的对象。类型取决于正在使用的模组。
¥modResults
: The object to modify and return. The type depends on the mod that's being used.
modRequest
:mod 编译器提供的附加属性。
¥modRequest
: Additional properties supplied by the mod compiler.
projectRoot: string
:通用应用的项目根目录。
¥projectRoot: string
: Project root directory for the universal app.
platformProjectRoot: string
:特定平台的项目根。
¥platformProjectRoot: string
: Project root for the specific platform.
modName: string
:模组名称。
¥modName: string
: Name of the mod.
platform: ModPlatform
:mods 配置中使用的平台名称。
¥platform: ModPlatform
: Name of the platform used in the mods config.
projectName?: string
:(仅限 iOS)用于查询项目文件的路径组件。ex.projectRoot/ios/[projectName]/
¥projectName?: string
: (iOS only) The path component used for querying project files. ex. projectRoot/ios/[projectName]/
¥Create a mod
假设你想编写一个 mod 来更新 Xcode 项目的 "产品名称":
¥Say you wanted to write a mod to update the Xcode Project's "product name":
import { ConfigPlugin, withXcodeProject, IOSConfig } from 'expo/config-plugins';
const withCustomProductName: ConfigPlugin<string> = (config, customName) => {
return withXcodeProject(
config,
async (
config
) => {
config.modResults = IOSConfig.Name.setProductName({ name: customName }, config.modResults);
return config;
}
);
};
// 💡 Usage:
/// Create a config
const config = {
name: 'my app',
};
/// Use the plugin
export default withCustomProductName(config, 'new_name');
¥Experimental functionality
mod 系统的某些部分尚未完全充实,这些部分使用 withDangerousMod
在没有基础 mod 的情况下读取/写入数据。这些方法本质上充当它们自己的基础 mod 并且无法扩展。例如,图标目前使用危险模式来执行单个生成步骤,无法自定义结果。
¥Some parts of the mod system aren't fully fleshed out, these parts use withDangerousMod
to read/write data without a base mod.
These methods essentially act as their own base mod and cannot be extended.
Icons, for example, currently use the dangerous mod to perform a single generation step with no ability to customize the results.
export const withIcons = config => {
return withDangerousMod(config, [
'ios',
async config => {
await setIconsAsync(config, config.modRequest.projectRoot);
return config;
},
]);
};
请小心使用 withDangerousMod
,因为它将来可能会发生变化。它的执行顺序也不可靠。目前,危险模组先于所有其他修饰符运行,这是因为我们在内部使用危险模组进行大型文件系统重构,例如当包名称更改时。
¥Be careful using withDangerousMod
as it is subject to change in the future.
The order with which it gets executed is not reliable either.
Currently, dangerous mods run first before all other modifiers, this is because we use dangerous mods internally for large file system refactoring like when the package name changes.
¥Plugin module resolution
传递给 plugins
数组的字符串可以通过几种不同的方式解析。
¥The strings passed to the plugins
array can be resolved in a few different ways.
下面未指定的任何解决模式都是意外行为,并且可能会发生重大更改。
¥Any resolution pattern that isn't specified below is unexpected behavior, and subject to breaking changes.
¥Project file
你可以在项目中快速创建插件并在配置中使用它。
¥You can quickly create a plugin in your project and use it in your config.
app.config.js
import "./my-config-plugin"
my-config-plugin.js
Imported from config
在此示例中,配置插件文件包含最低限度的功能:
¥In this example, the config plugin file contains a bare minimum function:
module.exports = config => config;
有时你希望你的包导出 React 组件并支持插件。为此,需要使用多个入口点,因为转译(Babel 预设)可能不同。如果 Node 模块文件夹的根目录中存在 app.plugin.js 文件,则将使用该文件而不是包的 main
文件。
¥Sometimes you want your package to export React components and also support a plugin. To do this, multiple entry points need to be used because the transpilation (Babel preset) may be different.
If an app.plugin.js file is present in the root of a Node module's folder, it'll be used instead of the package's main
file.
app.config.js
import "expo-splash-screen"
node_modules
expo-splash-screen
Node module
package.json
"main": "./build/index.js"
app.plugin.js
Entry file for custom plugins
build
index.js
Skipped in favor of app.plugin.js
module.exports = config => config;
¥Node module default file
节点模块中的配置插件(没有 app.plugin.js)将使用 package.json 中定义的 main
文件。
¥A config plugin in a node module (without an app.plugin.js) will use the main
file defined in the package.json.
app.config.js
import "expo-splash-screen"
node_modules
expo-splash-screen
Node module
package.json
"main": "./build/index.js"
build
index.js
Node resolve to this file
¥Project folder
这与 Node 模块中的配置插件的工作方式不同,因为默认情况下不会在目录中解析 app.plugin.js。你必须手动指定 ./my-config-plugin/app.plugin.js
才能使用它,否则将使用目录中的 index.js。
¥This is different to how Config Plugins in Node modules work because app.plugin.js won't be resolved by default in a directory. You'll have to manually specify ./my-config-plugin/app.plugin.js
to use it, otherwise index.js in the directory will be used.
app.config.js
import "./my-config-plugin"
my-config-plugin
index.js
Config Plugin
app.plugin.js
Skipped outside of a node module
¥Module internals
避免导入模块内部结构。
如果直接导入 Node 模块内的文件,则将跳过该模块的根 app.plugin.js 解析。这称为 "到达包内部",被认为是不好的形式。我们支持这一点是为了使测试和插件创作更容易,但我们不希望库作者将他们的插件像这样公开为公共 API。
¥If a file inside a Node module is directly imported, then the module's root app.plugin.js resolution will be skipped. This is referred to as "reaching inside a package" and is considered bad form. We support this to make testing, and plugin authoring easier, but we don't expect library authors to expose their plugins like this as a public API.
app.config.js
import "expo-splash-screen/build/index.js"
node_modules
expo-splash-screen
package.json
"main": "./build/index.js"
app.plugin.js
Ignored due to direct import
build
index.js
expo-splash-screen/build/index.js
¥Raw functions
Expo 配置对象还支持将函数按原样传递到 plugins
数组。这对于测试很有用,或者如果你想在不创建文件的情况下使用插件。
¥Expo config objects also support passing functions as-is to the plugins
array. This is useful for testing, or if you want to use a plugin without creating a file.
const withCustom = (config, props) => config;
const config = {
plugins: [
[
withCustom,
{
/* props */
},
],
withCustom,
],
};
使用函数而不是字符串的一个警告是序列化会将函数替换为函数的名称。这可以使清单(有点像应用的 index.html)按预期工作。
¥One caveat to using functions instead of strings is that serialization will replace the function with the function's name. This keeps manifests (kinda like the index.html for your app) working as expected.
序列化配置如下所示:
¥Here is what the serialized config would look like:
{
"plugins": [["withCustom", {}], "withCustom"]
}
¥Why app.plugin.js for plugins
当 Node 模块 ID 作为插件提供时,配置解析首先搜索名为 app.plugin.js 的文件。这是因为 Node 环境通常与 Android、iOS 或 Web JS 环境不同,因此需要不同的转译预设(例如,module.exports
而不是 import/export
)。
¥Config resolution searches for a file named app.plugin.js first when a Node module ID is provided as a plugin.
This is because Node environments are often different to Android, iOS, or web JS environments and therefore require different transpilation presets (for example, module.exports
instead of import/export
).
由于这个原因,我们会搜索 Node 模块的根目录,而不是紧挨着 index.js。想象一下,你有一个 TypeScript Node 模块,其中转译的主文件位于 build/index.js,如果应用配置插件解析搜索 build/app.plugin.js,你将失去以不同方式转译文件的能力。
¥Because of this reasoning, the root of a Node module is searched instead of right next to the index.js. Imagine you had a TypeScript Node module where the transpiled main file was located at build/index.js, if app config plugin resolution searched for build/app.plugin.js you'd lose the ability to transpile the file differently.