这几年,前端领域内的构建工具层出不穷,除了 webpack,叫得上号的还有 esbuild、rollup、vite、snowpack、swc、parcle 等等。咋一看,不由得感叹一句“前端好卷啊~”。其实,细想来看,倒不是大家真的卷,而是前端还处在一个高速发展的过程中,很多方面还不成熟,因此会有很多的工具出现来解决不成熟的问题。而当规范、浏览器支持方面逐渐成熟,形成足够的基础能力以后,这些工具就会逐步退出历史舞台。

不管是老牌的 webpack,还是新晋的 esbuild、vite 等,如果我们把注意力都集中在这些工具提供的功能上面,只学习如何使用,那么当下一个构建工具出现的时候,又需要重头开始学习,难免有“学不动了”的感觉。

本文将会跳出这些工具的具体细节,尝试总结一条工程构建领域内具有共性的规律。在这条规律的指导下,我们再来观察构建工具的能力。

时代的大背景

用“时代”这个词,稍微显得有点大,但是当说到 JavaScript 模块化这个问题时,业内确实也是经过了很多年的努力,才明确了一个大方向,现在也基本得到了主流浏览器的支持。

众所周知,JavaScript 在诞生之初,是没有模块的概念的。早些年,我们还在写 jQuery 的时候,会格外注意每个 JS 文件在 HTML 中的顺序,被依赖的 JS 要在前面被加载。

随着前端的发展,Web 上的应用越来越复杂,JS 代码越来越多,单靠人来维护各个 JS 文件之间的依赖关系已经不可实现。于是在工程实践层面先后诞生了 AMD、CMD 等规范,对应的实现有 RequireJS 和 SeaJS。在 NodeJS 诞生后,在服务端领域,引入了 CommonJS 规范。为了能让 JS 模块既能在浏览器运行,又能在服务端运行,又诞生了综合 AMD 和 CommonJS 的 UMD 规范。这些年,经过大家的不懈努力,JS 的模块化规范 ES Modules 终于落地,并已经得到大部分主流浏览器的支持。

规范不同,语法格式也不同。不同格式的转换工作理所当然的落到了构建工具的身上。在现在的工程实践中,webpack 是集大成者,可以根据需要构建成各种模块格式,是当之无愧的业内主流。但当 ES Modules 被大部分主流浏览器支持以后,在新的基础能力之上成长起来的构建工具就可以丢弃沉重的历史包袱,轻装上阵,比如 vite、esbuild 都是基于 ES Modules 规范来构建。由于浏览器承担了一部分打包的工作,新的构建工具不再需要分析所有模块的代码且减少了很多对源代码的分析和转换,性能上提升了很多。

下面是 vite 官方给出的两张图,可以看出基于 ES Modules 构建和基于 Bundle 构建的差异。

基于 Bundle 的 dev server

基于 ES Modules 的 dev server

因此,从 JavaScript 模块化发展的角度来看,不同的构建工具,因为利用规范和浏览器提供的能力的不同,表现出各方面尤其是性能方面的差异,采用 ES Modules 的构建工具性能更加卓著。

前端构建的核心能力

webpack 是一款出色的构建工具,功能非常强大,我们可以通过配置处理各种场景下的构建问题。但是 webpack 的上手成本也很高,丰富的功能导致文档非常庞大,初学者很难看懂。很多人都是使用了 webpack 很久之后,才慢慢理解的 webpack。

其实对于工程构建而言,有三个核心能力,大部分功能配置都是围绕这三个核心能力来展开,分别是:打包器(Bundler)、转换器(Transformer)和压缩器(Minimizer)。

打包器

打包器将项目代码打包成一到多个 JS 文件,每一个文件为一个包(Bundle),里面包含当前模块和依赖的其他模块。比如在 webpack 的输出结果中,通常包含三个文件:runtime.js、manifest.js 和 main.js 文件。

随着 ES Modules 的发展,浏览器将会承担起越来越多的模块加载的工作,且效果也会越来越好,将来我们对打包的需求会越来越弱。现在 vite 在开发阶段,已经可以不再打包 JS 文件了,直接使用 ES 模块,参考上图。具体细节可以在 vite 官方文档open in new window中找到说明。

转换器

在前端生态中,典型的转换器场景就是 Babel,大家也都非常熟悉。Babel 将最新的 JS 语法转换成 es5 等浏览器支持的语法,让我们可以自由的使用最新的语法标准而不用担心浏览器兼容问题。

一个典型的转换器的工作过程包含三部分:

  • Parse(解析):将源代码转换成更加抽象的表示方法(抽象语法树 AST)
  • Transform(转换):对(抽象语法树)做一些特殊处理,让它符合编译器的期望
  • Generate(代码生成):将第二步经过转换过的(抽象语法树)生成新的代码

转换器的核心是抽象语法树 AST,AST 的应用非常广泛,比如:

  • 编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
  • elint、pretiier 对代码错误或风格的检查;
  • webpack 通过 babel 转译 javascript 语法;

在工程构建方面,不管是 webpack,还是 esbuild、vite 都有转换器,区别是转换工作的多少。在 esbuild 的文档open in new window中,我们可以看到,esbuild 只在词法分析、代码转换和代码压缩时需要处理 JavaScript AST,而其他构建工具,因为需要在多个库中传递数据,因此对代码的处理存在多个转换过程,比如 string → TS → JS → string,然后 string → JS → old JS → string,再然后 string → JS → minified JS → string,对于资源的使用更多,性能也会更差。

除了 JavaScript AST 以外,很多构建工具都有 loader 的概念,用于处理图片、css 文件等各种各样的资源。这其实也是一种转换器,将其他静态资源转换成了 JS 模块。

压缩器

说到压缩大家都非常熟悉,比如代码压缩、图片压缩等,业内也有非常多的工具库可以使用。

目前主流的代码压缩工具有 terseropen in new windowuglify-jsopen in new window 等。比如 terser 就是 webpack 的内置代码压缩工具。构建领域的新秀 esbuild 自己实现了代码压缩的能力,性能要甩开其他压缩工具至少一个数量级。

社区里有一个对比各个压缩工具性能的仓库open in new window,可以对比感受下各个压缩工具的性能差距。

这里贴一张压缩 react 17 源代码的性能对比图。

代码压缩性能对比

我们应该怎么办

到这里,我们并没有讲什么实现细节,但是相信大家对构建这块心里已经有了一个基本的模型,知道有哪些核心问题了。

现在我们来说说,在面对层出不穷的构建工具时,我们应该怎么办。是每个构建工具都学一遍吗?肯定不是。其实,还是要从核心问题出发,看看这些构建工具具体解决了什么问题,是否已经足够成熟可以在生产环境中使用了。

比如,esbuild 还在快速迭代过程中,很多周边生态还不完善,直接在生产环境中使用风险比较大。但是我们可以在 webpack 使用 esbuild 的压缩能力来提升构建的性能。

vite 也在高速迭代过程中,已经具备了初步的生产使用的基础。vite 内部也大量使用了 esbuild 的能力,比如预构建open in new window、代码压缩等。vite 我们可以保持适当关注,不仅仅因为它是 vue 3 的默认构建工具,同时也给我们提供了 webpack 以外的其他选择。

关注微信公众号,获取最新推送~