webpack 插件工作流程和原理

通过插件我们可以扩展 webpack,在合适的时机通过 Webpack 提供的 API 改变输出结果,使 webpack 可以执行更广泛的任务,拥有更强的构建能力。

  • 想要了解 webpack 的插件的机制,需要弄明白以下几个知识点:
    1. 一个简单的插件的构成
    2. webpack 构建流程
    3. Tapable 是如何把各个插件串联到一起的
    4. compiler 以及 compilation 对象的使用以及它们对应的事件钩子

一个简单的插件的构成

class HelloPlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options) {}
  // Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在emit阶段插入钩子函数,用于特定时机处理额外的逻辑;
    compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
      // 在功能流程完成后可以调用 webpack 提供的回调函数;
    })
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知webpack,才会进入下一个处理流程。
    compiler.plugin('emit', function(compilation, callback) {
      // 支持处理逻辑
      // 处理完毕后执行 callback 以通知 Webpack
      // 如果不执行 callback,运行流程将会一直卡在这不往下执行
      callback()
    })
  }
}

module.exports = HelloPlugin

// 使用:
const HelloPlugin = require('./hello-plugin.js')
var webpackConfig = {
  plugins: [new HelloPlugin({ options: true })],
}
  • 分析一下 webpack Plugin 的工作原理:
    1. 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例
    2. 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象
    3. 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack

webpack 构建流程

  • Webpack 的基本构建流程如下:

    1. 校验配置文件 :读取命令行传入或者 webpack.config.js 文件,初始化本次构建的配置参数
    2. 生成 Compiler 对象:执行配置文件中的插件实例化语句 new MyWebpackPlugin(),为 webpack 事件流挂上自定义 hooks
    3. 进入 entryOption 阶段:webpack 开始读取配置的 Entries,递归遍历所有的入口文件
    4. run/watch:如果运行在 watch 模式则执行 watch 方法,否则执行 run 方法
    5. compilation:创建 Compilation 对象回调 compilation 相关钩子,依次进入每一个入口文件(entry),使用 loader 对文件进行编译。通过 compilation 我可以可以读取到 module 的 resource(资源路径)、loaders(使用的 loader)等信息。再将编译好的文件内容使用 acorn 解析生成 AST 静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行 compilation 的 seal 方法对每个 chunk 进行整理、优化、封装__webpack_require__来模拟模块化操作
    6. emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的 compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块 Chunk 等等信息

理解事件流机制 Tapable

webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable。

Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条 webapck 机制中,去改变 webapck 的运作,使得整个系统扩展性良好。

Tapable 也是一个小型的 library,是 Webpack 的一个核心工具。类似于 node 中的 events 库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。

webpack 中最核心的负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 Tapable 的实例,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

/**
 * 广播事件
 * event-name 为事件名称,注意不要和现有的事件重名
 */
compiler.apply('event-name', params)
compilation.apply('event-name', params)
/**
 * 监听事件
 */
compiler.plugin('event-name', function(params) {})
compilation.plugin('event-name', function(params) {})

Tapable 类暴露了 tap、tapAsync 和 tapPromise 方法,可以根据钩子的同步/异步方式来选择一个函数注入逻辑。

  • tap 同步钩子

    compiler.hooks.compile.tap('MyPlugin', (params) => {
      console.log('以同步方式触及 compile 钩子。')
    })
    
  • tapAsync 异步钩子,通过 callback 回调告诉 Webpack 异步执行完毕 tapPromise 异步钩子,返回一个 Promise 告诉 Webpack 异步执行完毕

    compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
      console.log('以异步方式触及 run 钩子。')
      callback()
    })
    
    compiler.hooks.run.tapPromise('MyPlugin', (compiler) => {
      return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
        console.log('以具有延迟的异步方式触及 run 钩子')
      })
    })
    

Tabable 用法

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook,
} = require('tapable')

tapable 是如何将 webpack/webpack 插件关联的?

// Compiler.js
const { AsyncSeriesHook ,SyncHook } = require("tapable");
//创建类
class Compiler {
    constructor() {
        this.hooks = {
           run: new AsyncSeriesHook(["compiler"]), //异步钩子
           compile: new SyncHook(["params"]),//同步钩子
        };
    },
    run(){
      //执行异步钩子
      this.hooks.run.callAsync(this, err => {
         this.compile(onCompiled);
      });
    },
    compile(){
      //执行同步钩子 并传参
      this.hooks.compile.call(params);
    }
}
module.exports = Compiler
const Compiler = require('./Compiler')

class MyPlugin {
  apply(compiler) {
    //接受 compiler参数
    compiler.hooks.run.tap('MyPlugin', () => console.log('开始编译...'))
    compiler.hooks.compile.tapAsync('MyPlugin', (name, age) => {
      setTimeout(() => {
        console.log('编译中...')
      }, 1000)
    })
  }
}

// 这里类似于webpack.config.js的plugins配置
// 向 plugins 属性传入 new 实例

const myPlugin = new MyPlugin()

const options = {
  plugins: [myPlugin],
}
let compiler = new Compiler(options)
compiler.run()

webpack4 核心模块 tapable 源码解析: https://www.cnblogs.com/tugenhua0707/p/11317557.html

理解 Compiler(负责编译)

Compiler 对象包含了当前运行 Webpack 的配置,包括 entry、output、loaders 等配置,这个对象在启动 Webpack 时被实例化,而且是全局唯一的。Plugin 可以通过该对象获取到 Webpack 的配置信息进行处理。

apply 方法中插入钩子的一般形式如下:

// compiler提供了compiler.hooks,可以根据这些不同的时刻去让插件做不同的事情。
compiler.hooks.阶段.tap函数('插件名称', (阶段回调参数) => {})
compiler.run(callback)

理解 Compilation

Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。 简单来说,Compilation 的职责就是构建模块和 Chunk,并利用插件优化构建过程。

介绍几个常用的 Compilation Hooks

buildModule(SyncHook): 在模块开始编译之前触发,可以用于修改模

succeedModule(SyncHook): 在模块开始编译之前触发,可以用于修改模块

finishModules(AsyncSeriesHook): 当所有模块都编译成功后被调用

seal(SyncHook): 当一次 compilation 停止接收新模块时触发

optimizeDependencies(SyncBailHook): 在依赖优化的开始执行

optimize(SyncHook): 在优化阶段的开始执行

optimizeModules(SyncBailHook): 在模块优化阶段开始时执行,插件可以在这个钩子里执行对模块的优化,回调参数:modules

optimizeChunks(SyncBailHook): 在代码块优化阶段开始时执行,插件可以在这个钩子里执行对代码块的优化,回调参数:chunks

optimizeChunkAssets(AsyncSeriesHook): 优化任何代码块资源,这些资源存放在 compilation.assets 上。一个 chunk 有一个 files 属性,它指向由一个 chunk 创建的所有文件。任何额外的 chunk 资源都存放在 compilation.additionalChunkAssets 上。回调参数:chunks

optimizeAssets(AsyncSeriesHook): 优化所有存放在 compilation.assets 的所有资源。回调参数:assets

Compiler 和 Compilation 的区别

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,compilation 就会被重新创建。

  • webpack 打包过程或者插件代码里该如何调试?
node --inspect-brk ./node_modules/webpack/bin/webpack.js --inline --progress

其中参数--inspect-brk 就是以调试模式启动 node