Please Stop Using Barrel Files
请停止使用桶文件
— JavaScript, TypeScript — 4 min read
— JavaScript,TypeScript — 阅读时间:4 分钟
- No translations available.
- Add translation 添加翻译
没有可用的翻译。
How to organize your files "correctly" is a hot, controversial topic among frontend developers. Move files around until it feels right is a common meme, but it's also not a joke according to author Dan Abramov. This is probably fine if you're working alone, but in teams, different things will likely feel right for different persons. So the advice is pretty subjective, and thus not really helpful in a professional environment.
如何“正确”地组织文件是前端开发者中一个热门且有争议的话题。移动文件直到感觉合适是一个常见的梗,但根据作者丹·阿布拉莫夫的说法,这并不是一个笑话。如果你独自工作,这可能没问题,但在团队中,不同的人可能会对不同的事情有不同的感觉。因此,这种建议是相当主观的,因此在专业环境中并不真正有帮助。
As far as I've experienced, most developers are happy to adjust to whatever structure a project is already using. Having a homogenous code base that's straightforward to grasp - even if you personally disagree with a structure - is a lot better than everybody just doing what they prefer.
根据我的经验,大多数开发人员乐于适应项目已经使用的任何结构。拥有一个统一的代码库,易于理解——即使你个人不同意某种结构——总比每个人都只做自己喜欢的事情要好得多。
I have my preferences, and (probably) so do you. I'm team consistency, and I'd want to statically enforce as much as possible to avoid bike-shedding discussions. The unicorn/filename-case rule is a very good example for that. Choose one casing, apply it everywhere, be done with it.
我有我的偏好,你可能也有。我支持一致性,并且我希望尽可能静态地强制执行,以避免无谓的讨论。unicorn/filename-case 规则就是一个很好的例子。选择一种大小写,应用到所有地方,完成它。
That said, there is one thing that I've put a lot of emphasis on lately, and that is avoiding barrel files at all costs.
也就是说,最近我非常强调的一件事是 不惜一切代价避免使用桶文件。
What are barrel files? 桶文件是什么?
A barrel is a file that does nothing but re-export things from other files. Usually, those are named index.js
or index.ts
. It's meant to hide the "internal" structure of a directory to its consumers. As an example, consider the following directory in your repository:
一个桶是一个文件,它只做一件事,就是从其他文件重新导出内容。通常,这些文件被命名为 index.js
或 index.ts
。它的目的是向其使用者隐藏目录的“内部”结构。作为一个例子,考虑一下您仓库中的以下目录:
1- tab2 - tab-list.tsx3 - tab-panel.tsx
Now let's assume each file exports a single component. That means we can import those modules individually like so:
现在假设每个文件导出一个单独的组件。这意味着我们可以像这样单独导入这些模块:
1import { TabList } from '@/tab/tab-list'2import { TabPanel } from '@/tab/tab-panel'
Oftentimes, developers want to improve the code organization by providing a single interface to import everything tab related from. We would usually do that with a barrel file that re-exports those:
开发人员通常希望通过提供一个单一的接口来导入与标签相关的所有内容,从而改善代码组织。我们通常会使用一个桶文件来重新导出这些内容。
1export { TabList } from './tab-list'2export { TabPanel } from './tab-panel'
Because the file is named index
, which can be omitted when importing, we get a seemingly cleared up usage site:
因为文件名为index
,在导入时可以省略,因此我们得到了一个看似清晰的使用站点:
1import { TabList, TabPanel } from '@/tab'
This not only seems a lot cleaner, it also means we can shift internal files around without having to update all consumer sides. So where's the catch?
这不仅看起来更干净,而且意味着我们可以在内部文件之间移动,而不必更新所有消费者端。那么,问题出在哪里呢?
Circular imports 循环导入
Having a barrel file in a directory changes how we import depending on our current location. Let's extend our example by adding a use-tab-state.ts
util. It's a custom hook that we're going to use inside our TabPanel
component. Additionally, we want to export it for everyone to use, which is why we add it to our barrel file:
在一个目录中拥有一个桶文件会根据我们当前的位置改变导入方式。让我们通过添加一个 use-tab-state.ts
工具来扩展我们的示例。这是一个我们将在 TabPanel
组件内部使用的自定义钩子。此外,我们希望将其导出供所有人使用,这就是我们将其添加到桶文件中的原因。
1export { TabList } from './tab-list'2export { TabPanel } from './tab-panel'3export { useTabState } from './use-tab-state'
So far, so good - consumers can import useTabState
directly from the barrel file. However, what happens if we are inside TabPanel
and we import useTabState
? It depends on how we write our import statement:
到目前为止,一切顺利——消费者可以直接从桶文件中导入 useTabState
。然而,如果我们在 TabPanel
内部并导入 useTabState
,会发生什么呢?这取决于我们如何编写导入语句:
1// ✅ good: imports from use-tab-state module2import { useTabState } from './use-tab-state'3
4// ❌ bad: these both import from the barrel file5import { useTabState } from '@/tab'6import { useTabState } from './'
If we do what we always do - importing from the barrel file - we will create a circular import, where tab-panel.ts
will import index.ts
, which imports tab-panel.ts
(because it re-exports it).
如果我们做我们一直在做的事情 - 从桶文件中导入 - 我们将创建一个循环导入,其中 tab-panel.ts
将导入 index.ts
,而 index.ts
又导入 tab-panel.ts
(因为它重新导出了它)。
Now JavaScript is quite tolerant when it comes to circular imports, but I've seen bundlers crash with the weirdest of error messages because of it. Those imports can also happen accidentally, because let's face it: most of the time, we just auto import and leave it at whatever our editor (or copilot) decides. At least I haven't written a manual import statement in a long time.
现在,JavaScript 在处理循环导入时相当宽容,但我见过打包工具因其而崩溃,出现奇怪的错误信息。这些导入也可能是意外发生的,因为说实话:大多数时候,我们只是自动导入,任由我们的编辑器(或副驾驶)决定。至少我已经很久没有手动编写导入语句了。
The lint rule import/no-cycle can catch a lot of these circles, so I can recommend turning that on.
该 lint 规则 import/no-cycle 可以捕捉到很多这样的循环,因此我建议开启它。
Development speed 开发速度
The second issue with barrels shows up when we think about what happens under the hood when a barrel file is imported. If we import { useTabState } from '@/tab'
, JavaScript will traverse the index.ts
file and load every module inside of it, synchronously. This probably doesn't matter much if our barrel only has three files, but it can get out of hand pretty quickly, for example, if one of our other imports that we don't need import from another barrel, or from a 3rd party lib that again imports many many modules.
第二个与桶文件相关的问题出现在我们考虑当一个桶文件被导入时,内部发生了什么。如果我们 import { useTabState } from '@/tab'
,JavaScript 将遍历 index.ts
文件并同步加载其中的每一个模块。如果我们的桶文件只有三个文件,这可能并不太重要,但如果我们其他不需要的导入又从另一个桶文件或从一个再次导入许多模块的第三方库中导入,那么情况可能会迅速失控。
In our NextJs project, I have seen pages that were loading over 11k modules, which took 5-10 seconds to start-up the page. After we started to get rid of most of our internal barrel files, we got that down to about 3.5k modules - a reduction of 68%. Turns out it's not so good if you have a shared package that exports a ton of things via a barrel and you only need a single module from it. 😅
在我们的 NextJs 项目中,我看到有些页面加载了超过 11,000 个模块,这使得页面启动需要 5 到 10 秒。我们开始摆脱大部分内部桶文件后,模块数量减少到了大约 3,500 个,减少了 68%。事实证明,如果你有一个共享包通过桶导出大量内容,而你只需要其中的一个模块,这样并不好。😅
The NextJs team has also realized that barrel file are a real problem in development mode and have started to ship the optimizePackageImports
feature to automatically transform imports from barrel files into their real module paths. The blog post How we optimized package imports in Next.js by Shu Ding explains in detail how this works. What's most interesting is that those optimizations can only be applied if the barrel is a "real" barrel - which means it does nothing but export other things. As soon as you add one line that isn't a re-export, like:
NextJs 团队也意识到,在开发模式下,桶文件确实是一个问题,并开始推出optimizePackageImports
功能,以自动将桶文件中的导入转换为其真实模块路径。博客文章我们如何优化 Next.js 中的包导入由Shu Ding详细解释了这一过程。最有趣的是,这些优化只能在桶是“真实”桶的情况下应用——这意味着它只做其他东西的重新导出。一旦你添加了一行不是重新导出的代码,例如:
1export { TabList } from './tab-list'2export { TabPanel } from './tab-panel'3export { useTabState } from './use-tab-state'4
5// ❌ bad: this makes the whole file non-optimizable6export const foo = 5
it can't be optimized because of potential side effects. So again, the best thing is to just avoid barrel files.
它无法被优化,因为可能会产生副作用。因此,再次强调,最好的办法就是避免使用桶文件。
What barrels are good for
什么桶是好的
In my opinion, barrels aren't made to group content of directories in your product application. Unless you add even stricter lint rules, there isn't anything that would force other developers to only import from the barrels, so you can't make certain modules "private" with them.
在我看来,桶并不是用来在您的产品应用中对目录内容进行分组的。除非您添加更严格的 lint 规则,否则没有任何东西可以强制其他开发人员仅从桶中导入,因此您无法使某些模块与它们“私有”。
Where barrels are necessary is when you are writing a library. Libraries like @tanstack/react-query
need a single entry point file that we can put into the main
field of package.json
. This is the public interface of what consumers can use. To me, this is the only place where a barrel makes sense. But if you are writing app code, you're just making your life harder by putting index.ts
files into arbitrary directories. So please stop using barrel files.
在编写库时,桶是必要的。像 @tanstack/react-query
这样的库需要一个单一的入口文件,我们可以将其放入 main
字段的 package.json
中。这是消费者可以使用的公共接口。对我来说,这就是桶有意义的唯一地方。但是,如果你在编写应用代码,随意将 index.ts
文件放入任意目录只会让你的生活变得更加困难。所以请停止使用桶文件。
That's it for today. Feel free to reach out to me on twitter
if you have any questions, or just leave a comment below. ⬇️
今天就到这里。如果您有任何问题,请随时通过推特与我联系,或者在下面留言。⬇️