metro.config.js
Metro 中可用配置的参考。
在 定制 Metro 指南 中查看更多关于 metro.config.js 的信息。
🌐 See more information about metro.config.js in the customizing Metro guide.
环境变量
🌐 Environment variables
Expo CLI 可以从 .env 文件加载环境变量。了解有关如何在 Expo CLI 中使用环境变量的更多信息,请参阅 环境变量指南。
🌐 Expo CLI can load environment variables from .env files. Learn more about how to use environment variables in Expo CLI in the environment variables guide.
EAS CLI 使用不同的环境变量机制,除非它调用 Expo CLI 来进行编译和打包。了解更多关于 EAS 中的环境变量 的信息。
🌐 EAS CLI uses a different mechanism for environment variables, except when it invokes Expo CLI for compiling and bundling. Learn more about environment variables in EAS.
如果你正在迁移一个较旧的项目,那么你应该通过在你的 .gitignore 中添加以下内容来忽略本地环境文件:
🌐 If you are migrating an older project, then you should ignore local env files by adding the following to your .gitignore:
# local env files .env*.local
禁用 dotenv 文件
🌐 Disabling dotenv files
在调用任何 Expo CLI 命令之前,通过启用 EXPO_NO_DOTENV 环境变量,可以在 Expo CLI 中完全禁用 Dotenv 文件的加载。
🌐 Dotenv file loading can be fully disabled in Expo CLI by enabling the EXPO_NO_DOTENV environment variable, before invoking any Expo CLI command.
# All users can run cross-env, followed by the Expo CLI command- npx cross-env EXPO_NO_DOTENV=1 expo start# Alternatively, macOS and Linux users can define the environment variable, then run npx, followed by the Expo CLI command- EXPO_NO_DOTENV=1 npx expo start禁用 EXPO_PUBLIC_ 前缀的客户端环境变量
🌐 Disabling EXPO_PUBLIC_-prefixed client environment variables
以 EXPO_PUBLIC_ 为前缀的环境变量将在构建时暴露给应用。例如,EXPO_PUBLIC_API_KEY 将作为 process.env.EXPO_PUBLIC_API_KEY 使用。
🌐 Environment variables prefixed with EXPO_PUBLIC_ will be exposed to the app at build-time. For example, EXPO_PUBLIC_API_KEY will be available as process.env.EXPO_PUBLIC_API_KEY.
可以使用环境变量 EXPO_NO_CLIENT_ENV_VARS=1 禁用客户端环境变量内联,该变量必须在进行任何打包之前定义。
🌐 Client environment variable inlining can be disabled with the environment variable EXPO_NO_CLIENT_ENV_VARS=1, this must be defined before any bundling is performed.
# All users can run cross-env, followed by the Expo CLI command- npx cross-env EXPO_NO_CLIENT_ENV_VARS=1 expo start# Alternatively, macOS and Linux users can define the environment variable, then run npx, followed by the Expo CLI command- EXPO_NO_CLIENT_ENV_VARS=1 npx expo startCSS
信息 CSS 支持正在开发中,目前仅在网页端可用。
Expo 在你的项目中支持 CSS。你可以从任何组件中导入 CSS 文件。CSS 模块也同样受支持。
🌐 Expo supports CSS in your project. You can import CSS files from any component. CSS Modules are also supported.
CSS 支持默认启用。你可以通过在 Metro 配置中设置 isCSSEnabled 来禁用该功能。
🌐 CSS support is enabled by default. You can disable the feature by setting isCSSEnabled in the Metro config.
/** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname, { // Disable CSS support. isCSSEnabled: false, });
全局 CSS
🌐 Global CSS
警告 全局样式仅适用于网页,使用它们可能导致你的应用在原生端的视觉效果出现差异。
你可以从任何组件导入 CSS 文件。CSS 将应用到整个页面。
🌐 You can import a CSS file from any component. The CSS will be applied to the entire page.
在这里,我们将为类名 .container 定义一个全局样式:
🌐 Here, we'll define a global style for the class name .container:
.container { background-color: red; }
然后,我们可以通过导入样式表并使用 .container 在组件中使用类名:
🌐 We can then use the class name in our component by importing the stylesheet and using .container:
import './styles.css'; import { View } from 'react-native'; export default function App() { return ( <> {/* Use `className` to assign the style with React DOM components. */} <div className="container">Hello World</div> {/* Use `style` with the following syntax to append class names in React Native for web. */} <View style={{ $$css: true, _: 'container', }}> Hello World </View> </> ); }
你还可以导入库中提供的样式表,就像任何节点模块一样:
🌐 You can also import stylesheets that are vendored in libraries, just like you would any node module:
// Applies the styles app-wide. import 'emoji-mart/css/emoji-mart.css';
- 在原生上,所有全局样式表都会自动忽略。
- 全局样式表支持热重载,只需保存文件即可应用更改。
CSS 模块
🌐 CSS Modules
警告 原生的 CSS 模块仍在开发中,目前仅适用于网页。
CSS 模块是一种将 CSS 限定到特定组件的方法。这对于避免命名冲突以及确保样式只应用于目标组件非常有用。
🌐 CSS Modules are a way to scope CSS to a specific component. This is useful for avoiding naming collisions and for ensuring that styles are only applied to the intended component.
在 Expo 中,CSS 模块是通过创建一个 .module.css 扩展名的文件来定义的。该文件可以从任何组件中导入。导出的值是一个对象,其键为类名,值为仅用于 Web 的作用域名称。可以使用 unstable_styles 导入来访问 react-native-web 安全的样式。
🌐 In Expo, CSS Modules are defined by creating a file with the .module.css extension. The file can be imported from any component. The exported value is an object with the class names as keys and the web-only scoped names as the values. The import unstable_styles can be used to access react-native-web-safe styles.
CSS Modules 支持平台扩展,让你可以为不同的平台定义不同的样式。例如,你可以定义一个 module.ios.css 文件和一个 module.android.css 文件,分别为 Android 和 iOS 定义样式。你需要在导入时省略扩展名,例如:
🌐 CSS Modules support platform extensions to allow you to define different styles for different platforms. For example, you can define a module.ios.css and module.android.css file to define styles for Android and iOS respectively. You'll need to import without the extension, for example:
Flipping the extension, for example, App.ios.module.css will not work and result in a universal module named App.ios.module.
You cannot pass styles to the
classNameprop of a React Native or React Native for web component. Instead, you must use thestyleprop.
import styles, { unstable_styles } from './App.module.css'; export default function Page() { return ( <> <Text style={{ // This is how react-native-web class names are applied $$css: true, _: styles.text, }}> Hello World </Text> <Text style={unstable_styles.text}>Hello World</Text> {/* Web-only usage: */} <p className={styles.text}>Hello World</p> </> ); }
.text { color: red; }
- On web, all CSS values are available. CSS is not processed or auto-prefixed like it is with the React Native Web
StyleSheetAPI. You can usepostcss.config.jsto autoprefix your CSS. - CSS Modules use lightningcss under the hood, check the issues for unsupported features.
PostCSS
PostCSS can be customized by adding a postcss.config.json file to the root of your project. This file should export a function that returns a PostCSS configuration object. For example:
{ "plugins": { "tailwindcss": {} } }
Both postcss.config.json and postcss.config.js are supported, but postcss.config.json enables better caching.
Expo CLI automatically handles CSS vendor prefixes with built-in support for browserslist. Avoid adding autoprefixer as this duplicates the functionality and slows down bundling.
Resetting cache after updates
Changing the Post CSS or browserslist config will require you to clear the Metro cache:
- npx expo start --clear- npx expo export --clearbrowserslist
Expo 通过基于 Rust 的 CSS 解析器提供自动的 browserslist 支持。你可以通过在 package.json 文件中添加 browserslist 字段来自定义 CSS 的厂商前缀和浏览器支持。例如:
🌐 Expo has automatic browserslist support via the Rust-based CSS parser. You can customize the CSS vendor prefixes and browser support by adding a browserslist field to your package.json file. For example:
{ "browserslist": [">0.2%", "not dead", "not op_mini all"] }
SASS
Expo Metro 对 SCSS/SASS 提供部分支持。
🌐 Expo Metro has partial support for SCSS/SASS.
要进行设置,请在你的项目中安装 sass 软件包:
🌐 To setup, install the sass package in your project:
- yarn add -D sass然后,确保在 metro.config.js 文件中设置了 CSS。
🌐 Then, ensure CSS is setup in the metro.config.js file.
- 当安装
sass时,没有扩展名的模块将按以下顺序解析:scss、sass、css。 - 仅在
sass文件中使用指定语法。 - 目前不支持从 scss/sass 文件中导入其他文件。
Tailwind
信息 标准 Tailwind CSS 仅支持网页平台。如需通用支持,可使用诸如 NativeWind 的库,它允许使用 Tailwind CSS 创建样式化的 React Native 组件。
了解如何在你的 Expo 项目中配置和使用 Tailwind CSS。
扩展 Babel 转换器
🌐 Extending the Babel transformer
Expo 的 Metro 配置使用自定义的 transformer.babelTransformerPath 值来确保始终使用 expo-babel-preset,并且支持 Web/Node.js 环境。
🌐 Expo's Metro config uses a custom transformer.babelTransformerPath value to ensure expo-babel-preset is always used and web/Node.js environments are supported.
如果你想扩展 Babel 转换器,请从 @expo/metro-config/babel-transformer 导入上游转换器,而不是 metro-react-native-babel-transformer。例如:
🌐 If you want to extend the Babel transformer, import the upstream transformer from @expo/metro-config/babel-transformer instead of metro-react-native-babel-transformer. For example:
const upstreamTransformer = require('@expo/metro-config/babel-transformer'); module.exports.transform = async ({ src, filename, options }) => { // Do something custom for SVG files... if (filename.endsWith('.svg')) { src = '...'; } // Pass the source through the upstream Expo transformer. return upstreamTransformer.transform({ src, filename, options }); };
自定义解析
🌐 Custom resolving
Expo CLI 扩展了默认的 Metro 解析器,以添加诸如 Web、Server 和 tsconfig 别名支持等功能。你也可以通过链式调用 config.resolver.resolveRequest 函数来自定义 Metro 的默认解析行为。
🌐 Expo CLI extends the default Metro resolver to add features like Web, Server, and tsconfig aliases support. You can similarly customize the default resolution behavior of Metro by chaining the config.resolver.resolveRequest function.
const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); config.resolver.resolveRequest = (context, moduleName, platform) => { if (moduleName.startsWith('my-custom-resolver:')) { // Logic to resolve the module name to a file path... // NOTE: Throw an error if there is no resolution. return { filePath: 'path/to/file', type: 'sourceFile', }; } // Ensure you call the default resolver. return context.resolveRequest(context, moduleName, platform); }; module.exports = config;
与传统的打包工具不同,Metro 在所有平台上共享相同的解析函数。因此,你可以在每个请求中使用 context 对象动态地修改解析设置。
🌐 Unlike traditional bundlers, Metro shared the same resolver function across all platforms. As a result, you can mutate the resolution settings dynamically on each request with the context object.
模拟模块
🌐 Mocking modules
如果你希望某个模块在特定平台上为空,你可以从解析器返回一个 type: 'empty' 对象。以下示例将导致 lodash 在网页端为空:
🌐 If you want a module to be empty for a given platform, you can return a type: 'empty' object from the resolver. The following example will cause lodash to be empty on web:
const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); config.resolver.resolveRequest = (context, moduleName, platform) => { if (platform === 'web' && moduleName === 'lodash') { return { type: 'empty', }; } // Ensure you call the default resolver. return context.resolveRequest(context, moduleName, platform); }; module.exports = config;
这种技术相当于在 Webpack 或 Vite 中使用空外部,但具有能够针对特定平台的额外好处。
🌐 This technique is equivalent to using empty externals in Webpack or Vite, but with the added benefit of being able to target specific platforms.
虚拟模块
🌐 Virtual modules
目前 Metro 不支持虚拟模块。你可以使用的一种实现类似行为的技术是在 node_modules/.cache/... 目录下创建一个模块,并将解析重定向到该文件。
🌐 Metro doesn't support virtual modules at the moment. One technique you can use to obtain similar behavior is to create a module in the node_modules/.cache/... directory and redirect the resolution to that file.
下面的示例将在 node_modules/.cache/virtual/virtual-module.js 创建一个模块,并将 virtual:my-module 的解析重定向到该文件:
🌐 The following example will create a module at node_modules/.cache/virtual/virtual-module.js and redirect the resolution of virtual:my-module to that file:
const path = require('path'); const fs = require('fs'); const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); const virtualPath = path.resolve(__dirname, 'node_modules/.cache/virtual/virtual-module.js'); // Create the virtual module in a generated directory... fs.mkdirSync(path.dirname(virtualPath), { recursive: true }); fs.writeFileSync(virtualPath, 'export default "Hello World";'); config.resolver.resolveRequest = (context, moduleName, platform) => { if (moduleName === 'virtual:my-module') { return { filePath: virtualPath, type: 'sourceFile', }; } // Ensure you call the default resolver. return context.resolveRequest(context, moduleName, platform); }; module.exports = config;
这可以用来通过自定义导入模拟 externals。例如,如果你想将 require('expo') 重定向到像 SystemJS.require('expo') 这样的自定义内容,你可以创建一个导出 SystemJS.require('expo') 的虚拟模块,并将 expo 的解析重定向到该文件。
🌐 This can be used to emulate externals with custom imports. For example, if you want to redirect require('expo') to something custom like SystemJS.require('expo'), you can create a virtual module that exports SystemJS.require('expo') and redirect the resolution of expo to that file.
定制改造
🌐 Custom transforming
转换在 Metro 中被大量缓存。如果你更新了某些内容,请使用
--clear标志来查看更新。例如,npx expo start --clear。
Metro 没有一个非常直观的插件系统来转换文件,建议使用 babel.config.js 和 caller 对象来定制转换。
🌐 Metro doesn't have a very expressive plugin system for transforming files, instead opt to use the babel.config.js and caller object to customize the transformation.
module.exports = function (api) { // Get the platform that Expo CLI is transforming for. const platform = api.caller(caller => (caller ? caller.platform : 'ios')); // Detect if the bundling operation is for Hermes engine or not, e.g. `'hermes'` | `undefined`. const engine = api.caller(caller => (caller ? caller.engine : null)); // Is bundling for a server environment, e.g. API Routes. const isServer = api.caller(caller => (caller ? caller.isServer : false)); // Is bundling for development or production. const isDev = api.caller(caller => caller ? caller.isDev : process.env.BABEL_ENV === 'development' || process.env.NODE_ENV === 'development' ); // Ensure the config is not cached otherwise the platform will not be updated. api.cache(false); // You can alternatively provide a more robust CONFIG cache invalidation: // api.cache.invalidate(() => platform); return { presets: ['babel-preset-expo'], plugins: [ // Add a plugin based on the platform... platform === 'web' && 'my-plugin', // Ensure you filter out falsy values. ].filter(Boolean), }; };
如果调用者没有 engine、platform、bundler 等,那么请确保你在使用转换器时使用 @expo/metro-config/babel-transformer。如果你使用的是自定义转换器,那么它可能需要继承 Expo 转换器。
🌐 If the caller doesn't have engine, platform, bundler, and so on, then ensure you are using @expo/metro-config/babel-transformer for the transformer. If you're using a custom transformer then it may need to extend the Expo transformer.
如果可能,尽量在解析器中实现自定义逻辑,缓存会更简单,也更容易理解。例如,如果你需要重新映射一个导入文件,与其解析所有可能的导入方法然后用转换器重新映射,不如用解析器直接解析到一个静态文件,这样更简单、更快。
🌐 Always try to implement custom logic in the resolver if possible, caching is much simpler and easier to reason about. For example, if you need to remap an import, it's simpler and faster to resolve to a static file with the resolver than to parse all possible import methods and remap them with the transformer.
始终将 babel-preset-expo 用作默认的 Babel 预设,这可确保转换始终与 Expo 运行时兼容。babel-preset-expo 在内部使用所有调用方输入,以针对特定平台、引擎和环境进行优化。
🌐 Always use babel-preset-expo as the default Babel preset, this ensures the transformation is always compatible with Expo runtimes. babel-preset-expo uses all of the caller inputs internally to optimize for a given platform, engine, and environment.
Node.js 内置组件
🌐 Node.js built-ins
在为服务器环境打包时,Expo 的 Metro 配置会根据当前的 Node.js 版本自动支持外部化 Node.js 内置模块(fs、path、node:crypto 等)。如果 CLI 正在为浏览器环境打包,则内置模块会首先检查该模块是否已在本地安装,如果没有,则回退到一个空的替代模块。例如,如果你在浏览器中安装了 path 使用,则可以使用它,否则该模块会自动被跳过。
🌐 When bundling for a server environment, Expo's Metro config automatically supports externalizing Node.js built-in modules (fs, path, node:crypto, and more) based on the current Node.js version. If the CLI is bundling for a browser environment, then built-ins will first check if the module is installed locally, then fallback on an empty shim. For example, if you install path for use in the browser, this can be used, otherwise, the module will automatically be skipped.
环境设置
🌐 Environment settings
信息 这些环境变量在测试环境中不会被定义。
Expo 的 Metro 配置会注入可以通过环境变量在客户端包中使用的构建设置。所有变量都会被内联,不能动态使用。例如,process.env["EXPO_BASE_URL"] 是不起作用的。
🌐 Expo's Metro config injects build settings that can be used in the client bundle via environment variables. All variables will be inlined and cannot be used dynamically. For example, process.env["EXPO_BASE_URL"] won't work.
process.env.EXPO_BASE_URL会暴露在experiments.baseUrl中定义的基础 URL。这在 Expo Router 中用于遵循生产环境的基础 URL 进行部署。
打包分裂
🌐 Bundle splitting
Expo CLI 会根据生产环境中的异步导入自动将Webpack拆分成多个块。此功能需要在入口包中的某个位置安装并导入 @expo/metro-runtime(在 Expo Router 中默认可用)。
🌐 Expo CLI automatically splits web bundles into multiple chunks based on async imports in production. This feature requires @expo/metro-runtime to be installed and imported somewhere in the entry bundle (available by default in Expo Router).
异步包的共享依赖会合并为一个单独的模块,以减少请求数量。例如,如果你有两个异步包都导入了 lodash,那么该库会被合并到一个初始模块中。
🌐 Shared dependencies of async bundles are merged into a single chunk to reduce the number of requests. For example, if you have two async bundles that import lodash, then the library is merged into a single initial chunk.
块分割启发式无法自定义。例如:
🌐 The chunk splitting heuristic cannot be customized. For example:
math.jsindex.jsexport function add(a, b) { return a + b; }
import '@expo/metro-runtime'; // This will be split into a separate chunk. import('./math').then(math => { console.log(math.add(1, 2)); });
当你运行 npx expo export -p web 时,打包包会被拆分成多个文件,并且入口打包包会被添加到主 HTML 文件中。@expo/metro-runtime 会添加用于加载和执行异步打包包的运行时代码。
🌐 When you run npx expo export -p web, the bundles are split into multiple files, and the entry bundle is added to the main HTML file. @expo/metro-runtime adds the runtime code that loads and evaluates the async bundles.
源映射调试 ID
🌐 Source map debug ID
如果一个打包包以外部源映射的方式导出,将会在文件末尾添加一个调试ID注释,同时在源映射中添加匹配的debugId以便对应文件。如果没有导出源映射,或者使用了内联源映射,则不会添加此注释。
🌐 If a bundle is exported with an external source map, a Debug ID annotation will be added to the end of the file, along with a matching debugId in the source map for corresponding the files together. If no source maps are exported, or inline source maps are used then this annotation will not be added.
// <all source code> //# debugId=<deterministic chunk hash>
相关的 *.js.map 或 *.hbc.map 源映射将是一个包含等效 debugId 属性的 JSON 文件。debugId 将在 hermes 字节码生成之前注入,以确保在所有情况下都匹配。
🌐 The associated *.js.map or *.hbc.map source map will be a JSON file containing an equivalent debugId property. The debugId will be injected before hermes bytecode generation to ensure matching in all cases.
debugId 是打包包内容的确定性哈希,不包含外部打包拆分引用。这个值与用于创建块文件名的值相同,只是格式化为 UUID。例如,431b98e2-c997-4975-a3d9-2987710abd44。
🌐 The debugId is a deterministic hash of the bundle's contents without the external bundle splitting references. This is the same value used to create a chunks filename but formatted as a UUID. For example, 431b98e2-c997-4975-a3d9-2987710abd44.
@expo/metro-config 会在 npx expo export 和 npx expo export:embed 期间注入 debugId。在 npx expo export:embed 中的任何额外优化步骤,例如 Hermes 字节码生成,都需要手动注入 debugId。
Metro 需要运行时
🌐 Metro require runtime
你可以选择通过环境变量 EXPO_USE_METRO_REQUIRE=1 启用自定义的 Metro require 实现。该运行时具有以下功能:
🌐 You can optionally enable a custom Metro require implementation with the environment variable EXPO_USE_METRO_REQUIRE=1. This runtime has the following features:
- 字符串模块 ID 是人类可读的,使缺失模块错误更容易跟踪。
- 在运行之间和模块之间相同的确定性 ID(开发中的 React 服务器组件所需)。
- 删除了对旧版 RAM 打包包的支持。
神奇的导入注释
🌐 Magic import comments
从 SDK 52 起在所有平台上可用。
像 Workers 和 Node.js 这样的服务器环境支持在运行时导入任意文件,所以你可能希望保持 import 语法不变,而不是使用 Metro 的 require 系统。你可以在 import() 语句中使用 /* @metro-ignore */ 注释来选择不使用动态导入。
🌐 Server environments such as Workers, and Node.js support import arbitrary files at runtime, so you may want to keep import syntax in-tact instead of using Metro's require system. You can opt-out dynamic imports with the /* @metro-ignore */ comment in import() statements.
// Manually ensure `./my-module.js` is included in the correct spot relative to the module. const myModule = await import(/* @metro-ignore */ './my-module.js');
Expo CLI 将跳过 ./my-module.js 依赖,并假设开发者已将其手动添加到输出包中。在内部,这用于导出自定义服务器代码,该代码可根据请求动态切换文件。避免在原生包中使用此语法,因为在启用 Hermes 的 React Native 中通常无法使用 import()。
🌐 Expo CLI will skip the ./my-module.js dependency and assume that the developer has manually added it to the output bundle. Internally, this is used for exporting custom server code that dynamically switches between files based on the request. Avoid using this syntax for native bundles since import() is generally not available in React Native with Hermes enabled.
许多 React 库使用了 Webpack 的 /* webpackIgnore: true */ 注释来实现类似的功能。为了弥补差距,我们也增加了对 Webpack 注释的支持,但建议在你的应用中使用 Metro 的等效做法。
🌐 Many React libraries shipped the Webpack /* webpackIgnore: true */ comment to achieve similar behavior. To bridge the gap, we've also added support for Webpack's comment but recommend using the Metro equivalent in your app.
ES 模块解析
🌐 ES Module resolution
本节适用于从 SDK 53 开始的所有平台。
Metro 使用不同的解析策略分别解析 ES 模块 import 和 CommonJS require。
🌐 Metro resolves ES Module import and CommonJS require with separate resolution strategies.
以前,Metro 使用经典的 Node.js 模块解析策略(与 Node.js v12 之前的版本一致),并添加了一些功能以支持 ES 模块。在这种解析策略中,Metro 会从 node_modules 解析模块、JS 文件,可选择性地省略扩展名,例如 .js,并使用 package.json 字段,例如 main、module 和 react-native。
🌐 Previously, Metro applied the classic Node.js module resolution strategy (which matches Node.js versions before v12), with some additions to support ES Modules. In this resolution strategy, Metro resolves modules from node_modules, JS files, optionally while omitting extensions, such as .js, and uses package.json fields such as main, module, and react-native.
现在,采用现代 ES 模块解析策略时,Metro 会先从 node_modules 解析模块,然后匹配不同的 package.json 字段,例如 exports、包公开的子路径嵌套映射 以及 main。
🌐 Now, with the modern ES Modules resolution strategy, Metro instead resolves modules from node_modules, then matches different package.json fields, such as exports, a nested map of sub-paths a package exposes, and main.
根据包的导入方式,将使用以下两种解析策略之一。通常,从 Node 模块中使用 import 导入的文件(而不是 require)将使用 ES 模块解析策略,并回退到常规的经典 Node.js 解析。未通过 ES 模块解析解析的文件或已使用 CommonJS require 导入的文件将使用经典解析策略。
🌐 Depending on how a package is imported, one of these two resolution strategies will be used. Typically, a file that is imported with import from a Node module (rather than require), will use the ES Modules resolution strategy, and fall back on regular classic Node.js resolution. A file that wasn't resolved with ES Modules resolution or has been imported with CommonJS require will use the classic resolution strategy.
package.json:exports
在执行 ES 模块解析时,Metro 会查看 package.json:exports 条件映射。这是一个将导入子路径和条件映射到 Node 模块包中文件的映射表。
🌐 When performing ES Modules resolution, Metro will look at the package.json:exports conditions map. This is a mapping of import subpaths and conditions to files in the Node module package.
例如,一个总是暴露 index.js 文件并且符合 Metro 经典 CommonJS 模块解析的包,可能会指定一个带有 default 条件的映射。
🌐 For example, a package that always exposes an index.js file, and matches Metro's classic CommonJS module resolution, may specify a map with the default condition.
{ "exports": { "default": "./index.js" } }
然而,同时提供 CommonJS 和 ES 模块入口点的软件包可能会提供一个带有 import 和 require 条件的映射。
🌐 However, a package providing both a CommonJS and ES Modules entrypoint may provide a mapping with the import and require conditions.
{ "exports": { "import": "./index.mjs", "require": "./index.cjs" } }
默认情况下,Metro 会根据平台以及解析是从 CommonJS require 调用还是 ES 模块 import 声明开始,匹配不同的条件,并相应地更改条件。
🌐 By default, Metro will match different conditions depending on the platform and whether the resolution has started from a CommonJS require call, or an ES Modules import statement and will change the condition accordingly.
对于本地平台,会添加条件 react-native;对于 Web 导出,会添加条件 browser;而对于服务器导出(例如 API 路由或 React 服务器函数),会添加条件 node、react-server 和 workerd。这些条件不是按照定义的顺序匹配的,而是根据 package.json:exports 映射中属性的顺序进行匹配。
🌐 For native platforms, the condition react-native is added, for web exports, the browser condition is added, and for server exports (such as API routes or React Server functions), the node, react-server, and workerd conditions are added. These conditions aren't matched in the order they're defined in. Instead, they're matched against the order of properties in the package.json:exports map.
TypeScript 会独立于 Metro 执行 ES 模块解析,并且在其 compilerOptions.moduleResolution 配置选项设置为 "bundler"(这与 Metro 的行为更为一致)或 "node16" / "nodenext" 时,也会遵循 package.json:exports 映射。然而,TypeScript 还会匹配 types 条件。因此,当一个包在其 exports 映射中没有将 types 条件放在首位时,类型可能无法正确解析。
🌐 TypeScript performs ES Module resolution separately from Metro and will also respect package.json:exports maps, when its compilerOptions.moduleResolution configuration option has either been set to "bundler" (which matches Metro's behavior more closely) or to "node16" / "nodenext". TypeScript will however also match the types condition. As such, types may not resolve properly when a package doesn't put the types condition first in its exports map.
由于 exports 映射可能包含子路径,包的导入不再必须匹配包的模块文件夹中的某个文件,而可能是一个“重定向”的导入。如果在 package.json:exports 中进行了指定,导入 'package/submodule' 可能会匹配不同于 node_modules/package/submodule.js 的文件。
🌐 Since an exports map may contain subpaths, a package import may not have to match a file in the package's modules folder any longer, but may be a "redirected" import. Importing 'package/submodule' may match a different file than node_modules/package/submodule.js if it's specified in package.json:exports.
{ "exports": { ".": "./index.js", "./submodule": "./submodule/submodule.js" } }
如果你遇到不兼容或尚未为新的 ES 模块解析策略准备的包,你可能可以通过修补它的 package.json 文件并添加或更正其 package.json:exports 条件映射来解决问题。不过,也可以通过禁用 unstable_enablePackageExports 选项来防止 Metro 在解析时使用 package.json:exports 映射。
🌐 If you're encountering packages that are incompatible or unprepared for the new ES Modules resolution strategy, you may be able to resolve problems by patching its package.json file and add or correct its package.json:exports conditions map. However, it's also possible to prevent Metro from using package.json:exports maps in its resolution by disabling the unstable_enablePackageExports option.
const { getDefaultConfig } = require('expo/metro-config'); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); config.resolver.unstable_enablePackageExports = false; module.exports = config;
资源导入
🌐 Asset imports
导入资源时,将创建一个虚拟模块来表示导入资源所需的数据。
🌐 When assets are imported, a virtual module is created to represent the data required for importing the asset.
在原生平台上,资源将是一个数字ID:1、2、3,依此类推,可以使用 require("@react-native/assets-registry/registry").getAssetByID(<NUMBER>) 查找。在网页和服务器平台上,资源会根据文件类型而变化。如果文件是图片,则资源将为 { uri: string, width?: number, height?: number },否则资源将是一个 string,表示该资源的远程URL。
资源的使用方式如下:
🌐 The assets can be used as follows:
import { Image } from 'react-native'; import asset from './img.png'; function Demo() { return <Image source={asset} />; }
在 API 路由中,你始终可以假设资源的类型不是数字:
🌐 In API routes, you can always assume the type of the asset will not be a number:
import asset from './img.png'; export async function GET(req: Request) { const ImageData = await fetch( new URL( // Access the asset URI. asset.uri, // Append to the current request URL origin. req.url ) ).then(res => res.arrayBuffer()); return new Response(ImageData, { headers: { 'Content-Type': 'image/png', }, }); }
Web Workers
🌐 Web workers
重要 此功能处于 alpha 版本,可能会有破坏性更改。
new Worker(new URL('./worker', window.location.href));
Expo Metro 具有实验性的 web worker 支持。该功能目前仅限于 web,在原生平台上无法使用,在原生平台上使用会触发错误 “属性 'Worker' 不存在”。
🌐 Expo Metro has experimental web worker support. This feature is currently web-only and does not work on native, usage on native will trigger an error "Property 'Worker' doesn't exist".
Web 工作线程可以用于将工作分配到网页的独立线程上,从而使主线程保持响应。这对于计算量大的任务非常有用,例如图片处理、加密或其他会阻塞主线程的任务。
🌐 Web workers can be used to offload work to a separate thread on web, allowing the main thread to remain responsive. This is useful for computationally expensive tasks, such as image processing, cryptography, or other tasks that would otherwise block the main thread.
可以使用 Blob 内联生成工作线程,但有时你可能想利用现代功能,如 TypeScript 或导入其他模块。
🌐 Workers can be generated inline using Blob, but sometimes you may want to leverage modern features like TypeScript or importing other modules.
Web workers 依赖于 Expo 的 bundle 分割支持,这意味着你需要使用 Expo Router 或安装并导入 @expo/metro-runtime。你也不能在 web workers 中使用环境 EXPO_NO_METRO_LAZY=1。
🌐 Web workers depend on Expo bundle splitting support, which means you need to either use Expo Router or install and import @expo/metro-runtime. You also cannot use the environment EXPO_NO_METRO_LAZY=1 with web workers.
假设以下一个将数字翻倍的 Worker 示例:
🌐 Consider the following example of a worker that doubles a number:
self.onmessage = ({ data }) => { const result = data * 2; // Example: double the number self.postMessage(result); };
这个 worker 文件可以在主应用中作为 Worker 导入:
🌐 This worker file can be imported as a Worker in the main app:
// worker is of type `Worker` const worker = new Worker(new URL('./worker', window.location.href)); worker.onmessage = ({ data }) => { console.log(`Worker responded: ${data}`); }; worker.postMessage(5);
Expo CLI 在后台生成如下代码:
🌐 Behind the scenes, Expo CLI is generating code like this:
const worker = new Worker( new URL('/worker.bundle?platform=web&dev=true&etc', window.location.href) );
生成的打包包 URL 会根据开发/生产环境而变化,以确保 worker 能够正确加载和打包。与传统的打包包拆分不同,worker 文件需要包含所有模块的独立副本,不能依赖主打包包中的公共模块。
🌐 The generated bundle URL changes based on development/production to ensure the worker is loaded and bundled correctly. Unlike traditional bundle splitting, a worker file needs to contain its own copy of all modules and cannot depend on common modules in the main bundle.
原生 API Worker 在 React Native 中传统上是不可用的,并且 Expo SDK 也未提供该 API,因此尽管这一打包功能从技术上对所有平台都有效,但它仅在 Web 上有用。如果你想支持原生平台,理论上可以编写一个原生的 Expo 模块来为 Worker API 提供 polyfill。或者,你也可以在 React Native Reanimated 中使用“worklet” API,将工作卸载到原生的独立线程上。
🌐 The native API Worker is traditionally unavailable in React Native and not provided by the Expo SDK, so even though this bundling feature technically works for all platforms, it's only useful on web. You could theoretically write a native Expo module that polyfills the Worker API if you want to support native platforms too. Alternatively, you can use the "worklet" API in React Native Reanimated to offload work to a separate thread on native.
或者,你可以通过公共路径导入 Workers,方法是先将转换后的 JS 文件放在 public 目录中,然后在 worker 导入中使用变量引用它:
🌐 Alternatively, you can import Workers using the public path by first putting a transformed JS file in the public directory, then referencing it in the worker import with a variable:
// Will avoid the transform and use the public path directly. const worker = new Worker('/worker.js'); // The variable breaks the transform causing the literal path to be used instead of the transformed path. const path = '/worker.js'; const anotherWorker = new Worker(new URL(path, window.location.href));
在 Worker 构造函数中使用变量不支持打包。要检查内部 URL,你可以使用内部语法 require.unstable_resolveWorker('./path/to/worker.js') 来获取 URL 片段。
🌐 Using a variable in the Worker constructor is not supported for bundling. To inspect the internal URL, you may use the internal syntax require.unstable_resolveWorker('./path/to/worker.js') to get the URL fragment.
裸工作流程设置
🌐 Bare workflow setup
本指南有版本控制,在升级或降级 Expo 时需要重新查看。或者,可以使用 Expo 预构建 进行全自动设置。
不使用 Expo Prebuild 的项目必须配置本地文件,以确保始终使用 Expo Metro 配置来打包项目。
🌐 Projects that don't use Expo Prebuild must configure native files to ensure the Expo Metro config is always used to bundle the project.
/* 如果不这样做,别名、绝对导入、资源哈希等功能将无法正常工作。*/
这些修改旨在分别将 npx react-native bundle 和 npx react-native start 替换为 npx expo export:embed 和 npx expo start。
🌐 These modifications are meant to replace npx react-native bundle and npx react-native start with npx expo export:embed and npx expo start respectively.
metro.config.js
确保 metro.config.js 扩展了 expo/metro-config:
🌐 Ensure the metro.config.js extends expo/metro-config:
const { getDefaultConfig } = require('expo/metro-config'); const config = getDefaultConfig(__dirname); module.exports = config;
android/app/build.gradle
Android app/build.gradle 必须配置为使用 Expo CLI 进行生产打包。修改 react 配置对象:
🌐 The Android app/build.gradle must be configured to use Expo CLI for production bundling. Modify the react config object:
ios/<Project>.xcodeproj/project.pbxproj
在你的 ios/<Project>.xcodeproj/project.pbxproj 文件中,替换以下脚本:
🌐 In your ios/<Project>.xcodeproj/project.pbxproj file, replace the following scripts:
"启动打包程序"脚本
🌐 "Start Packager" script
删除 "Start Packager" 脚本。开发服务器必须在运行应用之前或之后使用 npx expo 启动。
🌐 Remove the "Start Packager" script. The dev server must be started with npx expo before/after running the app.
“打包 React Native 代码和图片”脚本
🌐 "Bundle React Native code and images" script
或者,在 Xcode 项目中,选择 "Bundle React Native code and images" 构建阶段,并添加以下修改内容:
🌐 Alternatively, in the Xcode project, select the "Bundle React Native code and images" build phase and add the following modifications:
你可以设置
CLI_PATH、BUNDLE_COMMAND和ENTRY_FILE环境变量来覆盖这些默认值。
自定义条目文件
🌐 Custom entry file
默认情况下,React Native 仅支持使用根 index.js 文件作为入口文件(或平台特定的变体,如 index.ios.js)。Expo 项目允许使用任何入口文件,但这需要额外的裸工作配置。
🌐 By default, React Native only supports using a root index.js file as the entry file (or platform-specific variation like index.ios.js). Expo projects allow using any entry file, but this requires addition bare setup.
开发
🌐 Development
可以使用 expo-dev-client 包启用开发模式入口文件。或者,你可以添加以下配置:
🌐 Development mode entry files can be enabled by using the expo-dev-client package. Alternatively you can add the following configuration:
生产
🌐 Production
在你的 ios/<Project>.xcodeproj/project.pbxproj 文件中,替换 "Bundle React Native code and images" 脚本,以使用 Metro 设置 $ENTRY_FILE:
🌐 In your ios/<Project>.xcodeproj/project.pbxproj file, replace the "Bundle React Native code and images" script to set $ENTRY_FILE according using Metro:
Android app/build.gradle 必须配置为使用 Metro 模块解析来查找根入口文件。修改 react 配置对象:
🌐 The Android app/build.gradle must be configured to use Metro module resolution to find the root entry file. Modify the react config object: