跳到主要内容

React Native 中的包导出支持

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

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

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

什么是包导出?

在 Node.js 12.7.0 中引入的包导出,是 npm 包指定入口点的现代方法 — 即包子路径的映射,这些子路径可以被外部导入,以及它们应该解析到哪个文件。

支持 "exports" 改进了 React Native 项目与更广泛的 JavaScript 生态系统(目前约有 16.6k 个包在使用)协同工作的方式,并为包作者提供了一套标准化的功能集,以支持多平台包针对 React Native。

"exports" 可以与 package.json 文件中的 "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"!

包导出的主要功能是

  • 包封装:只有在 "exports" 中定义的子路径才能从包外部导入 — 这使得包可以控制其公共 API。
  • 子路径别名:包可以定义自定义子路径,这些子路径映射到不同的文件位置(包括通过 子路径模式)— 允许在保留公共 API 的同时重新定位文件。
  • 条件导出:一个子路径可能会解析到不同的底层文件,具体取决于环境。例如,可以针对 "node""browser""react-native" 运行时 — 取代了 "browser" 字段规范
注意

"exports" 的全部功能在 Node.js 包入口点规范中有详细介绍。

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

对于应用程序开发者

包导出功能今天即可启用,目前处于 Beta 阶段。

  • 依赖包导出功能的包(如 FirebaseStorybook)的导入现在应该可以按设计工作。
  • 使用 Metro 的 React Native for Web 项目现在将能够使用 "browser" 条件导出,无需进行变通。

启用包导出功能会带来一些 可能影响特定项目的兼容性变更,您可以 今天就进行测试

在未来的 React Native 版本中,包导出将默认启用。这是一个先有鸡还是先有蛋的局面,React Native 应用过去曾是某些包迁移到 "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 后查看解析是否发生变化。这是一个一次性过程。很可能根本不会有任何变化,但我们希望开发者能够确定地选择加入。

💡 验证项目中的更改
注意

如果您不使用 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
    • 为了获得全面覆盖,请用您的应用程序使用的其他平台(例如 iosweb)替换 --platform android
  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 中实现了一个符合规范的包导出(这需要一些兼容性变更),但在其他方面是向后兼容的(有助于应用程序逐步迁移现有导入)。

关键的兼容性变更在于,当包提供了 "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 的默认行为保持一致。因此,我们建议所有开发者都处理这些警告,如果用户提出的话。

对于包维护者(预览版)

信息

根据我们的 发布计划,在今年晚些时候的下一个 React Native 版本(0.73)中,包导出将对大多数项目启用。

我们目前没有计划很快移除对 "main" 字段和其他当前包解析功能的支持。

包导出提供了限制对包内部访问的能力,以及更可预测的库针对 React Native 和 React Native for Web 的功能。

如果您今天正在使用 "exports"

如果您的包与当前的 "react-native" 根字段一起使用 "exports",请注意上述对用户的影响的兼容性变更。对于在 Metro 中启用此功能的用户,"exports" 现在将在模块解析过程中被优先考虑。

实际上,我们预计用户的主要变化将是(通过警告)强制执行应用程序中任何不可访问的子路径,以尊重 "exports" 的包封装。

迁移到 "exports"

向您的包添加 "exports" 字段是完全可选的。对于不使用 "exports" 的包,现有的包解析功能将表现相同 — 并且我们没有计划移除这种行为。

我们相信 "exports" 的新功能为 React Native 包维护者提供了一套有吸引力的功能。

  • 收紧您的包 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,应在 "react-native" 条件之前指定 "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 后),并将默认启用包导出解析。

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

Rollout plan for Package Exports support

致谢

感谢 React Native 社区成员对 RFC 提供反馈:@SimenB@tido64@byCedric@thymikee

非常感谢 Meta 的 @motiz88@robhogan 支持此功能的开发。