6

上一篇文章我们实现了自己的 loader,这篇来实现 plugin

什么是 plugin

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

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

loaderplugin 的区别

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

plugin 的基本结构

想必大家对 html-webpack-plugin 见得非常多,通常我们都是这么使用的

  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 上。

webpack 插件由以下组成:

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

Compiler 和 Compilation

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

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

开发 plugin

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

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

入口文件 app.js

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

webpack 配置

// 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

完了!


见贤思齐
66 声望8 粉丝

写代码的