图书馆作者应对桶式文件的实用指南

2025-06-07

图书馆作者应对桶式文件的实用指南

在过去的几个月里,我一直在研究针对 barrel 文件的工具。barrel 文件本质上就是一个很大的index.js文件,它会重新导出包中的所有其他内容。这意味着,如果你只从该 barrel 文件中导入一个内容,最终会加载其模块图中的所有内容。这会带来很多负面影响,从降低运行时性能到降低打包器性能。你可以在这里找到关于这个主题的详细介绍,或者在这里深入了解 barrel 文件对一个流行的真实库的影响

在这篇文章中,我将为库作者提供一些关于如何处理包中的桶文件的实用建议。

使用自动化工具

首先,自动化工具可以帮上大忙。例如,如果你想检查你的项目是否以无 barrel 文件的方式设置,你可以使用barrel-begone

npx barrel-begone
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

避免从桶文件导入

如果你作为库作者从 barrel 文件导入了某些内容,它也会发出警告。例如,你的库可能使用了外部库:

import { thing } from 'external-dep-thats-a-barrel-file';
Enter fullscreen mode Exit fullscreen mode

这将给出以下警告:

The imported module is a barrel file, which leads to importing a module graph of <number> modules
Enter fullscreen mode Exit fullscreen mode

如果遇到这种情况,您可以尝试寻找更具体的入口点来导入thing。如果项目有一个包导出映射,您可以查阅它,看看是否有更具体的导入方法。以下是一个例子:

{
  "name": "external-dep-thats-a-barrel-file",
  "exports": {
    ".": "./barrel-file.js", // 🚨
    "./thing.js": "./lib/thing.js" // 
  }
}
Enter fullscreen mode Exit fullscreen mode

在这种情况下,我们只需导入表单就可以大大优化我们的模块图大小"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';
Enter fullscreen mode Exit fullscreen mode

现在,如果我作为您的包的消费者,只导入:

import { debounce } from 'my-utils-lib';
Enter fullscreen mode Exit fullscreen mode

仍在导入index.js文件中的所有内容,以及后续所有附带的内容;或许analytics.js会用到一个庞大的分析库,而这个库本身又会导入一堆模块。而我只想用一个debounce!太浪费了。

关于摇树

“但是 Pascal,其他所有东西难道不会被我的打包工具进行 treeshaking 吗?”

首先,你错误地认为每个人在开发或测试流程的每一步都会使用打包器。也可能是你的使用者从 CDN 使用你的库,而那里不适用摇树优化。此外,由于副作用,某些模式的摇树优化效果不佳,或者可能无法像你预期的那样进行摇树优化。你的代码中可能有一些内容被打包器视为副作用,这会导致代码无法进行摇树优化。例如,你知道吗:

Math.random().toString().slice(1);
Enter fullscreen mode Exit fullscreen mode

是否被视为有副作用并且可能会扰乱你的 treeshaking?

为您的库创建细粒度的入口点

相反,你应该为你的库提供精细的入口点,并对功能进行合理的分组。例如,我们的"my-utils-lib"库:

export * from './math.js';
export { debounce, throttle } from './timing.js';
export * from './analytics.js';
Enter fullscreen mode Exit fullscreen mode

我们可以看到,暴露的功能分为几种:一些与数学相关的辅助函数,一些与时间相关的辅助函数,以及一些分析函数。在本例中,我们可能会创建以下入口点:

  • my-utils-lib/math.js
  • my-utils-lib/timing.js
  • my-utils-lib/analytics.js

现在,作为的消费者"my-utils-lib",我必须更新我的导入:

import { debounce } from 'my-utils-lib';
Enter fullscreen mode Exit fullscreen mode

到:

import { debounce } from 'my-utils-lib/timing.js';
Enter fullscreen mode Exit fullscreen mode

只需很少的努力,就可以通过 codemods 完全自动化,并且全面实现巨大改进!

如何添加细粒度的入口点?

如果您在项目中使用包导出,并且您的主要入口点是一个桶文件,那么它可能看起来像这样:

{
  "name": "my-utils-lib",
  "exports": {
    ".": "./index.js" // 🚨
  }
}
Enter fullscreen mode Exit fullscreen mode

相反,创建如下所示的入口点:

{
  "name": "my-utils-lib",
  "exports": {
    "./math.js": "./lib/math.js", // 
    "./timing.js": "./lib/timing.js", // 
    "./analytics.js": "./lib/analytics.js" // 
  }
}
Enter fullscreen mode Exit fullscreen mode

替代方案:子路径导出

或者,您可以为您的包添加子路径导出,例如:

{
  "name": "my-utils-lib",
  "exports": {
    "./lib/*": "./lib/*"
  }
}
Enter fullscreen mode Exit fullscreen mode

这将使./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'
Enter fullscreen mode Exit fullscreen mode

如果我是 的使用者msw,我可能会使用函数、函数或两者msw来模拟 API 调用。假设我的项目不使用 GraphQL,所以我只使用函数:httpgraphqlhttp

import { http } from 'msw';
Enter fullscreen mode Exit fullscreen mode

只需导入此http功能,就会导入整个桶文件,包括整个graphql项目,这会为我们的模块图添加123 个模块!

注意:msw自从我上次在案例研究中发表博文以来,还为msw/core/http和添加了细粒度的入口点msw/core/graphql,但它们仍然公开一个桶文件作为主入口点,大多数用户实际上都在使用它。

我们可以将某些类型的功能分组,而不是发送这个桶文件,例如:

http.js

export { HttpHandler, http };
Enter fullscreen mode Exit fullscreen mode

graphql.js

export { GraphQLHandler, graphql };
Enter fullscreen mode Exit fullscreen mode

内置.js

export { bypass, passthrough };
Enter fullscreen mode Exit fullscreen mode

utils.js

export { cleanUrl, getResponse, matchRequestUrl };
Enter fullscreen mode Exit fullscreen mode

这给我们留下了以下导出的杂烩,坦率地说,我不确定该怎么命名,所以我现在就将它们命名为todo.js(我也认为这些实际上只是类型,但它们不是通过type导入导入的):

export { HttpMethods, RequestHandler, SetupApi };
Enter fullscreen mode Exit fullscreen mode

我们的包导出可能看起来像这样:

{
  "name": "msw",
  "exports": {
    "./http.js": "./http.js",
    "./graphql.js": "./graphql.js",
    "./builtins.js": "./builtins.js",
    "./utils.js": "./utils.js",
    "./TODO.js": "./TODO.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,作为 MSW 的消费者,我只需从以下位置更新我的导入:

import { http } from 'msw';
Enter fullscreen mode Exit fullscreen mode

到:

import { http } from 'msw/http.js';
Enter fullscreen mode Exit fullscreen mode

变化很小,并且可以通过 codemods 完全自动化,但带来了全面的改进,并且当我在项目中甚至不使用 graphql 时,不再导入所有的 graphql 解析器!

当然,用户可能不仅导入了该http函数,还导入了其他函数'msw'。在这种情况下,用户可能需要额外导入一两个函数,但这似乎不成问题。此外,证据表明,大多数http人主要还是会导入graphql其他函数。

结论

希望这篇博文能提供一些有用的示例和指南,帮助您在项目中避免使用 barrel 文件。以下是一些有用的链接,您可以探索并了解更多信息:

文章来源:https://dev.to/thepassle/a-practical-guide-against-barrel-files-for-library-authors-118c
PREV
每日挑战#3 - 元音计数器
NEXT
React 组件生命周期详解