0Pricing
React Native Academy · 课时

编写第一个配置插件

使用 @expo/config-plugins 辅助工具创建 withMyPlugin 函数,将其添加到 app.json 的 plugins 数组中,并运行 expo prebuild 以验证原生输出。

编写第一个配置插件 是 CoddyKit 上的免费 React Native Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 React Native Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 React Native Academy 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Project Setup for a Config Plugin

You can write a config plugin as a file inside your project or as a separate npm package. For project-specific plugins, create a plugins/ folder at the project root and add your plugin file there. Reference it in app.json using a relative path. This approach keeps the plugin co-located with the app and requires no publishing step.

# Project structure
your-expo-app/
  plugins/
    withCustomPermissions.js   <- your plugin
  app.json
  App.tsx

# app.json reference
{
  'expo': {
    'plugins': [
      ['./plugins/withCustomPermissions', { 'reason': 'Scan QR codes' }]
    ]
  }
}

The withMyPlugin Function Signature

Every config plugin is a synchronous function that takes two arguments: config (the full Expo config object) and an optional options object passed from app.json. It must return the (modified) config object. You chain Expo's built-in modifier helpers inside this function to make specific changes to native files, then return the result of the last modifier call.

// plugins/withCustomPermissions.js
const { withInfoPlist } = require('@expo/config-plugins');

/**
 * @param {import('@expo/config-plugins').ExpoConfig} config
 * @param {{ reason: string }} options
 */
function withCustomPermissions(config, options = {}) {
  const reason = options.reason || 'This app requires camera access.';

  return withInfoPlist(config, (iosConfig) => {
    iosConfig.modResults['NSCameraUsageDescription'] = reason;
    return iosConfig;
  });
}

module.exports = withCustomPermissions;

Understanding modResults

Inside a mod callback (like the one passed to withInfoPlist), you receive a config object where config.modResults contains the parsed native file as a JavaScript object. For withInfoPlist it is a plain object mirroring the plist dictionary. For withAndroidManifest it is a parsed XML document. You modify modResults in place and return the config — Expo serializes it back to the native file format automatically.

const { withInfoPlist } = require('@expo/config-plugins');

module.exports = function withAppTrackingPermission(config) {
  return withInfoPlist(config, (iosConfig) => {
    // modResults is the parsed Info.plist dictionary
    const plist = iosConfig.modResults;

    // Add multiple keys at once
    plist['NSUserTrackingUsageDescription'] =
      'We use tracking to improve your ad experience.';
    plist['ITSAppUsesNonExemptEncryption'] = false;

    return iosConfig;
  });
};

Modifying AndroidManifest.xml

Use withAndroidManifest to modify AndroidManifest.xml. The modResults is an XML object parsed by the xmldom library. Expo's AndroidConfig helpers provide utility functions like addUsesPermission, addMetaDataItemToMainActivity, and getMainApplication that operate on this XML structure, so you rarely need to manipulate the XML DOM directly.

const { withAndroidManifest, AndroidConfig } = require('@expo/config-plugins');
const { addUsesPermission } = AndroidConfig.Permissions;

module.exports = function withBluetoothPermissions(config) {
  return withAndroidManifest(config, (androidConfig) => {
    const manifest = androidConfig.modResults;

    // Add multiple permissions
    addUsesPermission(manifest, 'android.permission.BLUETOOTH');
    addUsesPermission(manifest, 'android.permission.BLUETOOTH_ADMIN');
    addUsesPermission(manifest, 'android.permission.BLUETOOTH_SCAN');

    return androidConfig;
  });
};

Testing Your Plugin with expo prebuild

Run npx expo prebuild --clean to test your plugin and inspect the generated native files. Use --clean to start fresh. After prebuild, open the generated files to verify your changes were applied. For plist changes check ios/YourApp/Info.plist. For manifest changes check android/app/src/main/AndroidManifest.xml. Fix any issues in the plugin file and re-run prebuild until the output is correct.

# Test the plugin
npx expo prebuild --clean --platform ios

# Verify the change in Info.plist
plutil -p ios/MyApp/Info.plist | grep -A1 Camera
# Output:
#   "NSCameraUsageDescription" => "Scan QR codes."

# For Android
cat android/app/src/main/AndroidManifest.xml | grep BLUETOOTH

Adding Files to the Native Project

Some plugins need to copy files into the native project — for example, a Google Services JSON file or a custom font. Use withDangerousMod for arbitrary filesystem operations during prebuild. This is the escape hatch for changes not covered by other helpers. Use it sparingly and document the side effects clearly.

const { withDangerousMod } = require('@expo/config-plugins');
const path = require('path');
const fs = require('fs');

module.exports = function withGoogleServicesJson(config) {
  return withDangerousMod(config, [
    'android',
    async (dangerousConfig) => {
      const projectRoot = dangerousConfig.modRequest.projectRoot;
      const src = path.join(projectRoot, 'google-services.json');
      const dest = path.join(projectRoot, 'android', 'app', 'google-services.json');

      if (!fs.existsSync(src)) {
        throw new Error('google-services.json not found in project root');
      }
      fs.copyFileSync(src, dest);
      return dangerousConfig;
    },
  ]);
};

Accessing Config Values Inside a Plugin

The config object passed to your plugin contains the full Expo configuration, including values from app.json. You can read config.name, config.version, config.ios.bundleIdentifier, config.android.package, and any extra fields you defined. This lets your plugin be data-driven based on the app config rather than hard-coded values.

