手把手用代码教你实现一个 webpack plugin

上一篇文章咱们实现了本身的 loader,这篇来实现 pluginhtml

什么是 plugin

loader 相比,plugin 功能更强大,更灵活node

插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者能够引入它们本身的行为到 webpack 构建流程中。

loaderplugin 的区别

  • loader: 顾名思义,某种类型资源文件的加载器,做用于某种类型的文件上。webpack 自己也是不能直接打包这些非 js 文件的,须要一个转化器即 loaderloader 自己是单一,简单的,不能将多个功能放在一个loader里。
  • plugin: pluginloaders 更加先进一点,你能够扩展 webpack 的功能来知足本身的须要。当 loader 不能知足的时候,就须要 plugin 了。

plugin 的基本结构

想必你们对 html-webpack-plugin 见得很是多,一般咱们都是这么使用的webpack

plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    new webpack.NoEmitOnErrorsPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    })
  ]

发现 webpack plugin 实际上是一个构造函数(classfunction)。为了可以得到 compiler,须要 plugin 对外暴露一个 apply 接口,这个 apply 函数在构造函数的 prototype 上。web

webpack 插件由如下组成:npm

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个apply方法。
  • 指定一个绑定到 webpack 自身的事件钩子
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

Compiler 和 Compilation

在插件开发中最重要的两个资源就是 compilercompilation 对象。理解它们的角色是扩展 webpack 引擎重要的第一步。segmentfault

  • compiler 对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好全部可操做的设置,包括 optionsloaderplugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可使用它来访问 webpack 的主环境。
  • compilation 对象表明了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了不少关键时机的回调,以供插件作自定义处理时选择使用。

开发 plugin

知道了 plugin 的基本构造,咱们就能够着手来写一个 plugin 了,仍是和开发 loader的目录同样,在src 中新建一个 plugins 文件夹,里面新建一个 DemoPlugin.js,里面内容为api

// src/plugins/DemoPlugin.js
class DemoPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    // console.log(compiler)
    console.log('applying', this.options)
  } 
}

入口文件 app.jsapp

// src/app.js
console.log('hello world')

webpack 配置async

// webpack.config.js
const DemoPlugin = require('./src/plugins/DemoPlugin')

module.exports = {
  mode: 'development',
  entry:  __dirname + "/src/app.js",
  output: {
    path: __dirname + "/dist",
    filename: "[name].js"
  },
  ...
  plugins: [
    new DemoPlugin({
      name: 'Jay'
    })
  ]
}

执行 ./node_modules/.bin/webpack 走一波,能够看到输出结果函数

image.png

说明咱们的插件已经成功运行了,你们也可自行将 compiler 打印出来看看。咱们再看涉及到 compilercompilation 的例子

// src/plugins/DemoPlugin.js
class DemoPlugin {
  constructor(options) {
    this.options = options
  }
  apply(compiler) {
    // Tap into compilation hook which gives compilation as argument to the callback function
    compiler.hooks.compilation.tap("DemoPlugin", compilation => {
      // Now we can tap into various hooks available through compilation
      compilation.hooks.optimize.tap("DemoPlugin", () => {
        console.log('Assets are being optimized.')
      })
    })
  } 
}

关于 compiler, compilation 的可用钩子函数,请查看插件文档

接下来咱们来本身写一个 BannerPlugin 的插件,这个插件是 webpack 官方提供的一款插件,能够在打包后的每一个文件上面加上说明信息,像是这样子的

image.png

固然官方提供的功能更丰富一些,打包时还能够加上文件更多诸如 hash, chunkhash, 文件名以及路径等信息。

这里咱们只实如今打包时加个说明,插件就命名为 MyBannerPlugin 吧。在 plugins 文件下新建 MyBannerPlugin.js,怎么写待会儿再说,咱们先在 webpack.config.js 中加上该插件

const path = require('path')
const MyBannerPlugin = require('./src/plugins/MyBannerPlugin')

module.exports = {
  mode: 'development',
  devtool: 'eval-source-map',
  entry:  __dirname + "/src/app.js",
  output: {
    path: __dirname + "/dist",
    filename: "[name].js"
  },
  plugins: [
    new DemoPlugin({
      name: 'Jay'
    }),
    new MyBannerPlugin('版权全部,翻版必究')
    // 或这么调调用
    // new MyBannerPlugin({
    //    banner: '版权全部,翻版必究'
    // })
  ]
}

