关于BarrelFile

🤔模块化

2026-03-301 min

Barrel File 通常指的是那种专门用来集中导出的文件,比如在一个目录下写一个 index.ts,然后把其他模块统一 export 出去:

ts
export * from "./button";export * from "./dialog";export * from "./form";

这样做的好处很明显:对外暴露的 API 更集中,调用方也不用关心内部文件结构。比如一个包可以只暴露 @scope/ui 这个入口,而不是让使用者去记 @scope/ui/components/button@scope/ui/components/dialog 这些路径。😋

但 Barrel File 的一些 issue 也因此体现出来了:依赖关系看起来更简单,但是可能让真实的模块依赖变得不透明。

此外,如果一个项目没有构建步骤,或者非常依赖原生 ESM 的加载行为,那么 Barrel File 的成本能很明显的体现出来。例如你导入一个入口文件时,运行时需要先解析这个入口文件,再继续解析它转发出去的模块。哪怕最终只用到了其中一个导出,也可能引入额外的模块加载和副作用风险。

所以在 no-build 架构里,理解模块系统本身非常重要。此时尽量避免随意使用 Barrel File,是一个更稳妥的选择。

但是话说回来,如果项目本身已经有成熟的构建系统,情况就不完全一样了。

现代打包工具通常会做 Tree Shaking。只要包和模块本身是 side effects free 的,未使用的导出就有机会被消除。很多常见包也是按这个思路设计的,例如 Three.js、Ramda 这类 npm 包都可以在构建阶段配合 Tree Shaking 使用。

这也是为什么我不太赞成简单地把 Barrel File 判断成“好”或者“坏”。它更像是一个边界工具:用在包的公开出口上很合理,用在业务内部的每一层目录里就未必划算。

我个人更倾向于只把 Barrel File 当成“包导出定义”来用。也就是说,它主要负责表达这个 package 对外提供了什么,而不是让整个代码库都通过统一入口互相引用。

在业务代码内部,我更希望模块之间保持明确的依赖路径。这样代码库能形成一棵更清晰的依赖树:一个模块依赖谁、谁又依赖了它,都更容易被看出来。后续重构、拆包、排查循环依赖时,也会少很多隐藏成本。

测试里尤其不应该依赖 Barrel File 来导入需要测试的模块 😡。

单元测试应该只依赖被测试的那个单元,而不是依赖某个聚合入口。哪怕构建工具最终可以 Tree Shake 掉多余内容,测试语义上也不应该把无关模块带进来。否则测试失败时,你很难快速判断问题到底来自当前单元,还是来自 Barrel File 间接引入的其他模块。

So:

  • 作为 package 的公开 API 出口,Barrel File 是有价值的。
  • 作为业务内部模块的默认导入方式,它容易掩盖依赖关系。
  • 在单元测试里,应尽量直接导入被测试模块。
  • 在 no-build 架构里,要更谨慎地使用 Barrel File。
  • 在有构建系统并且模块满足 side effects free 的前提下,Barrel File 的成本可以被 Tree Shaking 缓解。

最后还是要回到具体项目。

如果团队需要稳定的包出口,Barrel File 可以让 API 更清楚。如果团队更在意模块边界、加载行为和依赖可追踪性,那就应该减少它在内部代码里的使用。这个问题肯定是没有绝对正确的答案的,只能看是否适合当前项目具体情况。