详解webpack plugin的原理及编写一个plugin

1,436 阅读5分钟

plugin解决了什么问题?

plugin 解决了 webpack 构建生命周期过程中的功能定制问题,可以利用 plugin 参与到 webpack 构建流程中的各个阶段并劫持做一些代码处理。

比如,打包后需要生成一个 html 文件,那么就可以使用 html-webpack-plugin。还有,在打包之前把dist文件删除,就可以使用clean-webpack-plugin

webpack本身就是一个构建过程的状态机,其自身的核心功能也是构建在loader和plugin的机制上的。

compiler 和 compilation 具体是干什么的?

首先我们来看一个 webpack 自带的插件 BannerPlugin代码,其实 webpack 的很多核心公司就是利用插件来实现的。

插件的格式

  • 一个 JavaScript 函数或 JavaScript 类;
  • 在它原型上定义的 apply 方法,会在安装插件时被调用,并被 webpack compiler 调用一次;
  • 指定一个触及到 webpack 本身的事件钩子,即 hooks,用于特定时机处理额外的逻辑;
class BannerPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
       const options = this.options;
       const banner = this.banner;
       compiler.hooks.compilation.tap("BannerPlugin", compilation => {
           compilation.hooks.processAssets.tap({ name: "BannerPlugin"}, () =>{
               for (const chunk of compilation.chunks) {
                   for (const file of chunk.files) {
                       const data = {
                           chunk,
                           filename: file
                       };
                       // 生成注释
                       const comment = compilation.getPath(banner, data);
                       // 把注释加入到文件中
                       compilation.updateAsset(file, old => {
                               const source = options.footer
                                    ? new ConcatSource(old, "\n", comment)
                                    : new ConcatSource(comment, "\n", old);
                           
                                return source;
                     }
              });
      }} 
}}}}

从代码中出现了 compilercompilation,那它们到底是什么呢?

compiler

compiler 模块是 Webpack 最核心的模块。每次执行 Webpack 构建的时候,在 Webpack 内部,会首先实例化一个 Compiler 对象,然后调用它的 run 方法来开始一次完整的编译过程。compiler 对象代表了完整的 webpack 环境配置,插件可以通过它获取到 webpack 的配置信息,如entry、output、module等配置。

compiler 钩子 compiler 有很多钩子,下面只介绍常用的几个:

钩子名Tapable 类型触发时机传入 callback 的参数
entryOptionSyncBailHook在 webpack 中的 entry 配置处理过之后contextentry
afterPluginsSyncHook初始化完内置插件之后compiler
environmentSyncHook准备编译环境,webpack plugins配置初始化完成之后compiler
beforeRunAsyncSeriesHook开始正式编译之前compiler
runAsyncSeriesHook开始编译之后,读取 records 之前;compiler
compileSyncHook一次 compilation 编译创建之前compilationParams
compilationSyncHookcompilation创建成功之后compilationcompilationParams
emitAsyncSeriesHook生成资源到 output 目录之前compilation
doneAsyncSeriesHookcompilation完成之后stats
failedSyncHookcompilation失败

整个Compiler完整地展现了 Webpack 的构建流程:

  • 准备阶段make之前做的事情都属于准备阶段,这阶段的calback入参以compiler为主;
  • 编译阶段:这阶段以compilation的钩子为主,calback入参以compilation为主;
  • 产出阶段:这阶段从compilation开始,最后回到Compiler钩子上,calback传入参数是跟结果相关的数据,包括statserror

compilation

在 compilation 阶段,模块会被加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed)和重新创建(restored),Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展,通过 Compilation 也能读取到 Compiler 对象。

Compilation 钩子

在 Compilation 中处理的对象分别是modulechunkassets,由 modules 组成chunks,由chunks生成assets,处理顺序是:module → modules → chunks → assets,先从单个 module 开始处理,查找依赖关系,最后完成单个 module 处理,完成全部modules 之后,开始 chunks 阶段处理,最后在根据优化配置,按需生成 assets。

所以整个 Compilation 的生命周期钩子虽然比较多,但是大规律上是围绕这个顺序进行的,具体的钩子可以查看webpack官网。

Stats 对象 在 Webpack 的回调函数中会得到stats对象。这个对象实际来自于Compilation.getStats(),返回的是主要含有moduleschunksassets三个属性值的对象。

  • modules:记录了所有解析后的模块;
  • chunks:记录了所有chunk;
  • assets:记录了所有要生成的文件。

有了对compiler compilation的理解,那现在来看看BannerPlugin的实现,这个插件的功能是在最后生成的文件的头部加上一段我们自定义的注释,那么它的执行时机肯定是在编译完成之后,生成打包文件之间,也就是在 compiler.hooks.compilation这个大钩子下面的processAssets钩子里面执行我们的逻辑。

编写 plugin

plugin: 在文件尾部插入一段注释

首先创建一个plugins/FootPlugin.js,代码如下:

const { ConcatSource } = require('webpack-sources')

class FootPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    compiler.hooks.compilation.tap('FootPlugin', compilation => {
      compilation.hooks.processAssets.tap('FootPlugin', () => {
        for (const chunk of compilation.chunks) {
          for (const file of chunk.files) {
            console.log('file--', file) // bundle.js
            // 定义注释的内容
            const comment = `/* ${this.options.banner} */`
            compilation.updateAsset(file, old => {
              // 把注释和旧代码进行拼接
              return new ConcatSource(old, '\n', comment)
            })
          }
        }
      })
    })
  }
}

module.exports = FootPlugin

webpack.config.js

const FootPlugin = require('./plugins/FootPlugin')
module.exports = {
    plugins: [
        new webpack.BannerPlugin({
          banner: '欢迎学习'
        }),
        new FootPlugin({
          banner: '结束学习'
        })
  ]
}

可以看到在 bundle.js 的开头和结尾都有对应的注释。

image.png

image.png

plugin: 文件超过一定大小时给出警告

const { resolve } = require('path')
const fs = require('fs')

class BundleSizeWebpackPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    const { sizeLimit } = this.options
    console.log('bundle size plugin')
    // 在编译完成后,执行回调,拿到打包后文件路径,然后读取文件信息获取文件大小,然后定义一些逻辑
    compiler.hooks.done.tap('BundleSizePlugin', stats => {
      const { path, filename } = stats.compilation.outputOptions
      const bundlePath = resolve(path, filename)
      const { size } = fs.statSync(bundlePath)
      const bundleSize = size / 1024
      if (bundleSize < sizeLimit) {
        console.log(
          'safe: bundle-size',
          bundleSize,
          '\n size limit: ',
          sizeLimit
        )
      } else {
        console.warn(
          'unsafe: bundle-size',
          bundleSize,
          '\n size limit: ',
          sizeLimit
        )
      }
    })
  }
}

module.exports = BundleSizeWebpackPlugin

本章到这里就结束了,我们开始介绍了webpack的核心概念,有了对webpack的基本配置的了解;接着利用 css-loaderstyle-loader 对 webpack 的 loader机制进行了详细分析;最后,对 webpack plugin的工作机制和流程进行了梳理,并手写了两个 plugin,让你对 plugin 不再觉得遥不可及。有了这些前置知识,就可以对我们原生项目进行工程化的改造了。期待你的学习。