跳到主要内容

React Native 中的包导出支持

·10 分钟阅读
Alex Hunt
Alex Hunt
Meta 软件工程师

随着 React Native 0.72 的发布,Metro——我们的 JavaScript 构建工具——现在包含了对 package.json "exports" 字段的 Beta 版支持。当启用时,它会添加以下功能

在这篇文章中,我们将介绍包导出(Package Exports)的工作原理,以及这些变化对您作为 React Native 应用程序开发人员或包维护者的意义。

什么是 Package Exports?

Package Exports 是在 Node.js 12.7.0 中引入的,它是 npm 包指定入口点的现代方法——将包的子路径映射到可以从外部导入的文件以及它们应该解析到的文件。

支持 "exports" 改进了 React Native 项目与更广泛的 JavaScript 生态系统(目前有约 1.66 万个包使用)的协作方式,并为包作者提供了一套标准化的功能,以便多平台包能够针对 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 Package Entry Points 规范中有详细说明。

由于这些功能与现有的 React Native 概念(例如特定于平台的文件扩展名)有所重叠,并且 "exports" 已在 npm 生态系统中存在一段时间,我们联系了 React Native 社区,以确保我们的实现能够满足开发人员的需求(PR最终 RFC)。

对于应用开发者

Package Exports 今天可以启用,处于 Beta 版。

  • 依赖 Package Exports 功能的包(例如 FirebaseStorybook)的导入现在应该按预期工作。
  • 使用 Metro 的 React Native for Web 项目现在将能够使用 "browser" 条件导出,从而无需再使用变通方法。

启用 Package Exports 会带来一些可能影响特定项目的边缘情况下的重大变更,您今天就可以测试这些变更。

在未来的 React Native 版本中,Package Exports 将默认启用。在“先有鸡还是先有蛋”的困境中,React Native 应用之前是一些包迁移到 "exports" 的障碍——或者使用了我们的 "react-native" 根字段的逃生舱口。在 Metro 中支持这些功能将使生态系统向前发展。

启用 Package Exports (beta)

Package Exports 可以在你的应用程序的 metro.config.js 文件中通过 resolver.unstable_enablePackageExports 选项启用。

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

Metro 暴露了另外两个解析器选项,用于配置条件导出的行为

  • unstable_conditionNames — 解析条件导出时要断言的条件名称集合。默认情况下,我们匹配 ['require', 'import', 'react-native']
  • unstable_conditionsByPlatform — 在解析给定平台目标时要断言的附加条件名称。默认情况下,当平台为 'web' 时,此项匹配 'browser'
提示

请记住使用 React Native Jest 预设 Jest 默认支持 Package Exports。在测试中,您可以使用 testEnvironmentOptions 选项覆盖哪些 customExportConditions 被解析。

如果您正在使用 TypeScript,可以通过在项目的 tsconfig.json 中设置 moduleResolution: 'bundler'resolvePackageJsonImports: false 来匹配解析行为。

验证项目中的变更

对于现有项目,我们建议早期采用者按照以下步骤查看在启用 unstable_enablePackageExports 后是否发生解析变更。这是一个一次性过程。很可能根本不会有任何变更,但我们希望开发人员能够确信地选择加入。

💡 验证项目中的变更
注意

如果您不使用 Yarn,请将 yarn 替换为 npx(或项目中使用的相关工具)。

  1. 获取所有已解析的依赖项(变更前)

    # 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:如果您的项目尚未有 metro.config.js 文件,请运行 npx expo customize metro.config.js
    • 为了全面覆盖,请将 --platform android 替换为您的应用程序正在使用的其他平台(例如 iosweb)。
  2. metro.config.js 中启用 resolver.unstable_enablePackageExports

  3. 获取所有已解析的依赖项(变更后)

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

    diff before.txt after.txt

重大变更

我们决定在 Metro 中实现 Package Exports,该实现符合规范(需要一些重大更改),但其他方面向后兼容(帮助现有导入的应用程序逐步迁移)。

关键的重大更改是,当包提供 "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 的默认行为保持一致。因此,如果用户提出警告,我们建议所有开发人员都处理这些警告

对于包维护者(预览版)

信息

根据我们的推广计划,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_ 前缀(在处理完计划的性能工作和任何 bug 之后),并将默认启用 Package Exports 解析。

随着 "exports" 对所有人启用,我们可以开始推动 React Native 社区向前发展——例如,React Native 的核心包可以更新,以更好地分离公共和内部模块。

Rollout plan for Package Exports support

致谢

感谢参与 RFC 并提供反馈的 React Native 社区成员:@SimenB@tido64@byCedric@thymikee

衷心感谢 Meta 的 @motiz88@robhogan 对此功能开发的支持。