图书馆作者应对桶式文件的实用指南
在过去的几个月里,我一直在研究针对 barrel 文件的工具。barrel 文件本质上就是一个很大的index.js文件,它会重新导出包中的所有其他内容。这意味着,如果你只从该 barrel 文件中导入一个内容,最终会加载其模块图中的所有内容。这会带来很多负面影响,从降低运行时性能到降低打包器性能。你可以在这里找到关于这个主题的详细介绍,或者在这里深入了解 barrel 文件对一个流行的真实库的影响。
在这篇文章中,我将为库作者提供一些关于如何处理包中的桶文件的实用建议。
使用自动化工具
首先,自动化工具可以帮上大忙。例如,如果你想检查你的项目是否以无 barrel 文件的方式设置,你可以使用barrel-begone:
npx barrel-begone
barrel-begone
将分析您的软件包入口点,并分析您的代码并针对各种不同的事情发出警告:
- 通过导入入口点加载的模块总数
- 文件是否为桶
- 是否使用 export *,这会导致摇树效果不佳或没有摇树
- 是否使用 import *,导致摇树效果不佳或没有摇树
- 入口点是否指向模块图中某处的桶文件
针对桶形文件的 Lint
此外,如果您使用 ESLint,可以使用eslint-plugin-barrel-files等工具来检查 barrel 文件。如果您使用Oxlint
或 之类的工具Biome
,这些规则已内置。此 eslint 插件会警告您不要编写 barrel 文件,也会警告您不要从其他 barrel 文件导入,并帮助您避免在任何地方使用 barrel 文件。
避免编写 barrel 文件
例如,如果您编写以下文件,eslint 插件将发出警告:
// The eslint rule will detect this file as being a barrel file, and warn against it
// It will also provide additional warnings, for example again using namespace re-exports:
export * from './foo.js'; // Error: Avoid namespace re-exports
export { foo, bar, baz } from './bar.js';
export * from './baz.js'; // Error: Avoid namespace re-exports
避免从桶文件导入
如果你作为库作者从 barrel 文件导入了某些内容,它也会发出警告。例如,你的库可能使用了外部库:
import { thing } from 'external-dep-thats-a-barrel-file';
这将给出以下警告:
The imported module is a barrel file, which leads to importing a module graph of <number> modules
如果遇到这种情况,您可以尝试寻找更具体的入口点来导入thing
。如果项目有一个包导出映射,您可以查阅它,看看是否有更具体的导入方法。以下是一个例子:
{
"name": "external-dep-thats-a-barrel-file",
"exports": {
".": "./barrel-file.js", // 🚨
"./thing.js": "./lib/thing.js" // ✅
}
}
在这种情况下,我们只需导入表单就可以大大优化我们的模块图大小"external-dep-thats-a-barrel-file/thing.js"
!
如果一个项目没有包导出映射,那基本上意味着任何事情都是公平的,你可以尝试在文件系统上找到更具体的导入。
不要将 barrel 文件暴露为包的入口点
避免将 barrel 文件暴露为包的入口点。假设我们有一个名为 的项目,其中包含一些实用程序"my-utils-lib"
。你可能编写了一个index.js
如下所示的文件:
export * from './math.js';
export { debounce, throttle } from './timing.js';
export * from './analytics.js';
现在,如果我作为您的包的消费者,只导入:
import { debounce } from 'my-utils-lib';
我仍在导入index.js
文件中的所有内容,以及后续所有附带的内容;或许analytics.js
会用到一个庞大的分析库,而这个库本身又会导入一堆模块。而我只想用一个debounce
!太浪费了。
关于摇树
“但是 Pascal,其他所有东西难道不会被我的打包工具进行 treeshaking 吗?”
首先,你错误地认为每个人在开发或测试流程的每一步都会使用打包器。也可能是你的使用者从 CDN 使用你的库,而那里不适用摇树优化。此外,由于副作用,某些模式的摇树优化效果不佳,或者可能无法像你预期的那样进行摇树优化。你的代码中可能有一些内容被打包器视为副作用,这会导致代码无法进行摇树优化。例如,你知道吗:
Math.random().toString().slice(1);
是否被视为有副作用并且可能会扰乱你的 treeshaking?
为您的库创建细粒度的入口点
相反,你应该为你的库提供精细的入口点,并对功能进行合理的分组。例如,我们的"my-utils-lib"
库:
export * from './math.js';
export { debounce, throttle } from './timing.js';
export * from './analytics.js';
我们可以看到,暴露的功能分为几种:一些与数学相关的辅助函数,一些与时间相关的辅助函数,以及一些分析函数。在本例中,我们可能会创建以下入口点:
my-utils-lib/math.js
my-utils-lib/timing.js
my-utils-lib/analytics.js
现在,作为的消费者"my-utils-lib"
,我必须更新我的导入:
import { debounce } from 'my-utils-lib';
到:
import { debounce } from 'my-utils-lib/timing.js';
只需很少的努力,就可以通过 codemods 完全自动化,并且全面实现巨大改进!
如何添加细粒度的入口点?
如果您在项目中使用包导出,并且您的主要入口点是一个桶文件,那么它可能看起来像这样:
{
"name": "my-utils-lib",
"exports": {
".": "./index.js" // 🚨
}
}
相反,创建如下所示的入口点:
{
"name": "my-utils-lib",
"exports": {
"./math.js": "./lib/math.js", // ✅
"./timing.js": "./lib/timing.js", // ✅
"./analytics.js": "./lib/analytics.js" // ✅
}
}
替代方案:子路径导出
或者,您可以为您的包添加子路径导出,例如:
{
"name": "my-utils-lib",
"exports": {
"./lib/*": "./lib/*"
}
}
这将使./lib/*
你的包裹的消费者可以进口任何物品。
一个真实的例子
让我们再次msw
以该库为例进行研究。msw
公开一个桶文件作为主要入口点,它看起来像这样:
注意:为了简洁起见,我省略了导出类型
export { SetupApi } from './SetupApi'
/* Request handlers */
export { RequestHandler } from './handlers/RequestHandler'
export { http } from './http'
export { HttpHandler, HttpMethods } from './handlers/HttpHandler'
export { graphql } from './graphql'
export { GraphQLHandler } from './handlers/GraphQLHandler'
/* Utils */
export { matchRequestUrl } from './utils/matching/matchRequestUrl'
export * from './utils/handleRequest'
export { getResponse } from './getResponse'
export { cleanUrl } from './utils/url/cleanUrl'
export * from './HttpResponse'
export * from './delay'
export { bypass } from './bypass'
export { passthrough } from './passthrough'
如果我是 的使用者msw
,我可能会使用函数、函数或两者msw
来模拟 API 调用。假设我的项目不使用 GraphQL,所以我只使用函数:http
graphql
http
import { http } from 'msw';
只需导入此http
功能,就会导入整个桶文件,包括整个graphql
项目,这会为我们的模块图添加123 个模块!
注意:
msw
自从我上次在案例研究中发表博文以来,还为msw/core/http
和添加了细粒度的入口点msw/core/graphql
,但它们仍然公开一个桶文件作为主入口点,大多数用户实际上都在使用它。
我们可以将某些类型的功能分组,而不是发送这个桶文件,例如:
http.js:
export { HttpHandler, http };
graphql.js:
export { GraphQLHandler, graphql };
内置.js:
export { bypass, passthrough };
utils.js:
export { cleanUrl, getResponse, matchRequestUrl };
这给我们留下了以下导出的杂烩,坦率地说,我不确定该怎么命名,所以我现在就将它们命名为todo.js(我也认为这些实际上只是类型,但它们不是通过type
导入导入的):
export { HttpMethods, RequestHandler, SetupApi };
我们的包导出可能看起来像这样:
{
"name": "msw",
"exports": {
"./http.js": "./http.js",
"./graphql.js": "./graphql.js",
"./builtins.js": "./builtins.js",
"./utils.js": "./utils.js",
"./TODO.js": "./TODO.js"
}
}
现在,作为 MSW 的消费者,我只需从以下位置更新我的导入:
import { http } from 'msw';
到:
import { http } from 'msw/http.js';
变化很小,并且可以通过 codemods 完全自动化,但带来了全面的改进,并且当我在项目中甚至不使用 graphql 时,不再导入所有的 graphql 解析器!
当然,用户可能不仅导入了该http
函数,还导入了其他函数'msw'
。在这种情况下,用户可能需要额外导入一两个函数,但这似乎不成问题。此外,证据表明,大多数http
人主要还是会导入graphql
其他函数。
结论
希望这篇博文能提供一些有用的示例和指南,帮助您在项目中避免使用 barrel 文件。以下是一些有用的链接,您可以探索并了解更多信息:
- 桶装
- eslint-plugin-barrel-files
- oxlint 无桶锉
- 生物群系noBarrelFile
- Barrel 提交了一份案例研究
- 加速 JavaScript 生态系统——Barrel 文件崩溃