跳至主要内容

React Native 中的包导出支持

·阅读时长 9 分钟
Alex Hunt
Alex Hunt
Meta 软件工程师

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

在这篇文章中,我们将介绍包导出是如何工作的,以及这些更改对您作为 React Native 应用开发者或包维护者意味着什么。

什么是包导出?

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

支持 "exports" 改进了 React Native 项目与更广泛的 JavaScript 生态系统的工作方式(目前约有 16600 个包使用),并为包作者提供了一个标准化的功能集,用于面向 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 公开了另外两个解析器选项,用于配置条件导出行为

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

**请务必使用 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
    • 为了全面覆盖,请将 --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 中实现符合规范的包导出(需要一些重大更改),但在其他方面保持向后兼容(帮助具有现有导入的应用逐步迁移)。

关键的重大更改是,当包提供 "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,从而减少了错误的表面积。
  • 条件导出:如果您的包针对 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 的核心包可以更新以更好地分离公共模块和内部模块。

Rollout plan for Package Exports support

感谢

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

特别感谢 Meta 的 @motiz88@robhogan 支持此功能的开发。