一些 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 文件带来的调试摩擦。
由于 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 开发来说是合理的,对于你的应用程序来说可能也有道理,也可能没有道理。