Plugin

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26

Webpack 运行的生命周期中会广播许多事件, Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的API改变输出结果。

一个最基础的Plugin代码如下:

class BasicPlugin {
  // 在构造函数中获取用户为该插件传入的配置
  constructor(options) {
    this.options = options
  },
  // Webpack会调用BasicPlugin实例的apply方法为插件实例传入compiler对象
  apply(compiler) {
    compiler.plugin('compilation', function(compilation){
      // ... 监听到compilation事件后的一系列操作
    })
  }
}

// 导出
module.exports = BasicPlugin;

Webpack 的配置中这样使用

const BasicPlugin = require('./BasicPlugin.js');
module.exports = {
  Plugins: [
    new BasicPlugin(options),
  ]
}

Webpack启动后,在读取配置的过程中会先执行new BasicPlugin(options),初始化一个BasicPlugin并获得其实例。在初始化Compiler对象后,再调用basicPlugin.apply(compiler)为插件实例传入compiler对象。插件实例在获取到compiler对象后,就可以通过compiler.plugin(事件名称,回调函数)监听到Webpack广播的事件,并且可以通过compiler对象去操作Webpack

Compiler 和 Compilation

Compiler 对象包含了 Webpack 环境的所有配置信息,包含optionsloadersplugins等信息。这个对象在Webpack 启动时被实例化,它是全局唯一的,可以简单地将它理解为 Webpack 实例。

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack以开发模式运行时,每当检测到一个文件发生变化,便有一次新的Compilation 被创建。Compilation 对象也提供了很多事件回调供插件进行扩展。通过Compilation也能读取到Compiler 对象。

CompilerCompilation 的区别在于: Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而Compilation 只代表一次新的编译。

事件流

使用了观察者模式,插件中进行订阅,Webpack进行发布(开发的插件中也能发布其他事件)。

// 发布
compiler.apply('event-name', params);

// 插件中订阅的事件回调就会被调用
compiler.plugin('event-name', function(params){
  // 监听到了事件的发布 该回调就会调用。
})

只要能拿到 CompilerCompilation 对象,就能广播新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用。传给每个插件的CompilerCompilation 对象都是同一个引用。也就是说,若在一个插件中修改了CompilerCompilation 对象上的属性,就会影响到后面的插件。

有些事件是异步的,这些异步的事件会附带两个参数,第2个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一个处理流程。

compiler.plugin('emit', function(compilation, callback){
  // 处理完毕后执行callback 以通知Webpack
  // 如果不执行callback ,运行流程将会一直卡在这里而不往后执行
  callback();
})

常用API

读取输出资源、代码块、模块及其依赖

emit 事件发生时,代表源文件的转换和组装己经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。插件的代码如下:

class Plugin {
  constructor() {

  },
  apply(complier) {
    complier.plugin('emit', function(compilation, callback){
      // compilation.chunks : Array 存放所有代码块
      compilation.chunks.forEach(function(chunk) {
        // 而chunk本身又是由很多个Modules组成的,所以也是二维数组
        // 可以使用forEachModule来遍历
        chunk.forEachModule(function (module) {
          // module就是chunk中每一个单独的模块
          // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
          module.fileDependencies.forEach(function(filePath){
            // filePath 就是 module中每一个依赖文件的路径
          })
        })

        // Webpack 会根据Chunk 生成输出的文件资源,每个Chunk 都对应一个及以上的输出文件
        // 例如在chunk 中包含css 模块并且使用了ExtractTextPlugin 时
        // 该chunk 就会生成.js 和 .css 两个文件
        // chunk.files 就是该chunk将要输出的文件名称组成的数组
        chunk.files.forEach(function(filename){
          // compilation.assets 存放着当前即将输出的所有资源
          // 通过该文件名在compilation.assets中获取当前输出资源,调用一个输出资源的source()方法能获取输出资源的内容
          let source = compilation.assets[filename].source();
        })
      })
      // 这是一个异步事件,要记得调用callback 来通知Webpack 本次事件监听处理结束
      callback();
    })
  }
}
监听文件的变化

