前言
webpack 自身会提供一些基础插件,比如压缩、生成 html 文件、预加载等。有时,我们需要利用 webpack 提供的不同阶段钩子来做一些定制化功能的插件,以满足我们的业务需求,比如代码检查、打包后的文件处理(添加、删除、更改)。下面介绍如何自定义 webpack 插件。
插件基本结构
自定义插件大概包含以下几个步骤:
- webpack 插件其实就是一个构造函数,所以要先定义一个类函数;
function TestPlugin() {}
- 在构造函数的原型上定义 apply 方法,在安装插件时,apply 方法会被 Webpack compiler 调用。apply 方法可以接收一个 Webpack compiler 对象的引用;
TestPlugin.prototype.apply = function(compiler) {}
- 通过 compiler 对象,可以插入指定的事件钩子;
- 在钩子回调中,可以拿到 compilation 对象,使用 compilation 操纵修改 webpack 内部实例数据,其也提供了事件回调钩子;
- 实现功能后,调用 Webpack 提供的 callback
那 compiler 对象和 compilation 对象是什么呢?
- compiler 对象,包含了 webpack 的所有配置信息(webpack.config.js),包括 options,loader 和 plugin。该对象在启动 Webpac时被创建
- compilation 对象,代表一次资源版本的构建,包含了当前的模块资源、编译生成的资源、变化的文件以及依赖等信息。文件发生变化时,都会创建一个新的 compilation 对象,从而生成一组新的编译资源。compilation 对象也提供了许多事件回调钩子
compiler 钩子
钩子的用法
1 2 3 4 5 6 7 8 9
|
compiler.hooks.someHook.tap('MyPlugin', (res) => { })
compiler.plugin(someHook, (res) => { })
|
常用钩子介绍
- entryOption:在 webpack 选项中的 entry 被处理过之后调用
- afterPlugins:在初始化内部插件集合完成设置之后调用,回调参数 context 和 entry
- compilation:compilation 创建之后,输出 asset 之前执行。回调参数:compilation
- emit:输出 asset 到 output 目录之前执行。回调参数:compilation
- afterEmit:输出 asset 到 output 目录之后执行。回调参数:compilation
- done:在 compilation 完成时执行。回调参数:stats
全部 compiler 钩子用法请见 compiler 钩子
compilation 钩子
钩子的用法
1 2 3 4 5 6 7 8 9
|
compilation.hooks.someHook.tap('MyPlugin', (res) => { })
compilation.plugin(someHook, (res) => { })
|
常用钩子介绍
- buildModule:在模块构建开始之前触发,可以用来修改模块。
- rebuildModule:在重新构建一个模块之前触发。
- finishModules:所有模块都完成构建并且没有错误时执行。
- seal:compilation 对象停止接收新的模块时触发,不再接收任何模块,进入编译封闭阶段
- additionalAssets:为 compilation 创建额外 asset,可以加入一些自定义资源
全部 compilation 钩子用法请见 compilation 钩子
如何写插件
插件代码
1 2 3 4 5 6 7 8 9 10
| class MyWebpackPlugin { constructor(options) {} apply(compiler) { // 插入钩子函数,里面加入 compiler.hooks.emit.tap('MyWebpackPlugin', (compilation) => { console.log('Hello World!') }) } } module.exports = MyWebpackPlugin;
|
使用插件
在 webpack.config.js 中引入插件:
1 2 3 4 5 6
| module.exports = { plugins:[ new MyWebpackPlugin() ] }
|
插件示例
下面介绍的自定义的TestBuildPlugin插件,该插件是代码打包检查工具,通过一些简单的配置,在打包的过程中检查,检查webpack的配置项是否正确,打包后的代码是否符合要求。
TestBuildPlugin 插件代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
| const path = require('path') const resolvePath = dir => path.join(path.resolve('./'), dir) const testBuildConfig = require(resolvePath('.testBuildConfig.js')) || {}
class TestBuildPlugin { constructor (args) {} apply (compiler) { const { testBuild } = process.env || {} if (testBuild === '1') { if (!this.testConfigValCorrect(compiler.options, testBuildConfig.options)) { throw new Error('options config err!') } if (compiler.hooks) { compiler.hooks.emit.tapAsync('TestBuildPlugin', this.emitFn.bind(this)) } else { compiler.plugin('emit', this.emitFn.bind(this)) } if (compiler.hooks) { compiler.hooks.done.tap('TestBuildPlugin', this.doneFn.bind(this)) } else { compiler.plugin('done', this.doneFn.bind(this)) } } } emitFn(compilation, callback) { const assets = compilation.assets || {} const assetsArr = Object.keys(assets) if (assets && assetsArr && assetsArr.length) { const { mustHave, mustForbidden } = testBuildConfig.codeRules assetsArr.forEach((key) => { const asset = assets[key] const source = asset.source() const pathName = key const mustForbiddenRes = this.testCodeCorrect({ codeRule: mustForbidden, type: 1, name: pathName, source }) if (!mustForbiddenRes.isCorrect) { callback(new Error(`${mustForbiddenRes.errItem} must be forbidden\nError detail: ${mustForbiddenRes.errItem} occurred in ${pathName} `)) } const mustHaveRes = this.testCodeCorrect({ codeRule: mustHave, type: 2, name: pathName, source }) if (!mustHaveRes.isCorrect) { callback(new Error(`${mustHaveRes.errItem} must be have\nError detail: err occurred in ${pathName} `)) } }) } else { callback(new Error(`build err!`)) } callback() } doneFn() { console.log('test build passed!!') } testConfigValCorrect (buildVal, configVal) { if (!configVal) { return true } if (!buildVal) { return false } let correct = true if (this.getValType(configVal) === '[object Array]') { if (this.getValType(buildVal) !== '[object Array]') { return false } configVal.every((item, index) => { return correct = this.testConfigValCorrect(buildVal[index], configVal[index]) }) } else if (this.getValType(configVal) === '[object Object]') { if (this.getValType(buildVal) !== '[object Object]') { return false } Object.keys(configVal) .every((key) => { return correct = this.testConfigValCorrect(buildVal[key], configVal[key]) }) } else { return buildVal === configVal } return !!correct }
testCodeCorrect ({ codeRule, type, name, source }) { let errItem = null let result = { isCorrect: true } if (codeRule && codeRule.length) { codeRule.every((codeRuleItem) => { if (codeRuleItem) { let { test, val } = codeRuleItem test = test ? this.getRegExpTypeVal(test) : /\.(js|html)$/ val = val ? this.getArrayTypeVal(val) : null if (test.test(name)) { if (val && val.length) { val.every(valItem => { const itemRegExp = this.getRegExpTypeVal(valItem) if (itemRegExp) { if ((itemRegExp.test(source) && type === 1) || (!itemRegExp.test(source) && type === 2) ) { errItem = valItem return false } } }) } } } return !errItem }) } if (errItem) { result = { isCorrect: false, errItem } } return result }
getValType (val) { if (!val) { return null } return Object.prototype.toString.call(val) }
getRegExpTypeVal (val) { if (this.getValType(val) !== '[object RegExp]') { return new RegExp(val) } return val }
getArrayTypeVal (val) { return this.getValType(val) === '[object Array]' ? val : [val] } }
module.exports = TestBuildPlugin
|
TestBuildPlugin 插件的使用
安装
1
| npm install --save-dev test-build-plugin
|
使用
1)在项目的根目录中建立.testBuildConfig.js
2)在.testBuildConfig.js配置打包时的检查项
- options:webpack 打包配置项,用来检查打包的入口和出口等配置正确;
- codeRules.mustHave:用来配置必须有的代码,test: 检测的范围;val: 代码;
- 用来配置必须禁止的代码,test: 检测的范围;val: 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const path = require('path') const resolvePath = dir => path.join(__dirname, dir) module.exports = { options: { entry: { app: '' }, output: { path: resolvePath(''), publicPath: '' } }, codeRules: { mustHave: [{ test: /index\.html$/, val: ['必须有的代码'] }], mustForbidden: [{ test: /\.(js|html)$/, val: ['beta-api.m.jd.com'] }] } }
|
3)项目中引用
1 2 3 4 5
| const TestBuildPlugin = require('test-build-plugin')
config.plugins.push( new TestBuildPlugin() )
|
4)在打包指令中添加testBuild=1,比如:
1 2 3
| "scripts": { "build:testBuild": "testBuild=1 node build/build.js" }
|