一些 htmx 贡献者反复提出的问题是,为什么 htmx 不是用 TypeScript 编写的,或者更广泛地说,为什么 htmx 完全没有构建步骤。完整的 htmx 源代码是一个单一的 3,500 行 JavaScript 文件;如果你想为 htmx 贡献代码,你需要修改 htmx.js 文件,这个文件就是生产环境中发送到浏览器的同一个文件(除了最小化和压缩)。
我不代表 htmx 项目发言,但我已经为它做出了几个非琐碎的贡献,并且每次这个问题出现时,我都积极倡导保留这种无构建设置。从我的角度来看,这就是为什么 htmx 没有构建步骤的原因。
编写库的最佳理由是用纯 JavaScript,因为它可以永久存在。这可以说是 JavaScript 最被低估的特性之一。虽然我相信有一些边缘情况,但 1999 年的 JavaScript 在 Netscape Navigator 中运行的代码,可以在昨天下载的 Google Chrome 中与现代代码一起运行而无需修改。这在非常少的编程环境中是真实的。对于 Python、Java 或 C 等语言来说,这当然不是真的,它们都有版本机制,选择新的语言特性会迫使你放弃已弃用的 API。
当然,大多数人对 JavaScript 的体验是它像牛奶一样会变质。重新打开一个 node 仓库 3 个月后,你会发现你的项目陷入一堆安全警告、向后不兼容的库“升级”和前端框架的文化高峰——那个高峰正好是你开始项目的那一刻,现在被广泛视为技术债。谁该为此负责由别人决定,但无论如何,你可以通过不使用 JavaScript 运行时之外的任何依赖来消除整个问题类。
如今编写 JavaScript 的流行方式是从 TypeScript 编译它(我将经常用它作为例子,因为 TypeScript 可能是使用构建系统的最佳理由)。TypeScript 无法在 Web 浏览器中本地运行,因此 TypeScript 代码不受 ECMA’s 对向后兼容性的狂热追求保护。像任何依赖一样,新的主要 TypeScript 版本不保证与前一个版本向后兼容。它们可能兼容!但如果不兼容,你就需要进行维护才能使用现代开发工具链。
维护是用劳动力支付的成本,而开源代码库是最负担不起这种成本的项目。选择不使用构建步骤会极大地最小化保持 htmx 更新的劳动力需求。这种体验已在 intercooler.js 中得到验证,它是 htmx 的前身,可以无限期维护(据我了解)只需很少的努力。当 htmx 1.0 发布时,TypeScript 是 4.1 版本;当 intercooler.js 发布时,TypeScript 还是 pre-1.0 版本。用那些 TypeScript 版本编写的代码能在今天的 TypeScript 编译器(写作时为 5.1 版本)中不经修改编译吗?可能,也可能不。
但 htmx 是用 JavaScript 编写的,没有依赖,因此只要 Web 浏览器保持相关性,它就会不经修改运行。让浏览器供应商为你做这项艰苦的工作吧。
TypeScript 的开发者体验(DX)在许多方面确实优于 JavaScript 的开发者体验。但 TypeScript 的 DX 在每个方面都更好,这不是真的,软件工程师倾向于将进步视为能力的目的论而不是带有权衡的选项,这有时会让他们忽略为他们喜欢的 DX 方面支付的成本。例如,使用 TypeScript 的一个小权衡是编译它需要时间,你必须等待它重新编译来测试更改。通常这个成本微不足道,而且值得支付,但它仍然是一个成本。
使用 TypeScript 的一个更显著成本是,在浏览器中运行的代码不是你编写的代码,这使得浏览器的开发者工具更难使用。当你的 TypeScript 代码抛出异常时,你必须弄清楚堆栈跟踪(带有其 JavaScript 行号、JavaScript 函数签名等)如何映射到你编写的 TypeScript 代码;当你的 JavaScript 代码抛出异常时,你可以直接点击到源代码,阅读你编写的代码,并在调试器中设置断点。这是一个巨大的 DX。对于许多从未这样工作的年轻 Web 开发者来说,这可能是一种启发性的体验。
构建步骤倡导者指出,TypeScript 可以生成 source maps,这些地图告诉浏览器哪个 TypeScript 对应哪个 JavaScript,这是真的!但现在你有另一个东西需要跟踪——你编写的 TypeScript、它生成的 JavaScript,以及连接这两者的源映射。你现在依赖的热重载开发服务器会在 localhost 上为你保持这些更新——但你的 staging 服务器呢?生产环境呢?在这些环境中出现的 bug 会更难追踪,因为你丢失了很多关于它们来源的信息。这些是可解决的问题,但它们是你创建的问题;它们是一个成本。
htmx 的 DX 非常简单——你的浏览器加载一个单一文件,在每个环境中都是你编写的完全相同的文件。维护这种体验所需的权衡是真实的,但对于这个项目来说,它们是合理的权衡。
模块化是软件的伟大想法之一。模块使分解代码成解决较小问题的良好封装子结构成为可能,从而解决极其复杂的问题。模块真的很有用。
然而,有时你想解决简单问题,或者至少是相对简单的问题。在这些情况下,不使用更复杂软件的构建块可能会有帮助,以免在不创造相应价值的情况下模仿它们的复杂性。在其核心,htmx 解决了一个相对简单的问题:它向 HTML 添加了少量属性,使使用超文本的声明性特性更容易替换 DOM 元素。要求 htmx 保持在单一文件中(再次,大约 3,500 行代码)会强制库具有一定程度的意图;在处理 htmx 源代码时,添加新代码的压力是真实的,这种压力维持了相对简单性的平衡。
虽然 DX 成本显而易见,但也有令人惊讶的 DX 好处。如果你搜索源文件中的函数名,你会立即找到该函数的每个调用(这也缓解了对更高级代码内省的需求)。功能没有隐藏的地方使得处理 htmx 变得更容易接近。远更复杂的项目也使用这种方法的一些方面:SQLite3 从单一文件源代码合并编译(尽管他们为开发使用单独文件,他们不是疯了),这使得黑客操作它显著更容易。你永远无法用这种方式构建 linux 内核——但 htmx 不是 linux 内核。
像任何技术决策一样,选择放弃构建步骤有优势和劣势。承认这些权衡很重要,这样你才能做出明智的决定,并在一些好处或成本不再适用时重新审视该决定。考虑到编写纯 JavaScript 的优势,让我们考虑它引入的一些痛点。
TypeScript 是 JavaScript 的严格超集,它添加的一些特性非常有用。TypeScript 有… 类型,这使得你的 IDE 更好地建议代码 并指出你可能错误使用方法的地方。用于自动重命名和重构代码的工具对于 TypeScript 来说比 JavaScript 可靠得多。不过,htmx 代码必须用 JavaScript 编写,因为浏览器运行 JavaScript。而且只要 JavaScript 是动态类型的,在 htmx 源代码中获得真正静态类型所需的权衡就不值得(htmx 用户 仍然可以使用用 .d.ts 文件声明的类型化 API)。
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。
IE11 支持将在 htmx 2.0 中被放弃,那时源代码将允许使用 ES6。
这一点显而易见,但值得重述:如果 htmx 源代码可以拆分成模块,它会整洁得多。除了整洁之外,还有其他因素影响代码质量除了整洁,但在 htmx 源代码高质量的程度上,并不是因为它整洁。
这使得使用 htmx 做某些事情变得非常困难。idiomorph 算法 可能被包含在 htmx 2.0 核心中,但它也作为单独的包维护,以便人们可以在不使用 htmx 的情况下使用 DOM 变形算法。如果核心可以包含多个文件,你可以用任何数量的镜像方案(如 git 子模块)轻松实现这一点。但核心是一个单一文件,因此 idiomorph 代码也必须放在那里。
这篇文章的标题可能更适合叫“为什么 htmx 现在 没有构建步骤”。如前所述,情况会变化,这些权衡可以随时重新审视!我们目前正在探索的一个问题是关于发布。当 htmx 进行发布时,它使用几个不同的 shell 命令来填充 dist 目录,其中包含 htmx.js 的最小化和压缩版本( pedants 欢迎指出这显然在某种意义上是一个构建步骤)。未来,我们可能会扩展该脚本以自动生成 Universal Module Definition。或者我们可能有新的分发需求,需要更复杂的设置。谁知道呢!
htmx 的核心价值之一是,在过去十年主导的日益复杂的 JavaScript 技术栈主导的 Web 开发生态系统中,它赋予你选择。一旦你不再有一个巨大的前端 JavaScript 代码库,对在后端采用 JavaScript 的压力就会小得多。你可以用 Python、Go,甚至 Node.js 编写后端,对 htmx 来说无关紧要——每种主流语言都有成熟的 HTML 格式化解决方案。这就是 Hypermedia On Whatever you’d Like (HOWL) 的原则。
在没有构建过程的情况下编写 JavaScript 是,一旦你不再需要 Next.js 或 SvelteKit 来管理 SPA 框架的螺旋复杂性,你可用的选项之一。对于今天的 htmx 开发来说,这种选择有意义,对于你的应用来说,可能有意义,也可能没有。