module.exports = function withAppMetadata(config) {
  const appName = config.name;
  const bundleId = config.ios?.bundleIdentifier;
  const version = config.version;

  return withInfoPlist(config, (iosConfig) => {
    iosConfig.modResults['CFBundleDisplayName'] = appName;
    // Log for debugging during prebuild
    console.log(
      '[withAppMetadata] Setting display name to:', appName,
      'for bundle:', bundleId, 'v' + version
    );
    return iosConfig;
  });
};

Modifying build.gradle with withAppBuildGradle

To modify the Android app-level build.gradle file, use withAppBuildGradle. The modResults.contents property contains the file as a raw string, and you use string manipulation or regex to insert your changes. This approach is necessary for adding Gradle dependencies, plugin declarations, or custom build configurations that the higher-level helpers do not cover.

const { withAppBuildGradle } = require('@expo/config-plugins');

module.exports = function withGooglePlayServicesPlugin(config) {
  return withAppBuildGradle(config, (gradleConfig) => {
    const contents = gradleConfig.modResults.contents;

    // Add google-services plugin if not already present
    if (!contents.includes('com.google.gms.google-services')) {
      gradleConfig.modResults.contents = contents.replace(
        'apply plugin: "com.android.application"',
        'apply plugin: "com.android.application"\napply plugin: "com.google.gms.google-services"'
      );
    }
    return gradleConfig;
  });
};

Modifying the Xcode Project with withXcodeProject

For changes to the Xcode project file itself — like adding a build phase, linking a native library, or adding a capability — use withXcodeProject. The modResults is an XcodeProject object from the xcode npm package. This is the most complex mod and requires familiarity with the Xcode project format. Prefer higher-level helpers when they exist.

const { withXcodeProject } = require('@expo/config-plugins');

module.exports = function withRunScript(config) {
  return withXcodeProject(config, (xcode) => {
    const project = xcode.modResults;
    const target = project.getFirstTarget().uuid;

    // Add a run script build phase
    project.addBuildPhase(
      [],
      'PBXShellScriptBuildPhase',
      'Bundle React Native code and images',
      target,
      { shellPath: '/bin/sh', shellScript: 'echo Running custom script' }
    );

    return xcode;
  });
};

Entitlements with withEntitlementsPlist

iOS entitlements are capability declarations in a .entitlements file that must match your provisioning profile. Use withEntitlementsPlist to add entitlements like Push Notifications, iCloud, HealthKit, or App Groups. Without the correct entitlements, features silently fail at runtime on device even if the code looks correct.

const { withEntitlementsPlist } = require('@expo/config-plugins');

module.exports = function withPushNotifications(config) {
  return withEntitlementsPlist(config, (iosConfig) => {
    const entitlements = iosConfig.modResults;

    // Enable push notifications capability
    entitlements['aps-environment'] = 'production';

    // App Groups for sharing data with extensions
    if (!entitlements['com.apple.security.application-groups']) {
      entitlements['com.apple.security.application-groups'] = [];
    }
    entitlements['com.apple.security.application-groups'].push(
      'group.com.yourapp.shared'
    );

    return iosConfig;
  });
};

Composing a Complete Plugin

Real-world plugins often need to modify multiple native files across both platforms. Compose helpers in sequence, passing the result of each into the next. Add clear input validation at the top and log progress messages so developers can trace what the plugin changed during prebuild. Return the final config after all modifications.

const {
  withInfoPlist,
  withAndroidManifest,
  withEntitlementsPlist,
  AndroidConfig,
} = require('@expo/config-plugins');

module.exports = function withPaymentSDK(config, opts = {}) {
  if (!opts.merchantId) {
    throw new Error('withPaymentSDK requires a merchantId option.');
  }

  // iOS
  config = withInfoPlist(config, (c) => {
    c.modResults['PaymentMerchantId'] = opts.merchantId;
    return c;
  });

  config = withEntitlementsPlist(config, (c) => {
    c.modResults['com.apple.developer.in-app-payments'] = [opts.merchantId];
    return c;
  });

  // Android
  config = withAndroidManifest(config, (c) => {
    AndroidConfig.Permissions.addUsesPermission(
      c.modResults, 'android.permission.INTERNET'
    );
    return c;
  });

  return config;
};

Quick Check

Test your understanding of React Native Mobile Development concepts from this lesson.

Lesson Recap

In this lesson you learned: how to write a config plugin function that takes config and options and returns the modified config, how to use withInfoPlist, withAndroidManifest, withEntitlementsPlist, and withDangerousMod, and how to test your plugin with expo prebuild --clean. You also saw how to compose multiple mods into a complete cross-platform plugin. Next up we explore modifying AndroidManifest and Info.plist in detail.

常见问题解答

「编写第一个配置插件」课时是免费的吗?

是的 — 「编写第一个配置插件」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 React Native Academy 课程的其余内容,请升级到 CoddyKit PRO。 React Native Academy 课程共包含 4 节课。

「编写第一个配置插件」这节课中我会学到什么?

使用 @expo/config-plugins 辅助工具创建 withMyPlugin 函数,将其添加到 app.json 的 plugins 数组中,并运行 expo prebuild 以验证原生输出。 你通过在浏览器中直接运行的动手代码来练习 React Native Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 React Native Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 React Native Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「编写第一个配置插件」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 React Native Academy 课中编写并运行代码吗?

能。每节 React Native Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 什么是配置插件以及何时使用
  2. 编写第一个配置插件
  3. 修改 AndroidManifest 与 Info.plist
  4. 将配置插件发布为 npm 包
← 返回 React Native Academy