跳至主内容

React Native 中的 Package Exports 支持

· 1 分钟阅读
Alex Hunt
Alex Hunt
Software Engineer @ Meta
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

随着 React Native 0.72 的发布,我们的 JavaScript 构建工具 Metro 现已提供对 package.json"exports" 字段的 Beta 支持。启用此功能后将带来以下改进:

本文将深入解析 Package Exports 的工作原理,并阐述这些变更对 React Native 应用开发者和包维护者的实际影响。

什么是 Package Exports?

Package Exports 是 Node.js 12.7.0 引入的现代方案,用于定义 npm 包的入口点——即指定哪些子路径允许外部导入,以及这些路径应解析到哪些具体文件。

支持 "exports" 显著改善了 React Native 项目与更广泛的 JavaScript 生态系统的兼容性(目前约 16.6k 个包已采用),同时为包作者提供了面向多平台(包括 React Native)的标准化功能集。

package.json 文件中,"exports" 可与传统的 "main" 字段共存或完全替代它。

{
"name": "@storybook/addon-actions",
"main": "./dist/index.js",
...
"exports": {
".": {
"node": "./dist/index.js",
"import": "./dist/index.mjs",
"default": "./dist/index.js"
},
"./preview": {
"import": "./dist/preview.mjs",
"default": "./dist/preview.js"
},
...
"./package.json": "./package.json"
}
}

以下示例展示了应用程序如何通过导入 @storybook/addon-actions 的不同子路径来使用该包:

import {action} from '@storybook/addon-actions';
// -> '@storybook/addon-actions/dist/index.js'

import {action} from '@storybook/addon-actions/preview';
// -> '@storybook/addon-actions/dist/preview.js'

import helpers from '@storybook/addon-actions/src/preset/addArgsHelpers';
// Inaccessible - not listed in "exports"!

Package Exports 的核心特性包括:

  • 包封装:只有定义在 "exports" 中的子路径才能从包外部导入——让包完全掌控其公共 API。

  • 子路径别名:包可定义指向不同文件位置的自定义子路径(支持子路径模式),在重构内部文件时保持公共 API 不变。

  • 条件导出:子路径可根据运行环境解析到不同的底层文件。例如,可针对 "node""browser""react-native" 运行时环境——这取代了原有的 "browser" 字段规范

备注

"exports" 的完整功能详见 Node.js 包入口点规范

由于这些功能与 React Native 现有概念(如平台特定扩展)存在重叠,且 "exports" 已在 npm 生态中实际应用多时,我们特别征询了 React Native 社区意见,确保实现方案能满足开发者需求(提案 PR最终 RFC)。

面向应用开发者

包导出功能现已提供 Beta 版支持。

  • 依赖包导出功能的 npm 包(如 FirebaseStorybook)现在可正常导入使用

  • 使用 Metro 的 React Native for Web 项目现在可解析 "browser" 条件导出,无需额外兼容方案

启用包导出功能会带来少量边缘场景破坏性变更,您可立即进行项目验证

未来版本中,包导出功能将默认启用。由于此前 React Native 应用无法支持该特性,部分 npm 包无法迁移到 "exports" 或需依赖 "react-native" 根字段方案。Metro 对此功能的支持将推动整个生态向前发展

启用包导出功能 (Beta)

通过修改应用的 metro.config.js 配置文件,设置 resolver.unstable_enablePackageExports 选项即可启用

const config = {
// ...
resolver: {
unstable_enablePackageExports: true,
},
};

Metro 额外提供两个解析器选项用于配置条件导出行为:

技巧

请确保使用 React Native 的 Jest 预设配置!Jest 默认支持包导出功能,测试中可通过 testEnvironmentOptions 选项覆盖要解析的 customExportConditions

若使用 TypeScript,需在项目的 tsconfig.json 中设置 moduleResolution: 'bundler'resolvePackageJsonImports: false 以匹配解析行为

项目变更验证

对于现有项目,建议早期采用者通过以下步骤验证启用 unstable_enablePackageExports 后的解析变更。此流程只需执行一次,大多数项目不会出现变更,但此功能需开发者主动启用

💡 Validating changes in your project
备注

If you are not using Yarn, substitute yarn for npx (or the relevant tool used in your project).

  1. Get all resolved dependencies (before changes):

    # Replace index.js with your entry file if needed, such as App.js
    yarn metro get-dependencies index.js --platform android --output before.txt
    • Expo CLI: Run npx expo customize metro.config.js if your project doesn't have a metro.config.js file yet.
    • For full coverage, substitute --platform android for the other platforms in use by your app (e.g. ios, web).
  2. Enable resolver.unstable_enablePackageExports in metro.config.js.

  3. Get all resolved dependencies (after changes):

    yarn metro get-dependencies index.js --platform android --output after.txt
  4. Compare!

    diff before.txt after.txt

