为什么 htmx 没有构建步骤

亚历山大·佩特罗斯

一些 htmx 贡献者经常问的一个问题是,为什么 htmx 不是用 TypeScript 编写的,或者更确切地说,为什么 htmx 完全没有构建步骤。htmx 的完整源代码是一个包含 3500 行的 JavaScript 文件;如果你想为 htmx 做贡献,你所要做的就是修改 htmx.js 文件,这个文件与生产环境中发送到浏览器的文件相同,只不过经过了缩小和压缩。

我并不代表 htmx 项目发言,但我对它做了一些非微不足道的贡献,并且每次出现这个问题时,我都一直是保留这种无构建设置的强烈支持者。从我的角度来看,htmx 没有构建步骤的原因如下。

#一次编写,永远运行

用纯 JavaScript 编写库的最佳理由是它能永远运行。这可以说是 JavaScript 最被低估的特性。虽然我相信有一些特殊情况,但 1999 年在 Netscape Navigator 中运行的 JavaScript 代码可以不变地运行,与昨天下载的 Google Chrome 中的现代代码并行运行。这对极少数的编程环境来说是正确的。它当然不适用于 Python、Java 或 C,这些语言都有版本控制机制,选择新的语言特性会迫使你放弃过时的 API。

当然,大多数人对 JavaScript 的体验是它像牛奶一样变质。3 个月后重新打开一个 node 仓库,你会发现你的项目陷入了安全警告、向后不兼容的库“升级”以及一个前端框架的泥潭,而这个框架的文化巅峰是你开始项目的那一刻,现在却被广泛认为是技术债务。谁应该为这种情况负责,由其他人来决定,但无论如何,你可以通过不依赖于 JavaScript 运行时以外的任何东西来消除整个问题类别。

如今,编写 JavaScript 的一个流行方法是从 TypeScript 编译它(我将经常使用它作为例子,因为 TypeScript 可能是 使用构建系统的最佳理由)。TypeScript 无法在 Web 浏览器中本地运行,因此 TypeScript 代码不受 ECMA 对向后兼容性的狂热奉献的保护。与任何依赖项一样,新的主要 TypeScript 版本不保证与之前的版本向后兼容。它们可能是!但如果不是,那么如果你想使用现代开发工具链,你就需要进行维护。

维护需要用劳动力来支付,而开源代码库是最负担不起维护费用的项目。选择不使用构建步骤极大地减少了维护 htmx 最新状态所需的劳动力。intercooler.js(htmx 的前身)已经证实了这种体验,据我所知,它一直被维护,而且所需的努力很少。当 htmx 1.0 发布时,TypeScript 的版本是 4.1;当 intercooler.js 发布时,TypeScript 的版本是 1.0 之前的版本。在今天的 TypeScript 编译器(在撰写本文时,版本为 5.1)中,用这些 TypeScript 版本编写的代码是否可以不变地编译?也许,也许不。

但 htmx是用 JavaScript 编写的,没有任何依赖关系,因此只要 Web 浏览器仍然相关,它就可以不变地运行。让浏览器供应商为你做艰苦的工作。

#开发者体验

确实,在很多方面,TypeScript 开发者体验(DX)比 JavaScript 开发者体验更好。但 TypeScript DX 并非在所有方面都更好,软件工程师往往将进步视为能力的必然趋势,而不是权衡取舍,这有时会让他们看不到他们喜欢的 DX 方面的成本。例如,使用 TypeScript 所做的一个小的权衡是,编译它需要时间,而且你必须等待它重新编译才能测试更改。通常情况下,这种成本是可以忽略不计的,而且值得付出,但它毕竟是一种成本。

使用 TypeScript 的一个更重要的成本是,在浏览器中运行的代码不是你编写的代码,这使得浏览器的开发工具更难使用。当你的 TypeScript 代码抛出异常时,你必须弄清楚堆栈跟踪(及其 JavaScript 行号、JavaScript 函数签名等)如何映射到你的 TypeScript 代码;当你的 JavaScript 代码抛出异常时,你可以直接点击源代码,阅读你编写的代码,并在调试器中设置断点。这非常棒的 DX。对于许多从未以这种方式工作的年轻 Web 开发人员来说,这可能是一种启示性的体验。

构建步骤的支持者指出,TypeScript 可以生成 源映射,告诉你的浏览器什么 TypeScript 对应什么 JavaScript,这是真的!但现在你需要另外跟踪一个东西——你编写的 TypeScript、它生成的 JavaScript 以及连接这两者的源映射。你正在使用的热重载开发服务器将在 localhost 上为你更新这些文件——但你的登台服务器呢?生产环境呢?这些环境中出现的错误将更难追踪,因为你已经丢失了很多关于它们来自哪里的信息。这些问题是可以解决的,但它们是你自己创造的;它们是一种成本。

htmx 的 DX 非常简单——你的浏览器加载一个单一文件,该文件在所有环境中都是你编写的完全相同的文件。维护这种体验所需的权衡是真实的,但对于这个项目来说,这些权衡是有意义的。

#强制清晰度

模块化是软件的 很棒的想法 之一。模块使解决极其复杂的问题成为可能,方法是将代码分解成解决较小问题的良好封装的子结构。模块非常有用。

