Val Town 是一个依赖项众多的 React 应用程序。它很复杂,而且我们总是得处理依赖的更新。我们犯了一个根本性的错误——把网页设计的太复杂了:在撰写本文时,我们的 node_modules 目录大小为 863MB,诶~
这有什么问题吗?我们是否在随意安装依赖项,不断地增加技术债务?我想说,并非如此。
问题在于,我们试图构建的东西存在一些固有的复杂性。我们不打算自己动手开发 TypeScript 转译器,也不打算避免安装 CodeMirror 而使用文本区域进行代码编辑。我每周都会花一些时间查看 package.json,思考哪些可以删除。有时我能找到可以删除的依赖项,但很多时候我都是空手而归:我们实际上需要所有这些东西。经过现实毒打之后,我看人看事反而越来越不敢轻易下判断了。
但打理依赖关系这事儿其实也有门道。我自己琢磨出了一整套让依赖保持整洁的技巧和工具,虽然从来没完整写过,今天就试着唠一唠。
阅读所有新的依赖项(React 除外)
第一条规则是阅读。这是字面意思:阅读您要引入项目的任何依赖项的源代码。当然,还有 README。我强烈建议您用自己的眼睛和大脑来做这件事,但如果您更喜欢,大型语言模型 (LLM) 也能提供帮助:但不要把整个任务都交给机器人。真正的理解才是目标,而这是无法通过二手信息实现的。
通常,您会发现您要添加的新依赖项只有 50 行,最好是直接将其代码复制过来,而不是使用 NPM 安装:只需将代码复制过来,并在代码注释中保留其开源许可证即可。
不然你就会发现,这个模块压缩后都有2MB,还额外引入了三个间接依赖包,而你实际上只用到了其中的50行代码。这同样不是好事:你引入的这些依赖纯粹是负担——它们会占用更多的node_modules目录空间,而那些你压根用不上的部分可能隐藏着安全漏洞,你却照样得被迫替它擦屁股。
我认为 React 和其他大型依赖项是个例外:我看过 React 的算法和 TypeScript 编译器的内部代码,并决定我只需要信任那些公司的专家。
不阅读,就不会了解。
npm ls 和阅读 package-lock.json 是你的好朋友
或者,如果你使用 pnpm,那么就是 pnpm-lock.yaml 和 pnpm why。以及你使用的任何其他包管理器的类似命令。原因很简单:你的直接依赖项不可避免地只是冰山一角。真正填满 node_modules 的是它们带来的所有东西,或者说真正重要的是这些东西:传递性依赖项。
例如,假设你的项目需要转译 TypeScript。很可能你已经安装了一个转译器:在我们的项目中,我们有 drizzle-kit、Vite 和 tsx 引入的 esbuild 副本。因此,将 esbuild 作为直接依赖项不会增加任何成本:它会被去重到之前所有东西都在底层使用的同一个 esbuild 二进制文件。这是一个很有用的小技巧:当你为你的应用程序安装某些东西时,如果你能找到一种方法来重用已经作为传递性依赖项安装的东西,你就可以免费获得一个依赖项!
而且要阅读 package-lock.json 或 pnpm-lock.yaml。它并没有那么糟糕,你会学到一些东西。里面有很多信息。它会让你熟悉其他模块依赖哪些模块,这会在你的头脑中建立一个小型的 PageRank 算法,这样你就可以在遇到新问题时,立刻回想起可能需要使用的东西。满足你的好奇心,打开那些 npmjs.com 网页吧。
分析包的实际大小非常有用
大型 NPM 模块有两个不同的影响:它们对你发布的应用程序的贡献,以及它们在开发应用程序时在 node_modules 中占用的空间。首先关注应用程序大小是很好的,但两者都很重要:一个依赖 2GB 代码的 node_modules 的应用程序在持续集成中测试会很慢,部署也会更慢,因为它需要下载更多的依赖。
我使用 Grand Perspective 这个已经存在了 20 年的老牌应用程序来跟踪磁盘上的 node_modules,但如果你使用 Linux 或 Windows,还有很多其他值得考虑的磁盘空间分析器。
分析你发布的应用程序的大小是一个更复杂且与系统相关的任务。我们正在使用带有 Vite 的 React Router,所以 rollup-plugin-visualizer 是解决方案,但对于每个打包器来说情况都不同,一些框架有自己的解决方案,比如 Next.js 的包分析器。
一个好的 NPM 模块应该是什么样的
但你真正在寻找的是什么?一个好的模块的定义在不断变化,但通常它会是这样的:有良好的维护历史、内置 TypeScript 类型、通过测试、文档齐全。
用一个俚语来概括,就是它应该有一种“靠谱”的感觉。即使你正在仓促地构建一些东西,并且犯了很多错误,你也希望你使用的部分是可靠的。一个应用程序的错误是你自己写的错误和从别人那里继承的错误的总和,所以对你安装的代码比对自己写的代码有更高的标准实际上是公平的。
什么是一个写的很垃圾的模块?当然,一个被废弃且写得很差的模块固然很拉,但比这更糟糕的是一个解决错误问题的模块——它实际上不适合你遇到的问题,相反,你不得不改变问题来让它起作用。你可以通过阅读并花一些时间来理解你的问题和解决方案来解决这个问题。或者问问语言大模型,你这个小孩子。
剔除不用的,并保持其余的更新
你应该使用 Renovate。它会督促你保持模块更新,而且这种工作最好是逐步进行,而不是每年集中处理一次。
你应该使用 Knip。它简直就是魔法:它速度极快,而且非常准确。它会告诉你哪些模块你在 package.json 中指定了,但实际上并没有使用。很容易就会失去跟踪,导致项目旧版本中残留了很多垃圾。用 Knip 把它们清理掉吧!它甚至会显示你的项目不再使用的文件。如果有一件 Knip 的 T 恤,我现在就会穿着它,就是这么好。
有一个写的牛逼模块的人的速查表
NPM 模块生态系统是由人组成的。了解这些人是谁很有用!例如,当我寻找与 Promises(或许多其他主题)相关的东西时,我会检查 Sindre Sorhus 是否已经发布了什么。他通常都有!
其他有很多可靠作品的人也值得了解,比如 isaacs、Matteo Collina、Mafintosh。
如果你正在处理 Markdown,你应该知道所有的 wooorm 和 unified 仓库。下一代 Node.js 的东西?看看 unjs。转译器内部,你应该看一下 Rich Harris 的项目,里面可是包含了很多宝藏。
请继续关注后续文章,其中会包含一些你可以使用的依赖项的起点,如果你愿意,甚至可以把它们放在一个 AGENTS.md 文件中。
依赖项是不可避免的
事实就是如此:我们都站在巨人的肩膀上。但找到合适的肩膀来站立是一门艺术。
在某种程度上,Web 平台和 NPM 模块生态系统发展如此之快,需要如此多挑剔的更新和决策,这真是令人抑郁。但这几乎是常态:即使是吸取了 NPM 和 Node 教训的下一代语言也遭受着臃肿的包生态系统的困扰。管理依赖项是工作的一部分,我们应该把它做好。