React Native 中的 Package Exports 支持
随着 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 为目标。
"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"!
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 功能的软件包(例如 Firebase 和 Storybook)的导入现在应该按设计工作。
- 使用 Metro 的 React Native for Web 项目现在将能够使用
"browser"
条件导出,从而无需进行任何解决方法。
启用 Package Exports 会带来一些 边缘情况的重大更改,这些更改可能会影响特定项目,您可以立即测试。
在未来的 React Native 版本中,Package Exports 将默认启用。在先有鸡还是先有蛋的情况下,React Native 应用程序以前是一些软件包迁移到 "exports"
的障碍——或者使用了我们的 "react-native"
根字段转义舱口。在 Metro 中支持这些功能将使生态系统向前发展。
启用 Package Exports(Beta 版)
可以在应用程序的 metro.config.js 文件中通过 resolver.unstable_enablePackageExports
选项启用 Package Exports。
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
(或项目中使用的相关工具)。
-
获取所有已解析的依赖项(更改前)
# 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 中实现 Package Exports,该实现符合规范(需要进行一些重大更改),但在其他方面向后兼容(帮助具有现有导入的应用程序逐步迁移)。
关键的重大更改是,当软件包提供 "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 的默认行为保持一致。因此,我们建议所有开发人员解决用户提出的这些警告。
对于软件包维护者(预览)
根据我们的推广计划,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,从而减少错误发生的表面积。
- 条件导出:如果您的软件包以 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_
前缀(在解决了计划的性能工作和任何错误之后),并将默认启用 Package Exports 解析。
随着为所有人启用 "exports"
,我们可以开始推动 React Native 社区向前发展——例如,可以更新 React Native 的核心软件包,以更好地分离公共模块和内部模块。
感谢
感谢 React Native 社区成员对 RFC 提供的反馈:@SimenB、@tido64、@byCedric、@thymikee。