React Native 中的包导出支持
随着 React Native 0.72 的发布,我们的 JavaScript 构建工具 Metro 现在包含了对 package.json
"exports"
字段的测试版支持。启用后,它会增加以下功能:
在本文中,我们将介绍包导出(Package Exports)的工作原理,以及这些更改对作为 React Native 应用开发者或包维护者的你意味着什么。
什么是包导出(Package Exports)?
包导出(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';
// Inaccessible - not listed in "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 暴露了另外两个解析器选项,用于配置条件导出的行为方式:
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
(或你项目中使用的相关工具)。
-
获取所有已解析的依赖项(更改前)
# 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
替换为你的应用使用的其他平台(例如ios
、web
)。
- Expo CLI:如果你的项目还没有
-
在
metro.config.js
中启用resolver.unstable_enablePackageExports
。 -
获取所有已解析的依赖项(更改后)
yarn metro get-dependencies index.js --platform android --output after.txt
-
比较!
diff before.txt after.txt
重大更改
我们决定在 Metro 中实现符合规范(因此需要一些重大更改)但其他方面向后兼容(帮助具有现有导入的应用逐步迁移)的包导出。
关键的重大更改是,当包提供了 "exports"
时,它将首先被查阅(在任何其他 package.json
字段之前)——并且直接使用匹配的子路径目标。
- Metro 不会根据导入说明符扩展
sourceExts
。 - Metro 不会针对目标文件解析平台特定的扩展。
有关更多详细信息,请参阅 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 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。