随笔|闲扯前端工程化⑧---webpack的构建流程

102 阅读6分钟

通常在项目中有两种运行webpack的方式,基于命令行的方式以及基于代码的方式,如下所示:

// 命令

webpack --config webpack.config.js

//代码

const webpack = require('webpack')
const config = require('./webpack.config.js')

webpack(config,(err)=>{})
...

命令行的方式,就是我们比较熟悉的,我们在npm script中编写脚本去执行,我们可以把它e的config指向一个配置文件,这是命令行的方式。

基于代码的方式呢,就是我们去写一个nodejs文件,在JS文件里面去require webpack的依赖包,以及去require相关 配置文件,然后去通过执行的方式去调用。

需要注意的是,如果是以代码的方式去执行的话,这个执行函数要不就单独赋值给一个compile的变量,然后去执行这个compile的获取的流程,要不就像这个示例代码中那样,在执行外派的方法时去传入一个回调函数。只有这样他才会去真正的去执行到运行run的那个阶段。如果单单是传入config,实际上是不会去往下走。

const webpack = (options,callback)=>{
  options =... // 处理相关配置
  
  let compiler = new Complier(options.context)
  WebpackOptionsApply().process(options,compiler);
  // 分析参数,加载插件
  
  if(callback){
    ...
    compiler.run(callback)
    }
    
    return compiler
}

无论使用哪种方式去运行外派,它本质上都是webpackJS中的webpack函数

这个函数的核心逻辑是根据配置来生成编译器的实例compiler,然后去处理参数,执行WebpackOptionsApply.process(),然后根据参数来加载不同的插件, 然后根据参数来加载不同的插件,在有回调函数的情况下,根据是否是watch模式来决定是执行compiler.watch()还是compiler.run()。

运行还是compiler.run()的执行逻辑也会比较复杂,我们把它按照流程抽象一下,抽象后的执行流程是下面这样:

首先我们去readRecords读取构建记录,而且可以用于分包缓存优化,在未设置recordsPath时直接返回。

第二步是compil了的主要构建流程,涉及到以下几个环节:

  • 首先是newCompilationParams来创建这个构建过程的参数,创建NormalModules和ContextModule的工厂实力,用于创建后续的模块实例。
  • 第二个是new compilation创建编译过程的实例,传入上一步的两个工厂实例作为参数。
  • 第三步是执行compiler.hooks.make.callAsync, 它会触发make的这个hook,所有监听了make的插件都会在这个阶段进行执行处理,他会在具体的这个插件中去触发compilation的方法。
  • 第四步,当这个make的这个过程执行完成之后,我们进入到compilation.finish,表示编译过程结束,会在finish触发后,调用相应的钩子方法并报告构建模块的错误警告。
  • 第五步是compilation.seal(),它里面主要包含编译之后的所有优化过程。

在整个这个构建过程完成之后,下一步是emitAssets会调用第五步是compilation.getAssets()获取产物内容,然后去写入到输出文件中。

最后是emitRecords用于写入构建记录。在未设置recordsPath时直接返回。

在编译器运行流程中,核心的过程是第二步编译的过程。具体流程是在生成的compilation的实例中进行。

其中在编译执行过程中,主要从外部调用的是这个编码文件中的两个方法:

  • 第一个是addEntry这个方法,这个方法会从这个配置文件中的entry开始递归的添加和构建模块。
  • 第二个是在编译过程执行完成之后走到seal()方法,这个方法它的作用是冻结模块,表示在这个阶段开始上面的编译过程都结束了,他会对已经编译过的模块进行一系列优化,然后触发个优化阶段的Hooks。

以上就是执行webpack构建时的基本流程。

首先,第一步创建编译器compiler的实例。

第二步,根据webpack的参数加载相关的插件,不管参数中插件还是程序内置的插件,都在这个阶段进行加载。

第三步,执行编译流程,创建编译过程的compilation的实例,从入口递归添加和构建模块。构建完成之后,共建模块,然后进行相关的优化。

第四步,构建优化过程结束后,提交产物,将产物内容写入输入文件中。

除了了解上面的基本工作流程外,还有两个相关的概念需要理解,那就是webpack的生命周期和插件系统

webpack工作流程中核心的模块,Compiler和compilation都扩展自Tapple这个类。

用于实现工作流程中的生命周期划分,以便在不同的生命周期节点上注册和调用插件,所暴露出来的生命周期节点,我们就称为hooks组成的钩子。

webpack中的插件又怎么回事儿呢?

webpack引擎是基于插件系统搭建而成的,不同的插件各司其职,在webpack的工作流中的某一个或者多个时间点上,对构建流程的某一个方面进行处理。

class MyWebPackPlugin{
  apply(compiler){
    compiler.hooks.run.tap('MyWebPackPlugin',complation=>{
    console.log('MyWebPackPlugin--->')
    })
  }
}

webpack就是通过这样的工作方式,在各生命周期中经过一系列插件,将源代码逐步的变为最后的产物。

一个外派的插件是一个包含apply方法的js对象,它定义的这个插件是一个类,一般来说我们会new这个类,然后生成一个这个实例对象。

这个Apply方法的执行逻辑通常是注册webpack工作流程中的某一个生命周期的hook,然后添加对应hook,用该插件的实际处理函数。

Hooks的使用我们可以分为四步:

  • 第一步是在各函数定义hook的类型和参数来生成这个hook的对象。
  • 第二个是在插件中注册对应的hook添加对应后触发时的具体执行函数。
  • 第三步是在webpack运行时去生成这个插件实例。
  • 第四步是在运行到对应生命周期节点时,去调用hook,执行注册过插件的回调函数。
// compiler.js

this.hooks = {
...
makenew SyncHook(['compilation','params']),// 定义 HOOKS


...
}
this.hooks.compilation.call(compilation,params)// 调用 hook

// commonjsPlugin.js
// 插件中注册 hook
compiler.hooks.compilation.tap('commonjsPlugin',(compilation,{contextModuleFactory,})=>{...})
// webpackOptionsApply.js
// 生成插件实例,执行 apply方法

new CommonjsPlugin(options.module).apply(compiler)

webpack中hook一般使用方式,就是通过这种方式,webpack能够将编译器和编译过程的生命周期节点提供给外部插件,从而搭建起一个弹性化的工作引擎。

hook的类型按照同步或者异步,是否接收上插件的返回值等情况一共可以分为九种不同类型,不同类型的hook接收注册的方法也不相同,具体信息可以参考官方文档。

(完)

没有关注公众号的朋友,觉得文章对您有启发的话,记得点赞、关注、评论、转发一下。