前言
Bundle or Bundleless?自 2015 年 ESM 标准发布后,路线之争就开始逐步升温。转眼间,时间已来到 2021 年。如果白酒的车你错过了,那么不妨看看 Bundleless,或许它就是前端圈的下一位「茅台」。
前端构建当下的问题
不得不说,曾经把自己定位为「打包器」的 Webpack,如今已形成强大的构建生态,俨然一统江湖。但前端构建的道路还远没有走到最后。随着业务的发展,前端工程的复杂度越来越高,构建方面的也开始暴露出新的问题。
构建时间逐步拉长
相信许多前端同学刚入行时,都经历过「刷新一下全都有」的幸福时光:写几个 HTML 标签,写几句内嵌代码,浏览器中就会呈现出美妙的 UI。而如今,业务工程越来越复杂,代码量连年增长,构建的时间也越来越长。曾经「秒级构建」的前端,终究跻身「分钟级构建」的圈子了。
前端工程构建时间的拉长,自然使得前端开发者在日常业务工作中的状态,从图左逐渐运动到了图右。
模块标准引领方向
如果我们纵观前端领域的发展,就可以看到标准是如何推动各大浏览器建设,整个前端生态又是如何发生的变化。
2002 年,AJAX 推出,此后前端承担的工作越来越多。彼时,浏览器厂各行其是,因此兼容性是当时的主要问题。于是 2006 年,jQuery 的出现进一步带动了前端的发展。
2009 - 2011 年,CommonJS、AMD、UMD 相继为 JS 带来了模块规范。同一时期,部分遵循 CommonJS 的 Node 为 JS 带来了运行环境,为前端工程化的解锁奠定基础:
- 模块加载工具开始涌现,如 RequireJS、SeaJS 等
- 包管理工具,如 npm、spm 等
- 轻量的打包器开始出现,如 Browserify
- 任务工具开始出现,如 Gulp
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 为什么是答案
Bundleless 说到底,就是指无打包构建,与我们当下流行的打包构建相对,而打包器则是我们前端开发者用于将 JS 模块打包成单一的、可在浏览器内运行的文件的工具。
为什么过去需要打包
这一问题在社区也有非常多的总结,概况来讲,主要包括以下理由:
- HTTP/1.1 各浏览器有并行连接限制
- 浏览器不支持模块系统(如 CommonJS 包不能直接在浏览器运行)
- 代码依赖关系与顺序管理
HTTP/1.1 各浏览器默认并行连接数
并行连接 | 6 | 6 | 6 | 2 | 8 | 6 | 6 |
为什么开始尝试不打包
近几年时间,标准的确立、浏览器大厂和前端生态的跟进,使得「不打包」成为可能:
- HTTP/2.0 多路并用
- 各大浏览器逐一支持 ESM
- 越来越多的 npm 包拥抱 ESM(尽管很多包的依赖并不是)
我们可以发现:
- 通过打包来减少网络请求数量从而提高性能的优化手段理论上在 HTTP/2.0 下会变得不再必要;
ESM 标准的推广和各大浏览器的支持:
- 让模块代码可以直接在浏览器中运行
- 原生的解决了代码依赖和复用的问题
- 会进一步推动越来越多的 npm 包支持 ESM,甚至会出现新的包管理或分发方式
Bundleless vs Bundle
模块加载的对比
如果是打包式构建,在模块加载时,实际上加载的是若干模块的集合。这种方式的优点是以少量的请求连接数完成 JS 脚本的下载。如果是无打包式构建,模块的加载则是基于原生模块方案,直接获取具体的模块脚本。
本地开发构建的对比
如果是打包式构建,无论是项目启动还是文件变更,都需要完整的走一遍打包过程。以 Webpack 为例,我们就会经历依赖分析、代码转译和打包的过程,哪怕我们只是简单的修改了一行文案。当然,Split chunk 会在一定程度上缓解这一问题,但粒度仍然偏大。
而无打包式构建,在启动过程中基本只是启动服务(当然不同的 Bundleless 方案可能还会做些其他的工作),而不用对业务代码进行依赖分析、打包,ESM 会帮助我们在浏览器中完成依赖的分析。当文件发生变更时,本地开发服务只是提供了文件的映射,只需要重新转译对应的文件,并重新替换即可。
结论
以上,我们可以知道:
- 打包过程的必要性已降低
- 拥抱 ESM 是未来趋势
社区在领域内的工作
概览
前端构建并不只是构建工具的问题。事实上,「构建」和「分发」共同组成了前端工程的构建,只不过通常情况下,我们是通过 npm install
将三方包下载下来,并打包到构建结果中实现的。
构建可以分为两种类型。
一种是基于服务的构建方式,通常服务于实际生产。我们可以再细分成本地服务构建和远端服务构建。本地服务构建就是我们常规的操作,目前基本已经被 Webpack 统治,是 Bundle 方案的代表;Snowpack、Vite、Web Dev Server 则是目前非常火的 Bundleless 方案,近一年的时间里势头迅猛。远端服务构建则是依托云能力的玩法,把构建过程放在服务端完成,从而把本地的开发流程搬到 Web 上,并给出于本地服务构建基本一致的体验。
另一种是基于浏览器的构建方式,通常面向 Demo 的快速搭建或预览方案。Codesandbox、StackBlitz、CodePen 和 Riddle 是业内较出色的方案,整体是在浏览器端实现代码的编译、打包、构建和运行。当然,具体到各个方案的细节,通常对服务还是有一定依赖。
目前来看,100% 在浏览器端进行打包、构建,现阶段并不是最理想的方案。随着 Bundleless 的发展,浏览器 Bundleless 和包分发平台的结合会得到进一步的发展,并逐步影响前端工程的架构。
🗻Snowpack
原理
这一部分在 Snowpack 的文档上有一定的讲解。整体来说,Snowpack 尽可能利用了现有前端生态的工具,对三方包打包来压缩依赖链,对业务代码走无构建路线,以此提供 Bundleless 体系下的开发体验。值得一提的是,Snowpack 的构建速度很快,这得益于内置打包工具 esbuild 的发展。
Snowpack vs Webpack
我们不妨将其与 Webpack 进行一个对比。
启动时间,如上文所说,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
Vite 是尤大的力作,本篇便不再对其进行讨论。有趣的是,他和 Webpack 作者 Sean 在推上的讨论可以看出,大佬们也在 Bundleless 方向不断发力,Webpack 成为了大家发起挑战的目标。
总结
本文以 Bundleless 为切入点,结合前端构建的发展过程,对当下无构建方案进行了讨论。未来我们会更多的在此方面进行实践。