但愿支持两种调用方式,直接传字符串或者对象的形式,那就开始写吧

// src/plugins/MyBannerPlugin.js
class MyBannerPlugin {
  constructor(options) {
    if (arguments.length > 1) throw new Error("MyBannerPlugin only takes one argument (pass an options object or string)")
    if (typeof options === 'string') {
      options = {
        banner: options
      }
    }
    this.options = options || {}
    this.banner = options.banner
  }
}
module.exports = MyBannerPlugin

这样,咱们已经拿到传过来的配置,可是咱们的需求是在打包后的文件头部加上的说明信息是带有注释的,固然,也能够给使用者一个选项是否用注释包裹

// src/plugins/MyBannerPlugin.js

const wrapComment = str => {
  if (!str.includes('\n')) return `/*! ${str} */`
  return `/*!\n * ${str.split('\n').join('\n * ')}\n */`
}

class MyBannerPlugin {
  constructor(options) {
    ...
    if (typeof options === 'string') {
      options = {
        banner: options,
        raw: false // 默认是注释形式
      }
    }
    this.options = options || {}
    this.banner = this.options.raw ? options.banner : wrapComment(options.banner)
  }
}
module.exports = MyBannerPlugin

接下来就写 apply 部分了。因为要对文件写入东西,咱们须要引入一个 npm 包。

npm install --save-dev webpack-sources
const { ConcatSource } = require('webapck-sources')
...
  apply (compiler) {
    const banner = this.banner
    // console.log('banner: ', banner)
    compiler.hooks.compilation.tap("MyBannerPlugin", compilation => {
      compilation.hooks.optimizeChunkAssets.tap("MyBannerPlugin", chunks => {
        for (const chunk of chunks) {
          for (const file of chunk.files) {
            compilation.updateAsset(
              file,
              old => new ConcatSource(banner, '\n', old)
            )
          }
        }
      })
    })
  }
...

跑一下

./node_modules/.bin/webpack

能够看到结果了

image.png

打包出来的文件也有说明信息

image.png

完整代码以下

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

const wrapComment = (str) => {
  if (!str.includes('\n')) return `/*! ${str} */`
  return `/*!\n * ${str.split('\n').join('\n * ')}\n */`
}

class MyBannerPlugin {
  constructor (options) {
    if (arguments.length > 1) throw new Error("MyBannerPlugin only takes one argument (pass an options object or string)")
    if (typeof options === 'string') {
      options = {
        banner: options,
        raw: false // 默认是注释形式
      }
    }
    this.options = options || {}
    this.banner = this.options.raw ? options.banner : wrapComment(options.banner)
  }
  apply (compiler) {
    const banner = this.banner
    console.log('banner: ', banner)
    compiler.hooks.compilation.tap("MyBannerPlugin", compilation => {
      compilation.hooks.optimizeChunkAssets.tap("MyBannerPlugin", chunks => {
        for (const chunk of chunks) {
          for (const file of chunk.files) {
            compilation.updateAsset(
              file,
              old => new ConcatSource(banner, '\n', old)
            )
          }
        }
      })
    })
  }
}

module.exports = MyBannerPlugin

再看一个官网给的统计打包后文件列表的例子,在 plugins 中新建 FileListPlugin.js,直接贴代码

// src/plugins/FileListPlugin.js
class FileListPlugin {
  apply(compiler) {
    // emit is asynchronous hook, tapping into it using tapAsync, you can use tapPromise/tap(synchronous) as well
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
      // Create a header string for the generated file:
      var filelist = 'In this build:\n\n'

      // Loop through all compiled assets,
      // adding a new line item for each filename.
      for (var filename in compilation.assets) {
        filelist += '- ' + filename + '\n'
      }

      // Insert this list into the webpack build as a new file asset:
      compilation.assets['filelist.md'] = {
        source: function() {
          return filelist
        },
        size: function() {
          return filelist.length
        }
      }

      callback()
    })
  }
}

module.exports = FileListPlugin;
// webpack.config.js
...
const FileListPlugin = require('./src/plugins/FileListPlugin')

...
plugins: [
  new DemoPlugin({
    name: 'Jay'
  }),
  new MyBannerPlugin({
    banner: '版权全部,翻版必究'
  }),
  new FileListPlugin()
]
...

打包后会发现,dist 里面生成了一个 filelist.md 的文件,里面内容为

In this build:

- main.js

完了!