重大变更

我们选择在 Metro 中实现完全符合规范的包导出功能(因此需引入少量破坏性变更),同时最大限度保持向后兼容性(便于现有项目逐步迁移)

关键破坏性变更在于:当包提供了 "exports" 字段时,系统将优先读取该字段(高于 package.json 中的其他字段)——且匹配到的子路径目标会被直接使用。

更多细节请参阅 Metro 文档中的所有破坏性变更

包封装的宽松处理

当 Metro 遇到未在 "exports" 中列出的子路径时,会回退到旧版解析机制。这是为提升兼容性设计的特性,旨在减少现有 React Native 项目中此前允许的导入操作带来的用户摩擦。

Metro 不会抛出错误,而是记录警告。

warn: You have imported the module "foo/private/fn.js" which is not listed in
the "exports" of "foo". Consider updating your call site or asking the package
maintainer(s) to expose this API.
备注

我们计划在未来实现严格的包封装模式以符合 Node 的默认行为。因此建议所有开发者及时处理用户遇到的这些警告

面向包维护者(预览)

信息

根据我们的 rollout 计划,Package Exports 将在今年稍后的 React Native 0.73 版本中为大多数项目默认启用。

我们目前没有计划在短期内移除对 "main" 字段及其他现有包解析特性的支持。

Package Exports 提供了限制访问包内部实现的能力,并为库开发者提供了更可预测的方式来面向 React Native 和 React Native for Web 进行开发。

如果您当前正在使用 "exports"

如果您的包在使用 "exports" 的同时还使用了当前的 "react-native" 根字段,请务必注意上述破坏性变更对用户的影响。对于在 Metro 中启用此功能的用户,模块解析时将优先考虑 "exports"

实际上,我们预期用户遇到的主要变化是:由于遵循 "exports" 的包封装机制,应用中任何不可访问的子路径都将触发警告。

迁移到 "exports"

为包添加 "exports" 字段完全是可选的。对于未使用 "exports" 的包,现有包解析机制将保持原行为——且我们没有计划移除该行为。

我们相信 "exports" 的新特性为 React Native 包维护者提供了极具吸引力的功能集:

  • 收紧包 API:现在是审查包模块 API 的好时机,可以通过导出的子路径别名正式定义 API。这能防止用户访问内部 API,减少 bug 出现范围。

  • 条件导出:如果您的包同时面向 React Native for Web(即 "react-native""browser"),现在您可以通过条件控制这些环境的解析顺序(参见下一章节)。

如果您决定引入 "exports"我们建议将其作为破坏性变更处理。我们在 Metro 文档中准备了迁移指南,其中包含如何替换平台特定扩展名等特性。

备注

请不要依赖 Metro 实现的宽松行为。虽然 Metro 保持向后兼容,但包应遵循规范文档中 "exports" 的定义方式以及其他工具的严格实现。

新的 "react-native" 条件

我们引入了 "react-native" 作为社区条件(用于条件导出)。这代表 React Native 框架本身,与 "node""deno" 等其他公认运行时处于同等地位(参见 RFC)。

信息

社区条件定义 —— "react-native"

该条件会被 React Native 框架匹配(全平台适用)。若需针对 React Native for Web,应在该条件前优先指定 "browser"。

此方案取代了原有的 "react-native" 根字段。此前解析优先级由各项目自行决定,导致 React Native for Web 使用时存在歧义。在 "exports" 机制下,包可明确指定条件入口点的解析顺序——从而消除歧义。

  "exports": {
"browser": "./dist/index-browser.js",
"react-native": "./dist/index-react-native.js",
"default": "./dist/index.js"
}
备注

我们决定不引入 "android""ios" 平台条件,主要考虑现有平台选择方法已广泛普及,且跨框架实现此行为的复杂性较高。建议改用 Platform.select() API。

未来规划:稳定版 "exports" 将默认启用

在下一个 React Native 版本中,我们计划移除该特性的 unstable_ 前缀(在完成性能优化和问题修复后),并将默认启用包导出解析功能。

"exports" 全面启用后,我们可以推动 React Native 社区向前发展。例如,React Native 核心包可进行重构以更好地区分公共模块与内部模块。

包导出支持功能推进计划

致谢

感谢参与 RFC 讨论的 React Native 社区成员:@SimenB@tido64@byCedric@thymikee

特别鸣谢 Meta 的 @motiz88@robhogan 对本功能开发的大力支持。