前言

webpack 插件为什么不直接写成一个具有 apply 方法的对象?而是写成一个具有 apply 方法的构造函数或者一个类?这个问题是比较奇怪的的,因为写成一个类在 webpack plugins 配置也是使用 new 去实例化成一个对象,那么现在就想想这是为什么

基础配置

先写一份最基础的 webpack 配置,插件使用官方的写法

// 编写一个插件
function TestPlugin() {
}
TestPlugin.prototype.apply = function (compiler) {
  console.log('构造函数方式 - 挂载插件')
  compiler.hooks.run.tap('plugin-done', () => {
    console.log('plugin run')
  })
  compiler.hooks.done.tap('plugin-run', () => {
    console.log('plugin done')
  })
}
module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js',
  },
  output: {
    filename: 'main.js'
  },
  // 配置插件
  plugins: [
    new TestPlugin() // 实例化插件
  ]
};

执行打包:

插件运行时正常的

现在来尝试直接写成一个具有 apply 的对象来测试下是否能正常工作

// 直接使用对象
const testPlugin= {
  apply(compiler) {
    console.log('对象方式 - 挂载插件')
    compiler.hooks.run.tap('plugin-done', () => {
      console.log('plugin run')
    })
    compiler.hooks.done.tap('plugin-run', () => {
      console.log('plugin done')
    })
  }
}

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js',
  },
  output: {
    filename: 'main.js'
  },
  // 配置插件
  plugins: [
    testPlugin // 直接使用对象
  ]
};

接下来运行一下

运行完事实证明直接写一个具有 apply 的对象是可以正常工作的。因为 plugins 最终接收的本身就是一个插件实例、一个对象

分析:

通过例子可以看到看起来都没什么问题,原因呢是因为大部分场景 webpack 只需要一个配置就够了。也就是一个插件对应一个配置下是没有问题的

因为一个配置就代表着对应一个 compiler 对象。而 webpack 是支持多配置的,多配置就意味着多个 compiler 编译对象。多个 compiler 之间它们并没有什么关系。多配置只是代表仅需要运行一次 webpack 打包命令就可以打包多个条件或者多个项目。而插件采用对象的方式,意味着这个对象实际上就是一个单例在一次运行上这个单例对象是被多个 compiler 共享的。在某些场景下必然会出现问题

还有一个最致命的问题是使用对象的方式无法传递参数

接下来看看一个多配置的场景

多配置

需求场景:

同一份源代码,分别打包 nodejs 上使用 commonjs 规范、和浏览器上使用的 umd 规范的目标代码。然后去统计一个配置文件经历了几个 hook

代码如下

const testPlugin = {
   hookRunCount: 0,  // 记录本次插件 hook 运行次数
   apply(compiler) {
     console.log('对象方式 - 挂载插件');
     compiler.hooks.emit.tap('plugin-emit', (compilation) => {
       console.log('------')
       this.hookRunCount++
       console.log(compilation.outputOptions.filename + '目标文件经历了' + this.hookRunCount + '个 hook')
     });
     compiler.hooks.afterEmit.tap('plugin-afterEmit', (compilation) => {
       console.log('------')
       this.hookRunCount++
       console.log(compilation.outputOptions.filename + '目标文件经历了' + this.hookRunCount + '个 hook')
     });
     compiler.hooks.done.tap('plugin-done', (state) => {
       console.log('------')
       this.hookRunCount++
       console.log(state.compilation.outputOptions.filename + '目标文件经历了' + this.hookRunCount + '个 hook')
     });
   }
 };
 const merge = require('webpack-merge');
 const config = {
   mode: 'development',
   entry: {
     index: './src/index.js',
   },
   output: {
     library: 'LibraryName',
   },
   plugins: [
     testPlugin   // 配置插件
   ]
 };
 module.exports =  [
   // nodejs 配置
   merge(config, {
     output: {
       filename: 'node.main.js',
       libraryTarget: 'commonjs2',
     },
   }),
   // 浏览器端配置
   merge(config, {
     output: {
       filename: 'browser.main.js',
       libraryTarget: 'umd',
       umdNamedDefine: true
     },
   })
 ];

运行结果

可以看到这结果明显错误,按照上面讲的理论一个 compiler 应该对应一个插件实例。而这里因为使用了单例插件对象,导致了多个 compiler 共享了这个单例对象

但是如果单纯把上面配置的单例插件对象改成 new 去实例化一个类也依然是多个 compiler 共享这个插件实例

// 编写一个插件
function TestPlugin() {
  this.hookRunCount = 0
}

TestPlugin.prototype.apply = function (compiler) {
  console.log('构造函数方式 - 挂载插件');
  compiler.hooks.emit.tap('plugin-emit', (compilation) => {
    console.log('------')
    this.hookRunCount++
    console.log(compilation.outputOptions.filename + '目标文件经历了' + this.hookRunCount + '个 hook')
  });
  compiler.hooks.afterEmit.tap('plugin-afterEmit', (compilation) => {
    console.log('------')
    this.hookRunCount++
    console.log(compilation.outputOptions.filename + '目标文件经历了' + this.hookRunCount + '个 hook')
  });
  compiler.hooks.done.tap('plugin-done', (state) => {
    console.log('------')
    this.hookRunCount++
    console.log(state.compilation.outputOptions.filename + '目标文件经历了' + this.hookRunCount + '个 hook')
  });
};
const merge = require('webpack-merge');
const config = {
  mode: 'development',
  entry: {
    index: './src/index.js',
  },
  output: {
    library: 'LibraryName',
  },
  plugins: [
    new TestPlugin() // 配置插件
  ]
};
module.exports =  [
  // nodejs 配置
  merge(config, {
    output: {
      filename: 'node.main.js',
      libraryTarget: 'commonjs2',
    },
  }),
  // 浏览器端配置
  merge(config, {
    output: {
      filename: 'browser.main.js',
      libraryTarget: 'umd',
      umdNamedDefine: true
    },
    plugins: [
      new TestPlugin()
    ]
  })
];

按照这个配置运行完跟上面结果是一样的,正确的写法应该写在具体的单个配置里面。如下:

module.exports =  [
  // nodejs 配置
  merge(config, {
    output: {
      filename: 'node.main.js',
      libraryTarget: 'commonjs2',
    },
    plugins: [
      new TestPlugin() // 配置插件
    ]
  }),
  // 浏览器端配置
  merge(config, {
    output: {
      filename: 'browser.main.js',
      libraryTarget: 'umd',
      umdNamedDefine: true
    },
    plugins: [
      new TestPlugin() // 配置插件
    ]
  })
];

最后在运行一下

这次运行就是正确的

总结

  • 如果同一个插件实例里面使用了属性就需要按照上面最后一个例子中分开写,因为属性是对象定义、实例化类时是固定的。而方法是在运行时是一个新的上下文环境
  • 按照构造函数或者类在去实例化插件实例的方式写插件是风险比较小的,最可能忽略的地方就是多配置的场景
  • 使用构造函数或者类的方式编写插件不是必须要这么写的