然而,有时你想要解决简单的问题,或者至少是相对简单的问题。在这些情况下,使用更复杂软件的构建块可能会有所帮助,以免你在没有创造相称的价值的情况下模拟它们的复杂性。htmx 的核心解决了一个相对简单的问题:它为 HTML 添加了一些属性,使使用超文本的声明性特征更容易地替换 DOM 元素。要求 htmx 保持在一个文件中(再次强调,大约 3500 行 LOC)在库上施加了一定程度的意图;在处理 htmx 源代码时,确实存在着为添加新代码辩护的压力,这种压力维持着相对简洁的平衡。

虽然 DX 成本是显而易见的,但也有一些令人惊讶的 DX 好处。如果你在源文件中搜索一个函数名,你将立即找到所有调用该函数的地方(这也减轻了对更高级代码自省的需求)。缺乏隐藏功能的地方使处理 htmx 变得更加容易。远比 htmx 复杂得多的项目也使用这种方法的一些方面:SQLite3 从 单文件源代码合并 编译(虽然他们在开发中使用单独的文件,但他们并不疯狂),这使得对它的 hack 变得容易得多。你永远不可能用这种方式构建 Linux 内核——但 htmx 不是 Linux 内核。

#成本

与任何技术决策一样,选择放弃构建步骤都有利有弊。重要的是要认识到这些权衡,以便你能做出明智的决定,并在某些好处或成本不再适用时重新考虑该决定。考虑到编写纯 JavaScript 的优势,让我们考虑一下它带来的某些痛点。

#没有静态类型

TypeScript 是 JavaScript 的严格超集,它添加的一些特性非常有用。TypeScript 具有...类型,这使得你的 IDE 能够更好地建议代码,并指出你可能在使用方法时出错的地方。自动重命名和重构代码的工具对于 TypeScript 来说比 JavaScript 更可靠。htmx 代码必须用 JavaScript 编写,因为浏览器运行 JavaScript。而且,只要 JavaScript 是动态类型的,在 htmx 源代码中获得真正静态类型的权衡就不值得(htmx 用户仍然可以利用类型化 API,用 .d.ts 文件声明)。

htmx 的未来版本可能会使用 JSDoc 来获得一些相同的保证,而无需构建步骤。其他库,如 Svelte,也一直在朝着这个方向发展,部分原因是 TypeScript 文件带来的调试摩擦

#没有 ES6

由于 htmx 保持对 Internet Explorer 11 的支持,并且因为它没有构建步骤,因此 htmx 的每一行代码都必须用 IE11 兼容的 JavaScript 编写,这意味着没有 ES6。当像我这样的人说 JavaScript 现在已经相当好时,他们通常指的是 ES6 中引入的语言特性,例如 async/await、匿名函数和函数式数组方法(即 .map.forEach)——这些在 htmx 源代码中都不能使用。

虽然这非常令人讨厌,但在实践中这并不是一个很大的障碍。缺乏一些不错的语言特性并不妨碍你用函数式范式编写代码。没有编写一个自定义的 forEach 方法 会很愉快吗?当然。但直到 htmx 目标的所有浏览器都支持 ES6,用几个辅助函数来补充 ES5 并不难。如果你习惯了 ES6,你将自动编写更好的 ES5。

htmx 2.0 将放弃对 IE11 的支持,届时 ES6 将被允许在源代码中使用。

#核心代码中没有模块

这一点是显而易见的,但值得再次说明:如果 htmx 源代码可以拆分成多个模块,它会更整洁。 除了整洁之外,还有其他因素会影响代码质量,但就 htmx 源代码的质量而言,它并不是因为它很整洁。

这使得用 htmx 做某些事情变得非常困难。idiomorph 算法可能会包含在 htmx 2.0 的核心代码中,但它也作为一个单独的包进行维护,这样人们就可以在不使用 htmx 的情况下使用 DOM 变形算法。如果核心代码可以包含多个文件,就可以很容易地用任何数量的镜像方案(如 git 子模块)来完成这项工作。但核心代码是一个单一文件,因此 idiomorph 代码也必须放在那里。

#最后的想法

这篇论文的标题或许可以改为“为什么 htmx 现在没有构建步骤”。如前所述,情况会发生变化,这些权衡可以在任何时候重新考虑!我们目前正在探索的一个问题与发布有关。当 htmx 发布版本时,它使用一些不同的 shell 命令来填充 dist 目录,其中包含 htmx.js 的缩小和压缩版本(吹毛求疵的人可以指出,这显然在某种意义上是一个构建步骤)。在未来,我们可能会扩展该脚本来自动生成 通用模块定义。或者我们可能会有新的分发需求,这需要更复杂的设置。谁知道呢!

htmx 的核心价值之一是它在过去十年被日益复杂的 JavaScript 技术栈主导的 Web 开发生态系统中赋予你选择的权利。当你不再拥有庞大的前端 JavaScript 代码库时,在后端采用 JavaScript 的压力就会大大降低。你可以使用 Python、Go,甚至 NodeJS 编写后端,这对 htmx 来说无关紧要——每种主流语言都有成熟的 HTML 格式化解决方案。这就是 无论你想要什么,超媒体就在那里 (HOWL) 的原则。

当你不再需要 NextJS 或 SvelteKit 来管理单页应用程序框架的复杂性时,编写无构建过程的 JavaScript 就是你的一种选择。这种选择对于当今的 htmx 开发来说是合理的,对于你的应用程序来说可能也有道理,也可能没有道理。

</>