Vendoring

Carson Gross

“Vendoring” 软件是一种技术,即将另一个项目的源代码直接复制到自己的项目中。

这是一种古老的技术,在软件开发中已使用很长时间,但“vendoring”一词用于描述它似乎起源于 Ruby 社区

Vendoring 现在仍然可以并且正在被使用。例如,你可以轻松地 vendor 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 源代码提交到你自己的源代码控制仓库中。(我甚至推荐考虑使用 非最小化版本,这样你就能更好地理解和调试代码。)

就是这样,这就是 vendoring。

Vendoring 的优势

好的,那么像这样 vendoring 库有哪些优势呢?

事实证明,有相当多的优势:

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

如果 htmx 有子依赖,即它依赖的其他库,那么要正确 vendor 它,你就必须开始 vendor 所有这些库。如果那些依赖又有进一步的依赖,你就需要安装它们……如此循环往复。

更糟的是,两个依赖可能依赖同一个库,你需要确保获取该库的 正确版本,以使一切正常工作。

这可能会变得相当难以处理,但我想要提出一个矛盾的观点,即这个弱点(再说一次,它是一个真正的弱点)在某种程度上实际上是一种优势:

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

你会得到更多你使之容易的东西,如果你使依赖容易,你就会得到更多依赖。使依赖——尤其是 传递依赖——更困难,会使它们变得不那么常见。

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

依赖管理器

这听起来很棒,但 vendoring 有 显著的 缺点, 特别是传递依赖问题。

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

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

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

依赖管理器的问题

所以依赖管理器解决了 vendoring 的传递依赖问题。但正如软件工程中的一切一样,它们也伴随着权衡。要了解这些权衡中的一些,让我们看看 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 功能 polyfillfindLastIndex)。

现在,htmx 1.x 兼容 IE,我 不想要 任何 polyfill:我想编写无需额外库支持即可在 IE 中工作的代码。然而,一个 polyfill 通过依赖链偷偷溜了进来(htmx 并不直接依赖它),引入了一个危险的 polyfill,它会让我编写在 IE 以及其他旧浏览器中会崩溃的代码。

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

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

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

一个壮观的例子是臭名昭著的 left-pad 事件,其中一位工程师删除了一个广泛使用的包,导致 Facebook、PayPal、Netflix 等公司的构建中断。

那是一个相对无害的、虽然引人注目的问题,但更严重的问题是 供应链攻击,其中敌对实体通过依赖无意中注入的代码来危害公司。

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

重新审视依赖

我不是唯一思考我们依赖文化的人。这里有一些其他更聪明的人对此的看法:

Armin RonacherFlask 的创建者,最近在 老推特 上说:

我构建软件越多,就越讨厌依赖。我非常希望人们将东西复制/粘贴到自己的代码库中,或重新实现它。不幸的是,现在的氛围并不太拥抱这个想法。我需要那种氛围转变。

他还写了一篇关于他在 Rust 生态系统中 包管理经验 的精彩博文:

是时候有新视角了:我们应该赞扬那些自己编写小函数的工程师,而不是钩入一个传递的 crate 网络。我们应该对大的 crate 图感到怀疑。值得庆祝的是最小依赖、安静地完成工作的谦逊函数、因为一次做对而多年无需触碰的代码。

请完整阅读它。

早在 2021 年,Tom MacwrightVendor by default 中写道:

但我认为有点不寻常的一件事是:我 vendoring 了很多东西。

Vendoring,在编程意义上,意味着“将另一个项目的源代码复制到你的项目中。”它与使用依赖的实践相反,后者是将另一个项目的名称添加到你的 package.json 文件中,并让 npm 或 yarn 为你下载和链接它。

我也强烈推荐阅读他对 vendoring 的看法。

设计为可 Vendoring 的软件

如果你是开源开发者并喜欢 vendoring 的想法,有个好消息:有一个简单的方法使你的软件对供应商友好:尽可能移除依赖。

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

还有一组与 htmx 相关的项目认真对待 vendoring:

这些 JavaScript 项目都没有在 NPM 中可用,它们都 推荐 vendoring软件 到你自己的项目中作为主要安装机制。

以供应商为先的依赖管理器?

我想简要提到的最后一件事是一种结合 vendoring 和依赖管理的技术的:以供应商为先的依赖管理器。我以前从未使用过,但有人向我推荐了 vend,一个面向 Common Lisp 的供应商导向包管理器(有一个很棒的 README),以及 Go 的 vendoring 选项

在写这篇文章时,我也发现了 vendorpullgit-vendor,两者都是小型但有趣的项目。

这些看起来都是优秀的工具,在我看来,它们中的一些(以及类似工具)有机会添加额外功能来解决 vendoring 的传统弱点,例如:

有了这些额外功能,我怀疑以供应商为先的依赖管理器能否在现代软件开发中与“正常”依赖管理器竞争,或许结合两种方法的某些优势。

无论如何,我希望这篇文章能帮助你更多地思考依赖,并或许植入这样一个想法:也许你的软件可以少一些,嗯,对依赖的依赖。

</>