作者:ES2049 Studio
Bundle or Bundleless?自 2015 年 ESM 标准发布后,路线之争就开始逐步升温。转眼间,时间已来到 2021 年。如果白酒的车你错过了,那么不妨看看 Bundleless,或许它就是前端圈的下一位「茅台」。
不得不说,曾经把自己定位为「打包器」的 Webpack,如今已形成强大的构建生态,俨然一统江湖。但前端构建的道路还远没有走到最后。随着业务的发展,前端工程的复杂度越来越高,构建方面的也开始暴露出新的问题。
相信许多前端同学刚入行时,都经历过「刷新一下全都有」的幸福时光:写几个 HTML 标签,写几句内嵌代码,浏览器中就会呈现出美妙的 UI。而如今,业务工程越来越复杂,代码量连年增长,构建的时间也越来越长。曾经「秒级构建」的前端,终究跻身「分钟级构建」的圈子了。
img
前端工程构建时间的拉长,自然使得前端开发者在日常业务工作中的状态,从图左逐渐运动到了图右。
img
如果我们纵观前端领域的发展,就可以看到标准是如何推动各大浏览器建设,整个前端生态又是如何发生的变化。
img
2002 年,AJAX 推出,此后前端承担的工作越来越多。彼时,浏览器厂各行其是,因此兼容性是当时的主要问题。于是 2006 年,jQuery 的出现进一步带动了前端的发展。
2009 - 2011 年,CommonJS、AMD、UMD 相继为 JS 带来了模块规范。同一时期,部分遵循 CommonJS 的 Node 为 JS 带来了运行环境,为前端工程化的解锁奠定基础:
Angular、React、Vue 等的相继火爆,也推动了前端的又一波浪潮:它们的发展提高了前端在业务中的表达能力,并向更高程度的工程化提出诉求。
2015 年,HTTP/2.0 推出,同时 JS 迎来了自己的模块标准 ESM:ES2015 一发布,Babel 就让开发者们用上了 ES Module,真香。于是此后的几年,Webpack & Babel 几乎成了前端工程化的代名词,甚至让人以为,前端工程化已成定局。
2018 年,Chrome、Safari、Firefox 相继完成了对 ESM 的支持。但得益于 Webpack 生态对 CommonJS、AMD、UMD 的支持,开发者们对 ESM 的享用更多是在编码阶段和一定程度的 Tree-shaking,在构建层面并没有直接的得利。
当下时间点,出现了新的契机:其一,「工程体积的日益增长」与「亟待提升的构建性能」之间的矛盾;其二,「先进的前端模块标准」与「落后的前端模块规范」之间的矛盾。
Bundleless 说到底,就是指无打包构建,与我们当下流行的打包构建相对,而打包器则是我们前端开发者用于将 JS 模块打包成单一的、可在浏览器内运行的文件的工具。
这一问题在社区也有非常多的总结,概况来讲,主要包括以下理由:
HTTP/1.1 各浏览器默认并行连接数
浏览器 | Firefox 3+ | Opera 12 | Safari 5 | IE 7 | IE 10 | Edge | Chrome |
---|---|---|---|---|---|---|---|
并行连接 | 6 | 6 | 6 | 2 | 8 | 6 | 6 |
近几年时间,标准的确立、浏览器大厂和前端生态的跟进,使得「不打包」成为可能:
我们可以发现:
如果是打包式构建,在模块加载时,实际上加载的是若干模块的集合。这种方式的优点是以少量的请求连接数完成 JS 脚本的下载。如果是无打包式构建,模块的加载则是基于原生模块方案,直接获取具体的模块脚本。
img
如果是打包式构建,无论是项目启动还是文件变更,都需要完整的走一遍打包过程。以 Webpack 为例,我们就会经历依赖分析、代码转译和打包的过程,哪怕我们只是简单的修改了一行文案。当然,Split chunk 会在一定程度上缓解这一问题,但粒度仍然偏大。
img
而无打包式构建,在启动过程中基本只是启动服务(当然不同的 Bundleless 方案可能还会做些其他的工作),而不用对业务代码进行依赖分析、打包,ESM 会帮助我们在浏览器中完成依赖的分析。当文件发生变更时,本地开发服务只是提供了文件的映射,只需要重新转译对应的文件,并重新替换即可。
以上,我们可以知道:
前端构建并不只是构建工具的问题。事实上,「构建」和「分发」共同组成了前端工程的构建,只不过通常情况下,我们是通过 npm install
将三方包下载下来,并打包到构建结果中实现的。
构建可以分为两种类型。
一种是基于服务的构建方式,通常服务于实际生产。我们可以再细分成本地服务构建和远端服务构建。本地服务构建就是我们常规的操作,目前基本已经被 Webpack 统治,是 Bundle 方案的代表;Snowpack、Vite、Web Dev Server 则是目前非常火的 Bundleless 方案,近一年的时间里势头迅猛。远端服务构建则是依托云能力的玩法,把构建过程放在服务端完成,从而把本地的开发流程搬到 Web 上,并给出于本地服务构建基本一致的体验。
另一种是基于浏览器的构建方式,通常面向 Demo 的快速搭建或预览方案。Codesandbox、StackBlitz、CodePen 和 Riddle 是业内较出色的方案,整体是在浏览器端实现代码的编译、打包、构建和运行。当然,具体到各个方案的细节,通常对服务还是有一定依赖。
img
目前来看,100% 在浏览器端进行打包、构建,现阶段并不是最理想的方案。随着 Bundleless 的发展,浏览器 Bundleless 和包分发平台的结合会得到进一步的发展,并逐步影响前端工程的架构。
这一部分在 Snowpack 的文档上有一定的讲解。整体来说,Snowpack 尽可能利用了现有前端生态的工具,对三方包打包来压缩依赖链,对业务代码走无构建路线,以此提供 Bundleless 体系下的开发体验。值得一提的是,Snowpack 的构建速度很快,这得益于内置打包工具 esbuild 的发展。
img
我们不妨将其与 Webpack 进行一个对比。
img
启动时间,如上文所说,Webpack 会完整打包整个项目,因此随着项目体积的增长,启动时间也会越发漫长;而 Snowpack 主要是启动本地的服务,对于 Snowpack 来说,尽管初次启动时会分析三方依赖,并通过 Rollup 将其进行打包,但是打包结果会缓存在 node_modules/.cache/snowpack/development 目录下,后续就可以享受到飞一样的启动。
构建时间,对于 Webpack 而言,构建时长会随着项目体积整体以线性方式增长;而 Snowpack 的模式则是 O(1) 的复杂度(当然这里也有点小噱头)。
缓存能力,可以说 Webpack 的缓存利用率尚有优化空间。尽管我们可以通过 Split Chunk,合理的划分打包方式,但如果我们只是改了一句文案,那么用户侧仍然会重新获取对应 Chunk 资源。Snowpack 的缓存利用率近乎完美。业务代码文件发生变更,直接替代产出资源,其他全部可返回缓存;三方依赖包,如 react.js,则直接更新该包,其他全部可用缓存。不过,尽管生产环境优化可以做 Tree-shaking,但是业务代码本身,Snowpack 并不会做处理(只是以 ESM 来对待),即使使用 Snowpack 生态的 Webpack 插件来做生产环境的构建也是如此,所以是近乎完美,这是相对于理想的 DCE(dead code elimination) 而言的。Webpack 在生产环境会把没有使用到的代码 Tree-shaking 掉,不可谓不强大。
至于调试体验,因为 Webpack 需要打包,因此在调试的时候我们依赖 SourceMap 来帮助我们看到源码。但对于 Snowpack 而言(实际上 Bundleless 模式都是如此),我们并不强依赖于 SourceMap,如果转译后的代码阅读无碍(ES6 其实还好嘛),就可以直接进行单文件调试。
不过即使 Snowpack 有千番好,整个 Bundleless 生态还不足以取代 Webpack。Webpack 终究是一代神器,只是我们明白 Bundleless 也确实代表了未来。
Vite 是尤大的力作,本篇便不再对其进行讨论。有趣的是,他和 Webpack 作者 Sean 在推上的讨论可以看出,大佬们也在 Bundleless 方向不断发力,Webpack 成为了大家发起挑战的目标。
img
本文以 Bundleless 为切入点,结合前端构建的发展过程,对当下无构建方案进行了讨论。未来我们会更多的在此方面进行实践。