随着前端工程化的不断发展,构建工具也在不断完善。作为大前端时代的新宠,webpack 渐渐成为新时代前端工程师不可或缺的构建工具,随着 webpack 4 的不断迭代,我们享受着构建效率不断提升带来的快感,配置不断减少的舒适,也一直为重写的构建事件钩子机制煞费苦心,为插件各种不兼容心灰意冷,虽然过程痛苦,但结果总是美好的。
在阅读本文之前,我就默认电脑前的你已经掌握了 webpack 的基本配置,能够独立搭建一款基于 webpack 的前端自动化构建体系,所以这篇文章不会教你如何配置或者使用 webpack,自然具体概念我就不做介绍了,直面主题,开始讲解 webpack 原理。
webpack的运行过程可以简单概述为如下流程:
初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件
接下来,我们将对具体流程逐一介绍。
在分析 webpack 运行流程时,我们可以借助一个概念,便是 webpack 的事件流机制。
什么是 webpack 事件流?
Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 Webpack 通过
Tapable
来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 --吴浩麟《深入浅出webpack》
我们将 webpack 事件流理解为 webpack 构建过程中的一系列事件,他们分别表示着不同的构建周期和状态,我们可以像在浏览器上监听 click 事件一样监听事件流上的事件,并且为它们挂载事件回调。我们也可以自定义事件并在合适时机进行广播,这一切都是使用了 webpack 自带的模块 Tapable
进行管理的。我们不需要自行安装 Tapable
,在webpack被安装的同时它也会一并被安装,如需使用,我们只需要在文件里直接 require
即可。
Tapable的原理其实就是我们在前端进阶过程中都会经历的 EventEmit,通过发布-订阅模式实现,它的部分核心代码可以概括成下面这样:
因为 webpack 4 重写了事件流机制,所以如果我们翻阅 webpack hook 的官方文档会发现信息特别繁杂,但是在实际使用中,我们只需要记住几个重要的事件就足够了。
在讲解 webpack 流程之前先附上一张我自己绘制的执行流程图:
require
语法替换成 __webpack_require__
来模拟模块化操作。在1.2.2中,我们看到了一个陌生的字眼——AST,上网一搜:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是 “抽象” 的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于
if-condition-then
这样的条件跳转语句,可以使用带有两个分支的节点来表示。 --维基百科
其实,你只要记着,AST是一棵树,像这样:
转换成 AST 的目的就是将我们书写的字符串文件转换成计算机更容易识别的数据结构,这样更容易提取其中的关键信息,而这棵树在计算机上的表现形式,其实就是一个单纯的 Object。
示例是一个简单的声明赋值语句,经过AST转化后各部分内容的含义就更为清晰明了了。
接下来,我们来看看 webpack 的输出内容。如果我们没有设置 splitChunk,我们只会在dist 目录下看到一个 main.js 输出文件,过滤掉没用的注释还有一些目前不需要去考虑的Funciton,得到的代码大概是下面这样:
我们都知道其实 webpack 在浏览器实现模块化的本质就是将所有的代码都注入到同一个 JS 文件里,现在我们可以清晰明了地看出 webpack 最后生成的也不过只是一个 IIFE,我们引入的所有模块都被一个 function
给包起来组装成一个对象,这个对象作为 IIFE 的实参被传递进去。
但如果我们配置了 splitChunk,这时候输出的文件就和你的 Chunk 挂钩了
这时候,IIFE 的形参也变成了摆设,所有我们的模块都被放在了一个名为 webpackJsonp
的全局数组上,通过 IIFE 里的 webpackJsonpCallback
来处理数据。
纵观 webpack 构建流程,我们可以发现整个构建过程主要花费时间的部分也就是递归遍历各个 entry 然后寻找依赖逐个编译的过程,每次递归都需要经历 String->AST->String 的流程,经过 loader 还需要处理一些字符串或者执行一些 JS 脚本,介于 node.js 单线程的壁垒,webpack 构建慢一直成为它饱受诟病的原因。这也是 happypack 之所以能大火的原因,我们可以来看一段 happypack 的示例代码:
大家如果有用过 pm2 的话就能很容易明白了,其实原理是一致的,都是利用了 node.js 原生的 cluster 模块去开辟多进程执行构建,不过在 4 之后大家就可以不用去纠结这一问题了,多进程构建已经被集成在 webpack 本身上了,除了增量编译,这也是4之所以能大幅度提升构建效率的原因之一。