Webpack 会从配置的入口模块出发,依次找出所有依赖模块, 当入口模块或者其依赖的模块发生变化时, 就会触发一次新的Compilation

在开发插件时经常需要知道是哪个文件发生的变化导致了新的Compilation,可以监听watch-run事件,当依赖的文件发生变化时会被触发。

compiler.plugin('watch-run', function(watching, callback){
  // 获取发生变化的文件列表
  const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
  // changedFiles 格式为键值对,键为发生变化的文件路径
  if (changedFiles['./show.js'] !== undefined) {
    // show.js 发生了变化
  }
  // 异步事件
  callback();
})

Webpack 只会监视入口和其依赖的模块是否发生了变化,所以Webpack 不会监听HTML 文件的变化,编辑HTML 文件时就不会重新触发新的Compilation。某些情况下会需要监听HTML的变化,这时候可以将HTML 文件加入依赖列表中,为此可以使用如下代码:

// after-compile 会在一次Compilation 执行完成
comper.plugin('after-compile', (compilation, callback) => {
  // 将HTML 文件添加到文件依赖列表中,因此在HTML 模板文件发生变化时重新启动一次编译
  compilation.fileDependencies.push('./test.html');
  callback();
})
修改输出资源

在某些场景下插件需要修改、增加、删除输出的资源,要做到这一点, 则需要监听emit事件,因为发生emit 事件时所有模块的转换和代码块对应的文件已经生成好,需要输出的资源即将输出,因此emit 事件是修改 Webpack 输出资源的最后时机。

上面已提到过 compilation.assets存放着当前即将输出的所有资源,是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

// 修改输出文件
compiler.plugin('emit', (compilation, callback) => {
  // 设置名称为fileName 的输出资源
  compilation.assets[fileName] = {
      // source()用来输出文件内容,在source方法中对文件进行修改并输出
    source: () => {
      // 文件内容 既可以是代表文本文件的字符串,也可以是代表二进制文件的Buffer
      return fileContent;
    },
    // 返回文件的大小
    size: () => {
      return Buffer.byteLength(fileContent, 'utf8');
    }
  }
  callback();
})

// 读取输出文件
compiler.plugin('emit', (compilation, callback) => {
  // 读取名称为fileName 的输出资源
  const asset = compiler.assets[fileName];
  // 获取输出的内容
  asset.source();
  // 获取输出资源的大小
  asset.size();
  callback();
})
判断Webpack 使用了哪些插件

在开发一个插件时,我们可能需要根据当前配置是否使用了其他插件来做下一步决定,因此需要读取Webpack 当前的插件配置情况。比如,若想判断当前是否使用了ExtractTextPlugin

function hasExtractTextPlugin(compiler) {
  // 当前配置使用的所有插件列表
  const plugins = compiler.options.plugins;

  const tmp = plugins.find(plugin => {
    // 去plugins 中寻找有没有ExtractTextPlugin 的实例
    return plugin.__proto__.constructor === ExtractTextPlugin
  });

  return tmp != null;
}

写一个简单至极的插件

该插件的名称为 EndWebpackPlugin ,作用是在Webpack 即将退出时 针对构建成功还是构建失败 附加一些额外的操作。

// 如何使用 
module.exports = {
  Plugins: [
    // 在初始化的时候,传入两个参数,分别会成功回调和失败回调
    new EndWebpackPlugin(() => {
      // Webpack 构建成功,并且在文件输出后会执行到这里,在这里可以做发布文件操作
    }, (err) => {
      // Webpack 构建失败, err 是导致错误的原因
      console.log(err);
    })
]
}
// 插件代码

class EndWebpackPlugin {
  constructor(doneCb, failCb) {
    this.doneCb = doneCb;
    this.failCb = failCb;
  },
  apply(compiler) {
    compiler.plugin('done', function(stats){
      // 监听webpack 的 done 事件,回调doneCb
      this.doneCb(stats)
    })

    compiler.plugin('failed', function(err){
      // 监听webpack 的 done 事件,回调doneCb
      this.failCb(err)
    })
  }
}

module.exports = EndWebpackPlugin;