一些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的体验是它像牛奶一样容易变质。三个月后重新打开一个Node仓库,你会发现项目陷入一堆安全警告、向后不兼容的库“升级”以及一个文化巅峰恰好在你启动项目时的前端框架中——而现在它被广泛视为技术债务。这种情况该怪谁让别人去决定,但无论如何,只要除了JavaScript运行时外没有其他依赖,你就能完全避免这类问题。
如今编写JavaScript的一种流行方式是从TypeScript编译而来(我将频繁使用TypeScript作为例子,因为TypeScript可能是使用构建系统的最佳理由)。TypeScript无法在浏览器中原生运行,因此TypeScript代码不受ECMA对向后兼容的狂热执着保护。像任何依赖一样,新的TypeScript主版本不保证与之前版本向后兼容。可能兼容!但如果不兼容,那么想要使用现代开发工具链就需要进行维护。
维护是需要用劳动力支付的成本,而开源代码库是最无力承担这种成本的项目。选择不使用构建步骤大大减少了保持htmx最新所需的工作量。这一经验已被intercooler.js(htmx的前身)所证实,据我所知它只需很少努力就能无限期维护。当htmx 1.0发布时,TypeScript是4.1版;当intercooler.js发布时,TypeScript还是1.0之前版本。用那些TypeScript版本编写的代码能在今天的TypeScript编译器(本文撰写时为5.1版)中不经修改地编译吗?也许能,也许不能。
但htmx是用JavaScript编写的,没有依赖项,因此只要浏览器仍然相关,它就能不经修改地运行。让浏览器厂商替你完成艰苦的工作。
确实,TypeScript的开发者体验(DX)在许多方面优于JavaScript。但并非TypeScript的DX在所有方面都更好,软件工程师倾向于将进步视为能力的终极目标而非有取舍的选择,这有时会让他们对所喜欢DX方面付出的代价视而不见。例如,使用TypeScript的一个小代价是编译需要时间,你必须等待它重新编译才能测试更改。通常这个代价可以忽略不计且值得付出,但它仍然是个代价。
使用TypeScript更显著的代价是浏览器中运行的代码不是你编写的代码,这使得浏览器的开发者工具更难使用。当你的TypeScript代码抛出异常时,你必须弄清楚堆栈跟踪(包含JavaScript行号、函数签名等)如何映射到你编写的TypeScript代码;当你的JavaScript代码抛出异常时,你可以直接点击跳转到源代码,阅读你写的内容,并在调试器中设置断点。这是巨大的DX优势。对于许多从未以这种方式工作的年轻Web开发者来说,这可能是一次启示性的体验。
构建步骤倡导者指出TypeScript可以生成源映射,它能告诉浏览器哪些TypeScript对应哪些JavaScript,这没错!但现在你有了另一个需要跟踪的东西——你编写的TypeScript、它生成的JavaScript,以及连接这两者的源映射。你依赖的热重载开发服务器会在localhost上为你保持这些更新——但你的预发布环境呢?生产环境呢?在这些环境中出现的bug将更难追踪,因为你丢失了大量关于它们来源的信息。这些都是可解决的问题,但都是你自己创造的问题;它们是代价。
htmx的DX非常简单——你的浏览器加载一个文件,在所有环境中这个文件都是你编写的完全相同的文件。维持这种体验所需的权衡是真实的,但对这个项目来说是有意义的权衡。
模块化是软件的伟大理念之一。模块通过将代码分解为解决较小问题的良好封装的子结构,使得解决极其复杂的问题成为可能。模块非常有用。
然而,有时你想解决简单的问题,或至少是相对简单的问题。在这些情况下,不使用更复杂软件的构建块可能更有帮助,以免在没有创造相应价值的情况下模仿其复杂性。htmx的核心解决了一个相对简单的问题:它向HTML添加少量属性,使其更容易利用超文本的声明特性替换DOM元素。要求htmx保持为单个文件(约3500行代码)强制了库的某种意图;在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命令来用htmx.js
的压缩和压缩版本填充dist
目录(学究们可以指出这在某种意义上显然是构建步骤)。将来,我们可能会扩展该脚本以自动生成通用模块定义。或者我们可能有新的分发需求需要更复杂的设置。谁知道呢!
htmx的核心价值之一是它在过去十年被日益复杂的JavaScript栈主导的Web开发生态系统中给你选择。一旦你不再拥有庞大的前端JavaScript代码库,在后台采用JavaScript的压力就小得多。你可以用Python、Go甚至Node.js编写后端,这对htmx无关紧要——每个主流语言都有成熟的HTML格式化解决方案。这就是超媒体随心(HOWL)原则。
编写没有构建过程的JavaScript是你不再需要Next.js或SvelteKit管理SPA框架螺旋式复杂性时可用的选项之一。这个选择对今天的htmx开发有意义,对你的应用也可能有意义。