上一篇文章我们实现了自己的 loader
,这篇来实现 plugin
什么是 plugin
与 loader
相比,plugin
功能更强大,更灵活
插件向第三方开发者提供了webpack
引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到webpack
构建流程中。
loader
和 plugin
的区别
-
loader
: 顾名思义,某种类型资源文件的加载器,作用于某种类型的文件上。webpack
本身也是不能直接打包这些非js
文件的,需要一个转化器即loader
。loader
本身是单一,简单的,不能将多个功能放在一个loader里。 -
plugin
:plugin
比loaders
更加先进一点,你可以扩展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
其实是一个构造函数(class
或 function
)。为了能够获得 compiler
,需要 plugin
对外暴露一个 apply
接口,这个 apply
函数在构造函数的 prototype
上。
webpack
插件由以下组成:
- 一个
JavaScript
命名函数。- 在插件函数的
prototype
上定义一个apply
方法。- 指定一个绑定到
webpack
自身的事件钩子。- 处理
webpack
内部实例的特定数据。- 功能完成后调用
webpack
提供的回调。
Compiler 和 Compilation
在插件开发中最重要的两个资源就是 compiler
和 compilation
对象。理解它们的角色是扩展 webpack
引擎重要的第一步。
-
compiler
对象代表了完整的webpack
环境配置。这个对象在启动webpack
时被一次性建立,并配置好所有可操作的设置,包括options
,loader
和plugin
。当在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
走一波,可以看到输出结果
说明我们的插件已经成功运行了,大家也可自行将 compiler
打印出来看看。我们再看涉及到 compiler
和 compilation
的例子
// 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
官方提供的一款插件,可以在打包后的每个文件上面加上说明信息,像是这样子的
当然官方提供的功能更丰富一些,打包时还可以加上文件更多诸如 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
可以看到结果了
打包出来的文件也有说明信息
完整代码如下
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
完了!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。