跳到主要内容

React Native 中的包导出支持

· 阅读需 10 分钟
Alex Hunt
Alex Hunt
Software Engineer @ Meta

随着 React Native 0.72 的发布,我们的 JavaScript 构建工具 Metro 现已加入对 package.json 中 "exports" 字段的测试支持。启用后(详情见此处),它提供了以下功能:

本文将介绍包导出的工作原理,以及这些变更对你作为 React Native 应用开发者或包维护者意味着什么。

什么是包导出(Package Exports)?​

包导出是 Node.js 12.7.0 引入的现代机制,允许 npm 包指定入口点——即可外部导入的包子路径与其应解析到的文件之间的映射。

支持 "exports" 可提升 React Native 项目与更广泛 JavaScript 生态系统的兼容性(目前约有 1.66 万包使用),并为包作者提供多平台包针对 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';
// 无法访问 - 未在 "exports" 中列出!

包导出的主要特性包括:

  • 包封装:只有在 "exports" 中定义的子路径才能被包外部导入——赋予包对其公共 API 的控制权。
  • 子路径别名:包可以定义自定义子路径映射到不同的文件位置(包括通过子路径模式),允许重定位文件的同时保持公共 API 不变。
  • 条件导出:同一子路径可根据环境解析到不同的底层文件,比如针对 "node"、"browser" 或 "react-native" 运行时——替代了之前的 "browser" 字段规范。
备注

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

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

针对应用开发者​

包导出功能今天可通过测试版启用。

  • 对使用包导出功能的包(例如 Firebase 和 Storybook)的导入现在应该能正常工作。
  • 使用 Metro 的 React Native for Web 项目现在也能使用 "browser" 条件导出,省去了许多解决方案。

启用包导出会引入一些 边缘案例的破坏性变更,可能会影响特定项目,你可以立即进行验证。

未来 React Native 版本中,包导出将默认启用。此前 React Native 应用是一些包迁移到 "exports" 的瓶颈,或使用我们的 "react-native" 根字段作为逃生通道。Metro 支持这些功能将推动整个生态系统发展。

启用包导出(测试版)​

你可通过在应用的 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. 获取所有解析依赖(变更前):

    # 如有需要,将 index.js 替换为你的入口文件,比如 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 为项目使用的其他平台(如 ios、web)。
  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.json 中存在 "exports" 字段时,Metro 会优先咨询该字段(优先于其他字段),并直接使用匹配子路径的目标映射。

更多详情请参考 Metro 文档中的所有 破坏性变更。

包封装是宽松的​

当 Metro 遇到未列在 "exports" 中的子路径时,会回退到传统解析。这是兼容性特性,旨在减少现存 React Native 项目中允许的导入所带来的用户摩擦。

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

warn: 你导入了模块 "foo/private/fn.js",但它未列在 "foo" 包的 "exports" 中。建议更新调用位置或联系包维护者以公开该 API。
备注

我们计划未来引入严格模式,实现与 Node 默认行为一致的包封装策略。因此,建议所有开发者在出现警告时积极修正。

针对包维护者(预览)​

信息

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

我们暂无计划在近期移除对 "main" 字段以及其他当前包解析功能的支持。

包导出让你能限制包的内部访问权限,并提供更可预测的手段来针对 React Native 和 React Native for Web 编写库。

如果你今天已经使用 "exports"​

若你的包同时使用 "exports" 和现有的 "react-native" 根字段,请留意上述用户侧的破坏性变更。启用此功能的用户在模块解析时,"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,应在此条件之前指定 "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 核心包可更新以更好地划分公共与内部模块。

Package Exports 支持的发布计划

致谢​

感谢 React Native 社区成员对该 RFC 的反馈支持:SimenB、tido64、byCedric、thymikee。

特别感谢 Meta 的 motiz88 和 robhogan 对该功能开发的支持。