内嵌依赖(vendoring)

Carson Gross

"内嵌依赖(vendoring)"软件是一种将另一个项目的源代码直接复制到自己项目中的技术。

这是一种古老的技术,在软件开发中由来已久,但"vendoring"这个术语似乎起源于Ruby社区

内嵌依赖可以并且仍然在今天使用。例如,你可以很轻松地内嵌htmx。

假设你的项目中有一个/js/vendor目录,你可以像这样将源代码下载到自己的项目中:

curl https://raw.githubusercontent.com/bigskysoftware/htmx/refs/tags/v2.0.4/dist/htmx.min.js > /js/vendor/htmx-2.0.4.min.js

然后在head标签中包含该库:

<script src="/js/vendor/htmx-2.0.4.min.js"></script>

然后将htmx源代码检入你自己的源代码控制仓库。(我甚至建议考虑使用非最小化版本,以便更好地理解和调试代码。)

就是这样,这就是内嵌依赖。

内嵌依赖的优势

好的,那么内嵌依赖库有哪些优势呢?

事实证明有不少:

另一方面,内嵌依赖也有一个巨大的缺点:通常没有好的方法来解决所谓的传递依赖问题。

如果htmx有子依赖项,即它依赖的其他库,那么要正确内嵌它,你必须开始内嵌所有这些库。如果这些依赖项还有进一步的依赖项,你也需要安装它们……如此往复。

更糟糕的是,两个依赖项可能依赖同一个库,你需要确保为所有东西都获得正确版本的库。

这可能相当难以处理,但我想提出一个看似矛盾的观点:这个弱点(再次强调,这是真实的)在某种程度上其实是一种优势:

因为处理大量依赖项很困难,内嵌依赖鼓励了一种独立的文化。

你让什么变得容易,就会得到更多什么。如果你让依赖变得容易,特别是传递依赖,你就会得到更多依赖。

而且,正如我们稍后将看到的,也许更少的依赖并不是件坏事。

依赖管理器

这些都很好,但内嵌依赖有显著缺点,特别是传递依赖问题。

现代软件工程使用依赖管理器来处理软件项目的依赖关系。这些工具允许你指定项目的依赖项,通常通过某种文件。然后它们会安装这些依赖项,并解析和管理这些依赖项正常工作所需的所有其他依赖项。

使用最广泛的包管理器之一是NPM:Node包管理器。尽管没有运行时依赖,htmx使用NPM指定了16个开发依赖项。开发依赖项是开发htmx所必需的依赖项,但运行它则不需要。你可以在项目NPM package.json文件的底部看到这些依赖项。

依赖管理器是现代软件开发的关键部分,如今许多开发者无法想象没有它们如何编写软件。

依赖管理器的问题

因此依赖管理器解决了内嵌依赖存在的传递依赖问题。但是,正如软件工程中的一切一样,它们也有相关的权衡。要了解其中一些问题,让我们看看htmx中的package-lock.json文件。

NPM生成一个package-lock.json文件,其中包含项目已解析的传递依赖闭包,以及这些依赖项的具体版本。这有助于确保除非用户显式更新它们,否则使用相同的依赖项。

如果你查看htmx的package-lock.json,你会发现原始的13个开发依赖项已经膨胀到总共411个依赖项。

事实证明,htmx依赖大量的包,尽管它自豪于相对精简。实际上,htmx中的node_modules文件夹高达110兆字节!

但是,除了这种膨胀之外,在那堆依赖中还有更深层次的问题潜伏着。

在写这篇文章时,我发现htmx显然依赖于array.prototype.findlastindex,这是一个为2022年引入的JavaScript功能提供的polyfill

现在,htmx 1.x是兼容IE的,而我不想要任何polyfill:我想编写无需任何额外库支持就能在IE中工作的代码。然而一个polyfill通过依赖链(htmx不直接依赖它)悄悄混入,引入了一个危险的polyfill,会让我编写的代码在IE和其他旧版浏览器中崩溃。

这个polyfill在我运行htmx 测试套件时可能可用也可能不可用(很难说),但重点是:一些危险的代码在我不知情的情况下潜入了我的项目,这是由于其(开发)依赖项的数量和复杂性造成的。

这展示了依赖管理器存在的一个严重文化问题:

它们倾向于培养一种,嗯,依赖的文化。

一个典型的例子是臭名昭著的left-pad事件,其中一位工程师下架了一个广泛使用的包,并破坏了Facebook、PayPal、Netflix等公司的构建。

那是一个相对无害但轰动的事件,但更严重的问题是供应链攻击,敌对实体能够通过依赖项中无意注入的代码入侵公司。

我们的依赖图越大,这些问题就越严重。

重新思考依赖

我不是唯一一个思考我们依赖文化的人。以下是一些更聪明的人对此的看法:

Armin RonacherFlask的创建者最近在旧推上这样说:

我构建的软件越多,就越讨厌依赖。我更喜欢人们将内容复制/粘贴到自己的代码库中或重新实现它。不幸的是,这个时代的氛围不太接受这个想法。我需要那种氛围转变。

他还写了一篇很棒的博客文章,讲述他在Rust生态系统中的包管理经验

是时候有新的视角了:我们应该向那些自己编写小函数而不是引入传递依赖网络的工程师致敬。我们应该警惕大的依赖图。值得称赞的是最小的依赖关系,是那些只是安静地完成工作的普通函数,是那些因为一次做对而多年不需要碰的代码。

请通读全文。

早在2021年,Tom MacWright就在《默认内嵌依赖》中写道:

但我觉得有点不寻常的是:我内嵌了很多东西。

内嵌依赖,在编程意义上,是指"将另一个项目的源代码复制到你的项目中"。这与使用依赖的做法形成对比,后者会将另一个项目的名称添加到你的package.json文件中,并让npm或yarn为你下载和链接它。

我也强烈推荐阅读他对内嵌依赖的看法。

为内嵌依赖设计的软件

如果你是一个开源开发者并且喜欢内嵌依赖的想法,有个好消息:有一种简单的方法可以让你的软件对内嵌友好:尽可能减少依赖项。

例如,DaisyUI一直在移除其依赖项,从版本3的100个依赖项减少到版本5的0个。

还有一组htmx相关项目正在认真对待内嵌依赖:

这些JavaScript项目都没有在NPM上提供,它们都推荐软件内嵌到你的项目中作为主要安装机制。

内嵌优先的依赖管理器?

最后我想简单提一下一种结合了内嵌依赖和依赖管理两者的技术:内嵌优先的依赖管理器。我以前从未使用过,但有人向我推荐了vend,一个Common Lisp的内嵌导向包管理器(有很棒的README),以及Go的内嵌选项

在写这篇文章时,我还发现了vendorpullgit-vendor,它们都是小而有趣的项目。

这些看起来都是很棒的工具,在我看来,有机会让其中一些(以及类似工具)添加额外功能来解决内嵌依赖的传统弱点,例如:

有了这些额外功能,我想知道内嵌优先的依赖管理器是否能在现代软件开发中与"普通"依赖管理器竞争,也许能结合两种方法的一些优点。

无论如何,我希望这篇文章能帮助你更多思考依赖关系,也许能种下一个想法:也许你的软件可以少一点,嗯,对依赖的依赖。

</>