阅读 1055

webpack构建流程及梳理

摘要

webpack的核心功能是通过抽离出很多插件来实现的,因此系统内功能的划分粒度很细,这样做到了完美解偶同时又分工明确,代码容易维护。可以说插件就是webpack的基石,这些基石又影响着流程的走向。这些钩子是通过Tapable串起来的,可以类比Vue框架的生命周期,webpack也有自己的生命周期,在周期里边会顺序地触发一些钩子,挂载在这些钩子上的插件得以执行,从而进行一些特定的逻辑处理。在插件里边,构建的实体或构建出来的数据结果都是可触达的,这样做实现了webpack的高度可扩展。了解了这些之后,我们就不再怀疑webpack是如何拥有如此丰富的生态体系及社区、如何达到了今天的高度。

关于Compiler

Compiler对象就是webpack的实体(是Tapable的实例),掌控者整个webpack的生命周期,他不执行具体的任务,只是进行一些调度工作(调兵遣将)。他创建了Compilation对象,Compilation任务执行完毕后会将最终的处理结果返回给Compiler。官网列出了该对象暴露出的所有钩子。

关于Compilation

Compilation是编译阶段的主要执行者,(是Tapable的实例),执行模块创建、依赖收集、分块、打包等主要任务的对象。官网列出了该对象暴露出的所有钩子。

在第二章节,我们了解到获取到配置数据之后,启动了compiler.run方法。其实在compiler启动run之前,在webpack.js我们会发现还做了很多初始化操作,比如增加默认操作,比如针对不同的配置项(如target、devtool)初始化相应的插件等。

webpack支持传入多个配置对象,比如一个library有多个构建目标,就需要传入多个配置对象,每个配置对象都会执行。如果传入一个数组,初始化的就不是Compiler,而是MultiCompiler,最后会运作MultiCompiler上的run方法,在里边遍历compilers对象,存放着Compiler数组,然后会依次调用Conmpiler的run方法。

Compiler

compiler的启动是run方法,run方法里边主要关注两个动作:调用了compile方法;声明了调用compile传入的回调函数onCompiled。
复制代码
  1. compile()

    涉及webpack构建生命周期的几个重要钩子:

    1. compile上的钩子:beforeCompile、compile、make(关键钩子)、afterCompile、thisCompilation、compilation
    2. compilation上的钩子:finish、seal
      在这个方法里边,主要是构建创建Compilation所需要的参数并创建了Compilation,这个参数就是后边解析module需要用的工厂函数。
      make钩子是一个关键的钩子,调用make钩子时传入的是新建的compilation对象,在这个钩子上挂载了一些入口插件的处理逻辑,这些入口插件里边调用compilation.addEntry(),此后,控制权就由Compiler转移到了Compilation。在make钩子的回调函数里边调用了compilation的finish、seal钩子。
  2. onCompiled()

    涉及webpack构建生命周期的最后几个重要钩子:emit、done。该方法相当于将Compilation的权限又收取回来。此时拿到的compilation对象是汇集了经过module解析、loader处理、template编译后的所有资源文件。
    该方法里边主要调用了emitAssets方法,该方法调用了emit钩子(这一步我们可以获取完整的构建数据),获取compilation构建出来的所有的assets资源数据,里边递归的调用writeOut写入最终的chunk文件,并调用done钩子。

Compilation

compilation开始于addEntry方法并结束于addEntry。
复制代码
  1. addEntry(): 根据入口的配置模式,分为单入口和多入口。该方法里边主要调用了_addModuleChain()。
  2. _addModuleChain(): 创建module。根据入口的不同,使用不同的模块工厂(从创建Compilation传入的参数中获取,包括ContextModuleFactory或NormalModuleFactory)的create方法创建模块,获取模块相关的parser、loader、hash等数据信息。
  3. buildModule(): 调用module.build()进行module构建。
  4. addModuleDependencies(): 构建成功之后,递归地获取依赖模块并构建(逻辑同_addModuleChain)。
  5. successEntry: 执行完上述的操作之后,在_addModuleChain的回调函数里边调用succeedEntry钩子,在这个钩子里边可以获取刚创建的module。然后将控制权返回给Compiler。

上述阶段完成后,cimpiler调用了compilation的finish()、seal(),这里我们重点关注seal方法。

  1. seal(): 该方法主要完成了chunk的构建。主要是收集modules、chunks,使用template(在Compilation的构建函里初始化了几种Template:MainTemplate、ChunkTemplate等)对chunk进行编译。entry属性配置的入口模块使用的是MainTemplate,里边会加入启动webpack多需要的代码,创建并更新hash信息等,并调用emitAsset()会将最终的资源文件全部收集在assets对象里边。
  2. emitAsset(): 收集assets资源数据,多个插件都有调用该方法。

Tapable改造

为方便源码的学习,想获取webpack执行过程中钩子的挂载及触发情况,改造了Tapable。主要是修改Hook.js文件。顶部需要引入fs库。

const fs = require('fs')
复制代码
  1. 在call方法里插入逻辑
_fnCp(fn, name, type) {
      const _fn = (...arg) => {
            // console.log('hahaha:', arg)
            fs.writeFileSync('/Users/eleme/Documents/my/test-webpack/calls.js', `${type}: ${name} \n`, { 'flag': 'a' },  () => {})
                  return fn(...arg)
            }
      return _fn
}
// 改造tap、tapAsync、tapPromise方法
tap(options, fn) {
      // ...
      // options = Object.assign({ type: "sync", fn: fn }, options);
      options = Object.assign({ type: "sync", fn: this._fnCp(fn, options.name, "sync") }, options);
      // ...
}
tapAsync(options, fn) {
      // ...
      // options = Object.assign({ type: "async", fn: fn }, options);
      options = Object.assign({ type: "async", fn: this._fnCp(fn, options.name, "async") }, options);
      // ...
}
tapPromise(options, fn) {
      // ...
      // options = Object.assign({ type: "promise", fn: fn }, options);
      options = Object.assign({ type: "promise", fn: this._fnCp(fn, options.name, "promise") }, options);
      // ...
}
复制代码
  1. 在tap方法插入逻辑
// 改造insert方法,在方法最后插入一条语句
_insert(item){
      fs.writeFileSync('/Users/eleme/Documents/my/test-webpack/taps.js', `${item.type}: ${item.name} \n`, { 'flag': 'a' },  () => {})
}
复制代码

总结

至此,我们大致理了一下webpack构建的脉络。webpack体系非常庞大,内部封装了很多webpack自己的库。学习webpack源码的目的一个是学习好的构建思想,一个是方便自己在业务中开发插件,这里有源码注释版及其他资料可供拓展。

关注下面的标签,发现更多相似文章
评论