你盼世界,我盼望你无bug
。Hello 你们好!我是霖呆呆!javascript
有不少小伙伴在打算学写一个webpack
插件的时候,就被官网上那一长条一长条的API
给吓到了,亦或者翻阅了几篇文章以后但仍是不知道从何下手。css
而呆呆认为,当你了解了整个插件的建立方式以及执行机制以后,那些个长条的API
就只是你后期用来开发的"工具库"
而已,我须要什么,我就去文档上找,大可没必要以为它有多难 😊。html
本篇文章会教你们从浅到深的实现一个个webpack
插件,案例虽然都不是什么特别难的插件,可是一旦你掌握了如何写一个插件的方法以后,剩下的就只是在上面作增量了。呆呆仍是那句话:"授人予鱼不如授人予渔"
。前端
OK👌,让咱们来看看经过阅读本篇文章你能够学习到:java
No1-webpack-plugin
案例Tapable
compiler?compile?compilation?
No2-webpack-plugin
案例fileList.md
案例Watch-plugin
案例Decide-html-plugin
案例Clean-plugin
案例全部文章内容都已整理至 LinDaiDai/niubility-coding-js 快来给我Star呀😊~node
此系列记录了我在webpack
上的学习历程。若是你也和我同样想要好好的掌握webpack
,那么我认为它对你是有必定帮助的,由于教材中是以一名webpack
小白的身份进行讲解, 案例demo
也都很详细, 涉及到:webpack
建议先mark
再花时间来看。git
(其实这个系列在很早以前就写了,一直没有发出来,当时还写了一大长串前言可把我感动的,想看废话的能够点这里:GitHub地址,不过如今让咱们正式开始学习吧)github
全部文章webpack
版本号^4.41.5
, webpack-cli
版本号^3.3.10
。web
(本章节教材案例GitHub地址: LinDaiDai/webpack-example/tree/webpack-custom-plugin ⚠️:请仔细查看README说明)
好了,我已经准备好阅读呆呆的这篇文章而后写一个炒鸡牛x的插件了,赶忙的。
额,等等,在这以前咱们不是得知道须要怎么去作吗?咱们老是听到的插件插件的,它究竟是个啥啊?
对象?函数?类?
小伙伴们不妨结合咱们已经用过的一些插件来猜猜,好比HtmlWebpackPlugin
,咱们会这样使用它:
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
})
]
}
复制代码
能够看到,这很明显的就是个构造函数,或者是一个类嘛。咱们使用new
就能够实例化一个插件的对象。而且,这个函数或者类是可让咱们传递参数进去的。
那你脑子里是否是已经脑补出一个轮廓了呢?
function CustomPlugin (options) {}
// or
class CustomPlugin {
constructor (options) {}
}
复制代码
知道了plugin
大概的轮廓,让咱们从构建的角度来看看它。插件不一样于loader
一个很大的区别就是,loader
它是一个转换器,它只专一于转换这一个领域,例如babel-loader
能将ES6+
的代码转换为ES5
或如下,以此来保证兼容性,那么它是运行在打包以前的。
而plugin
呢?你会发现市场上有各类让人眼花缭乱的插件,它可能运行在打包以前,也可能运行在打包的过程当中,或者打包完成以后。总之,它不局限于打包,资源的加载,还有其它的功能。因此它是在整个编译周期都起做用。
那么若是让咱们站在一个编写插件者的角度上来看的话,是否是在编写的时候须要明确两件事情:
webpack
环境配置呢?由于我在编写插件的时候确定是要与webpack
的主环境结合起来的webpack
个人插件是在何时发挥做用呢?在打包以前?仍是以后?也就是咱们常常听到的钩子。因此这时候咱们就得清楚这几个硬知识点:
(看不懂?问题不大,呆呆也是从官网cv过来的,不事后面会详细讲到它们哦)
compiler
对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好全部可操做的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可使用它来访问 webpack 的主环境。
compilation
对象表明了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了不少关键时机的回调,以供插件作自定义处理时选择使用。
钩子的本质其实就是事件
老规矩,为了能更好的让咱们掌握本章的内容,咱们须要本地建立一个案例来进行讲解。
建立项目的这个过程我就快速的用指令来实现一下哈:
mkdir webpack-custom-plugin && cd webpack-custom-plugin
npm init -y
cnpm i webpack webpack-cli clean-webpack-plugin html-webpack-plugin --save-dev
touch webpack.config.js
mkdir src && cd src
touch index.js
复制代码
(mkdir
:建立一个文件夹;touch
:建立一个文件)
OK👌,此时项目目录变成了:
webpack-custom-plugin
|- package.json
|- webpack.config.js
|- /src
|- index.js
复制代码
接着让咱们给src/index.js
随便加点东西意思一下,免得太空了:
src/index.js
function createElement () {
const element = document.createElement('div')
element.innerHTML = '孔子曰:中午不睡,下午崩溃!孟子曰:孔子说的对!';
return element
}
document.body.appendChild(createElement())
复制代码
webpack.config.js
也简单的来配置一下吧,这些应该都是基础了,以前有详细说过了哟:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin()
]
}
复制代码
(clean-webpack-plugin
插件会在咱们每次打包以前自动清理掉旧的dist
文件夹,对这些内容还不熟悉的小伙伴得再看看这篇文章了:跟着"呆妹"来学webpack(基础篇))
另外还须要在package.json
中配置一条打包指令哈:
{
"script": {
"build": "webpack --mode development"
}
}
复制代码
这里的"webpack"
其实是"webpack --config webpack.config.js"
的缩写,这点在基础篇中也有说到咯。
--mode development
就是指定一下环境为开发环境,由于咱们后续可能有须要看到打包以后的代码内容,若是指定了为production
的话,那么webpack
它会自动开启UglifyJS
的也就是会对咱们打包成功以后的代码进行压缩输出,那一坨一坨的代码咱们就不利于咱们查看了。
好的了,基本工做已经准备完毕了,让咱们动手来编写咱们的第一个插件吧。
这个插件案例主要是为了帮助你了解插件大概的建立流程。
从易到难,让咱们来实现这么一个简单的功能:
"good boy!"
在刚刚的案例目录中新建一个plugins
文件夹,而后在里面建立上咱们的第一个插件: No1-webpack-plugin
:
webpack-custom-plugin
|- package.json
|- webpack.config.js
|- /src
|- index.js
+ |- /plugins
+ |-No1-webpack-plugin.js
复制代码
如今依照前面所说的插件的结构,以及咱们的需求,能够写出如下代码:
plugins/No1-webpack-plugin.js:
// 1. 建立一个构造函数
function No1WebpackPlugin (options) {
this.options = options
}
// 2. 重写构造函数原型对象上的 apply 方法
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.plugin('done', () => {
console.log(this.options.msg)
})
}
// 3. 将咱们的自定义插件导出
module.exports = No1WebpackPlugin;
复制代码
接着,让咱们来看看如何使用它,也就是:
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+ const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin(),
+ new No1WebpackPlugin({ msg: 'good boy!' })
]
}
复制代码
OK👌,代码已经编写完啦,快npm run build
一下看看效果吧:
能够看到,控制台已经在夸你"good boy!"
了😄。
那么让咱们回到刚刚的那段自定义插件的代码中:
plugins/No1-webpack-plugin.js:
// 1. 建立一个构造函数
function No1WebpackPlugin (options) {
this.options = options
}
// 2. 在构造函数原型对象上定义一个 apply 方法
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.plugin('done', () => {
console.log(this.options.msg)
})
}
// 3. 将咱们的自定义插件导出
module.exports = No1WebpackPlugin;
复制代码
注意到这里,咱们一共是作了这么三件事情,也就是我在代码中的注释。
很显然,为了能拿到webpack.config.js
中咱们传递的那个参数,也就是{ msg: 'good boy!' }
,咱们须要在构造函数中定义一个实例对象上的属性options
。
而且在prototype.apply
中呢:
compiler.plugin()
并传入第一个参数来指定咱们的插件是发生在哪一个阶段,也就是这里的"done"
(一次编译完成以后,即打包完成以后);this
获取到的是咱们的实例对象,也就是为了能保证咱们拿到options
,并成功的打印出msg
。(若是对this
还不熟悉的小伙伴你该看看呆呆的这篇文章了:【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理))因此,如今你的思惟是否是已经很清晰了呢?咱们想要编写一个插件,只须要这么几步:
webpack.config.js
中的配置);compiler.plugin
注册咱们的自定义插件。那么除了用构造函数的方式来建立插件,是否也能够用类呢?让咱们一块儿来试试,将刚刚的代码改动一下:
plugins/No1-webpack-plugin.js:
// // 1. 建立一个构造函数
// function No1WebpackPlugin (options) {
// this.options = options
// }
// // 2. 重写构造函数原型对象上的 apply 方法
// No1WebpackPlugin.prototype.apply = function (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
class No1WebpackPlugin {
constructor (options) {
this.options = options
}
apply (compiler) {
compiler.plugin('done', () => {
console.log(this.options.msg)
})
}
}
// 3. 将咱们的自定义插件导出
module.exports = No1WebpackPlugin;
复制代码
这时候你执行打包指令效果也是同样的哈。这其实也很好理解,class
它不就是我们构造函数的一个语法糖吗,因此它确定也能够用来实现一个插件啦。
不过不知道小伙伴们注意到了,在咱们刚刚输出"good boy!"
的上面,还有一段小小的警告:
Tabable.plugin
这种的调用形式已经被废弃了,请使用新的
API
,也就是
.hooks
来替代
.plugin
这种形式。
若是你和呆呆同样,开始看的官方文档是 《编写一个插件》这里的话,那么如今请让咱们换个方向了戳这里了: 《Plugin API》。
但并非说上面的文档就不能看了,咱们依然仍是能够经过阅读它来了解更多插件相关的知识。
既然官方都推荐咱们用compiler.hooks
了,那咱们就遵循呗。不过若是你直接去看Plugin API的话对新手来讲好像又有点绕,里面的Tapable
、compiler
、compile
、compilation
它们直接究竟是存在怎样的关系呢?
不要紧,呆呆都会依次的进行讲解。
如今让咱们将No1-webpack-plugin
使用compiler.hooks
改造一下吧:
plugins/No1-webpack-plugin.js:
// 初版
// function No1WebpackPlugin (options) {
// this.options = options
// }
// No1WebpackPlugin.prototype.apply = function (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
// 第二版
// class No1WebpackPlugin {
// constructor (options) {
// this.options = options
// }
// apply (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
// }
// 第三版
function No1WebpackPlugin (options) {
this.options = options
}
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.done.tap('No1', () => {
console.log(this.options.msg)
})
}
module.exports = No1WebpackPlugin;
复制代码
能够看到,第三版中,关键点就是在于:
compiler.hooks.done.tap('No1', () => {
console.log(this.options.msg)
})
复制代码
它替换了咱们以前的:
compiler.plugin('done', () => {
console.log(this.options.msg)
})
复制代码
让咱们来拆分一下compiler.hooks.done.tap('No1', () => {})
:
compiler
:一个扩展至Tapable
的对象compiler.hooks
:compiler
对象上的一个属性,容许咱们使用不一样的钩子函数.done
:hooks
中经常使用的一种钩子,表示在一次编译完成后执行,它有一个回调参数stats
(暂时没用上).tap
:表示能够注册同步的钩子和异步的钩子,而在此处由于done
属于异步AsyncSeriesHook
类型的钩子,因此这里表示的是注册done
异步钩子。.tap('No1')
:tap()
的第一个参数'No1'
,其实tap()
这个方法它的第一个参数是能够容许接收一个字符串或者一个Tap类的对象的,不过在此处咱们不深究,你先随便传一个字符串就好了,我把它理解为此次调用钩子的方法名。因此让咱们连起来理解这段代码的意思就是:
new No1WebpackPlugin()
的时候,会初始化一个插件实例且调用其原型对象上的apply
方法webpack
当你在一次编译完成以后,得执行一下个人箭头函数里的内容,也就是打印出msg
如今咱们虽然会写一个简单的插件了,可是对于上面的一些对象、属性啥的好像还不是很懂耶。想要一口气吃完一头大象🐘是有点难的哦(并且那样也是犯法的),因此接下来让咱们来大概了解一下这些Tapable
、compiler
等等的东西是作什么的😊。
首先是Tapable
这个东西,我看了一下网上有不少对它的描述:
固然这些说法确定都是对的哈,因此总结一下:
其实若是你去看了它Git上的文档的话,它就是暴露了9个Hooks
类,以及3种方法(tap、tapAsync、tapPromise
),可用于为插件建立钩子。
9种Hooks
类与3种方法之间的关系:
Hooks
类表示的是你的钩子是哪种类型的,好比咱们上面用到的done
,它就属于AsyncSeriesHook
这个类tap、tapAsync、tapPromise
这三个方法是用于注入不一样类型的自定义构建行为,由于咱们的钩子可能有同步的钩子,也可能有异步的钩子,而咱们在注入钩子的时候就得选对这三种方法了。对于Hooks
类你大可没必要全都记下,通常来讲你只须要知道咱们要用的每种钩子它们其实是有类型区分的,而区分它们的就是Hooks
类。
若是你想要清楚它们以前的区别的话,呆呆这里也有找到一个解释的比较清楚的总结:
Sync*
Async*
(总结来源:XiaoLu-写一个简单webpack plugin所引起的思考)
而对于这三种方法,咱们必须得知道它们分别是作什么用的:
tap
:能够注册同步钩子也能够注册异步钩子tapAsync
:回调方式注册异步钩子tapPromise
:Promise
方式注册异步钩子OK👌,听了霖呆呆这段解释以后,我相信你起码能看得懂官方文档-compiler 钩子这里面的钩子是怎样用的了:
就好比,我如今想要注册一个compile
的钩子,根据官方文档,我发现它是SyncHook
类型的钩子,那么咱们就只能使用tap
来注册它。若是你试图用tapAsync
的话,打包的话你就会发现控制台已经报错了,好比这样:
(额,不过我在使用compiler.hooks.done.tapAsync()
的时候,查阅文档上它也是SyncHook
类,可是却能够用tapAsync
方法注册,这边呆呆也有点没搞明白是为何,有知道的小伙伴还但愿能够评论区留言呀😄)
接下来就得说一说插件中几个重要的东西了,也就是这一小节的标题里的这三个东西。
首先让咱们在官方的文档上找寻一下它们的足迹:
能够看到,这几个属性都长的好像啊,并且更过度的是,compilation
居然还有两个同名的,你这是给👴整真假美猴王呢?
那么呆呆这边就对这几个属性作一下说明。
首先对于文档左侧菜单上的compiler
钩子和compilation
钩子(也就是第一个和第四个)咱们在以后称它们为Compiler
和Compilation
好了,也是为了和compile
作区分,其实我认为你能够把"compiler钩子"
理解为"compiler的钩子"
,这样会更好一些。
Compiler
:是一个对象,该对象表明了完整的webpack
环境配置。整个webpack
在构建的时候,会先初始化参数也就是从配置文件(webpack.config.js
)和Shell
语句("build": "webpack --mode development"
)中去读取与合并参数,以后开始编译,也就是将最终获得的参数初始化这个Compiler
对象,而后再会加载全部配置的插件,执行该对象的run()
方法开始执行编译。所以咱们能够理解为它是webpack
的支柱引擎。Compilation
:也是一个对象,不过它表示的是某一个模块的资源、编译生成的资源、变化的文件等等,由于咱们知道咱们在使用webpack
进行构建的时候多是会生成不少不一样的模块的,而它的颗粒度就是在每个模块上。因此你如今能够看到它俩的区别了,一个是表明了整个构建的过程,一个是表明构建过程当中的某个模块。
还有很重要的一点,它们两都是扩展至咱们上面👆提到的Tapable
类,这也就是为何它两都能有这么多生命周期钩子的缘由。
再来看看两个小写的compile和compilation
,这两个其实就是Compiler
对象下的两个钩子了,也就是咱们能够经过这样的方式来调用它们:
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.compile.tap('No1', () => {
console.log(this.options.msg)
})
compiler.hooks.compilation.tap('No1', () => {
console.log(this.options.msg)
})
}
复制代码
区别在于:
compile
:一个新的编译(compilation)建立以后,钩入(hook into) compiler。compilation
:编译(compilation)建立以后,执行插件。(为何感受仍是没太读懂它们的意思呢?别急,呆呆会在下个例子中来进行说明的)
这个插件案例主要是为了帮你理解Compiler、Compilation、compile、compilation
四者之间的关系。
仍是在上面👆那个项目中,让咱们在plugins
文件夹下再新增一个插件,叫作No2-webpack-plugin
:
plugins/No2-webpack-plugin.js:
function No2WebpackPlugin (options) {
this.options = options
}
No2WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.compile.tap('No2', () => {
console.log('compile')
})
compiler.hooks.compilation.tap('No2', () => {
console.log('compilation')
})
}
module.exports = No2WebpackPlugin;
复制代码
在这个插件中,我分别调用了compile
和compilation
两个钩子函数,等会让咱们看看会发生什么事情。
同时,把webpack.config.js
中的No1
插件替换成No2
插件:
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
const No2WebpackPlugin = require('./plugins/No2-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin(),
// new No1WebpackPlugin({ msg: 'good boy!' })
new No2WebpackPlugin({ msg: 'bad boy!' })
]
}
复制代码
如今项目的目录结构是这样的:
webpack-custom-plugin
|- package.json
|- webpack.config.js
|- /src
|- index.js
|- /plugins
|-No1-webpack-plugin.js
+ |-No2-webpack-plugin.js
复制代码
OK👌,来执行npm run build
看看:
哈哈哈😄,是否是给了你点什么启发呢?
咱们最终生成的dist
文件夹下会有两个文件,那么compilation
这个钩子就被调用了两次,而compile
钩子就只被调用了一次。
有小伙伴可能就要问了,咱们这里的src
下明明就只有一个index.js
文件啊,为何最终的dist
下会有两个文件呢?
main.bundle.js
index.html
别忘了,在这个项目中咱们但是使用了html-webpack-plugin
这个插件的,它会帮我自动建立一个html
文件。
为了验证这个compilation
是跟着文件的数量走的,咱们暂时先把new HtmlWebpackPlugin
给去掉看看:
const path = require('path');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
const No2WebpackPlugin = require('./plugins/No2-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
// new HtmlWebpackPlugin({
// title: 'custom-plugin'
// }),
new CleanWebpackPlugin(),
// new No1WebpackPlugin({ msg: 'good boy!' })
new No2WebpackPlugin({ msg: 'bad boy!' })
]
}
复制代码
试试效果?
这时候,compilation
就只执行一次了,并且dist
中也没有再生成html
文件了。
(固然,我这里只是为了演示哈,在肯定完了以后,我又把html-webpack-plugin
给启用了)
想必上面两个钩子函数的区别你们应该都搞懂了吧,接下来就让咱们看看Compiler
和Compilation
这两个对象的区别。
经过查看官方文档,咱们发现,刚刚用到的compiler.hooks.compilation
这个钩子,是可以接收一个参数的:
貌似这个形参的名字就是叫作compilation
,它和Compilation
对象是否是有什么联系呢?或者说,它就是一个Compilation
?。
OK👌,我就假设它是吧,接下来我去查看了一下compilation钩子
,哇,这钩子的数量是有点多哈,随便挑个顺眼的来玩玩?额,翻到最下面,有个chunkAsset
,要不就它吧:
能够看到这个钩子函数是有两个参数的:
chunk
:表示的应该就是当前的模块吧filename
:模块的名称接着让咱们来改写一下No2-webpack-plugin
插件:
src/No2-webpack-plugin.js:
function No2WebpackPlugin (options) {
this.options = options
}
No2WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.compile.tap('No2', (compilation) => {
console.log('compile')
})
compiler.hooks.compilation.tap('No2', (compilation) => {
console.log('compilation')
+ compilation.hooks.chunkAsset.tap('No2', (chunk, filename) => {
+ console.log(chunk)
+ console.log(filename)
+ })
})
}
module.exports = No2WebpackPlugin;
复制代码
咱们作了这么几件事:
Compiler
的compilation
钩子函数中,获取到Compilation
对象Compilation
对象调用它的chunkAsset
钩子chunkAsset
钩子是一个SyncHook
类型的钩子,因此只能用tap
去调用若是和咱们猜想的同样,每一个Compilation
对象都对应着一个输出资源的话,那么当咱们执行npm run build
以后,控制台确定会打印出两个chunk
以及两个filename
。
一个是index.html
,一个是main.bundle.js
。
OK👌,来瞅瞅。
如今看看你的控制台是否是打印出了一大长串呢?呆呆这里简写一下输出结果:
'compile'
'compilation'
'compilation'
Chunk {
id: 'HtmlWebpackPlugin_0',
...
}
'__child-HtmlWebpackPlugin_0'
Chunk {
id: 'main',
...
}
'main.bundle.js'
复制代码
能够看到,确实是有两个Chunk
对象,还有两个文件名称。
只不过index.html
不是按照咱们预期的输出为"index.html"
,而是输出为了__child-HtmlWebpackPlugin_0
,这点呆呆猜想是html-webpack-plugin
插件自己作了一些处理吧。
若是你们把这两个对象打印在控制台上的话会发现有一大长串,呆呆这边找到了一份比较全面的对象属性的清单,你们能够看一下:
(图片与总结来源:编写一个本身的webpack插件plugin)
Compiler 对象包含了 Webpack 环境全部的的配置信息,包含 options
,hook
,loaders
,plugins
这些信息,这个对象在 Webpack
启动时候被实例化,它是全局惟一的,能够简单地把它理解为 Webpack
实例;Compiler
中包含的东西以下所示:
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack
以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation
将被建立。Compilation
对象也提供了不少事件回调供插件作扩展。经过 Compilation
也能读取到 Compiler
对象。
好了,看到这里我相信你已经掌握了一个webpack
插件的基本开发方式了。这个东西咋说呢,只有本身去多试试,多玩玩上手才能快,下面呆呆也会为你们演示一些稍微复杂一些的插件的开发案例。能够跟着一块儿来玩玩呀。
唔...看了网上挺多这个fileList.md
案例的,要不咱也给整一个?
它的功能点其实很简单:
webpack
打包以后,自动产生一个打包文件清单,实际上就是一个markdown
文件,上面记录了打包以后的文件夹dist
里全部的文件的一些信息。你们在接收到这个需求的时候,能够先想一想要如何去实现:
markdown
文件并塞到dist
里markdown
文件内的内容是长什么样的针对第一点,我认为咱们能够传递一个最终生成的文件名进去,例如这样调用:
module.exports = {
new FileListPlugin({
filename: 'fileList.md'
})
}
复制代码
第二点,由于是在打包完成以前,因此咱们能够去compiler 钩子来查查有没有什么能够用的。
咦~这个叫作emit
的好像挺符合的:
AsyncSeriesHook
output
目录以前。compilation
第三点的话,难道要弄个node
的fs
?再建立个文件之类的?唔...不用搞的那么复杂,等会让咱们看个简单点的方式。
第四点,咱们就简单点,例如写入这样的内容就能够了:
# 一共有2个文件
- main.bundle.js
- index.html
复制代码
因为功能也并不算很复杂,呆呆这里就直接上代码了,而后再来一步一步解析。
仍是基于刚刚的案例,让咱们继续在plugins
文件夹下建立一个新的插件:
plugins/File-list-plugin.js:
function FileListPlugin (options) {
this.options = options || {};
this.filename = this.options.filename || 'fileList.md'
}
FileListPlugin.prototype.apply = function (compiler) {
// 1.
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
// 2.
const fileListName = this.filename;
// 3.
let len = Object.keys(compilation.assets).length;
// 4.
let content = `# 一共有${len}个文件\n\n`;
// 5.
for (let filename in compilation.assets) {
content += `- ${filename}\n`
}
// 6.
compilation.assets[fileListName] = {
// 7.
source: function () {
return content;
},
// 8.
size: function () {
return content.length;
}
}
// 9.
cb();
})
}
module.exports = FileListPlugin;
复制代码
代码分析:
compiler.hooks.emit.tapAsync()
来触发生成资源到output
目录以前的钩子,且回调函数会有两个参数,一个是compilation
,一个是cb
回调函数markdown
文件的名称compilation.assets
获取到全部待生成的文件,这里是获取它的长度markdown
文件的内容,也就是先定义一个一级标题,\n
表示的是换行符markdown
文件内dist
文件夹里添加一个新的资源,资源的名称就是fileListName
变量webpack
展现tapAsync
异步调用,因此必须执行一个回调函数cb
,不然打包后就只会建立一个空的dist
文件夹。好滴,大功告成,让咱们赶忙来试试这个新插件吧,修改webpack.config.js
的配置:
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
// const No2WebpackPlugin = require('./plugins/No2-webpack-plugin');
const FileListPlugin = require('./plugins/File-list-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin(),
// new No1WebpackPlugin({ msg: 'good boy!' })
// new No2WebpackPlugin({ msg: 'bad boy!' })
new FileListPlugin()
]
}
复制代码
来执行一下npm run build
看看吧:
能够看到,上面👆的案例咱们是使用tapAsync
来调用钩子函数,这个tapPromise
好像尚未玩过,唔...咱们看看它是怎样用的。
如今让咱们来改下需求,刚刚咱们好像看不太出来是异步执行的。如今咱们改成1s
后才输出资源。
重写一下刚刚的插件:
plugins/File-list-plugin.js:
function FileListPlugin (options) {
this.options = options || {};
this.filename = this.options.filename || 'fileList.md'
}
FileListPlugin.prototype.apply = function (compiler) {
// 第二种 Promise
compiler.hooks.emit.tapPromise('FileListPlugin', compilation => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
const fileListName = this.filename;
let len = Object.keys(compilation.assets).length;
let content = `# 一共有${len}个文件\n\n`;
for (let filename in compilation.assets) {
content += `- ${filename}\n`;
}
compilation.assets[fileListName] = {
source: function () {
return content;
},
size: function () {
return content.length;
}
}
})
})
}
module.exports = FileListPlugin;
复制代码
能够看到它与第一种tapAsync
写法的区别了:
compilation
,不须要再调用一下cb()
Promise
,这个Promise
在1s
后才resolve()
。你们能够本身写写看看效果,应该是和咱们预期的同样的。
另外,tapPromise
还容许咱们使用async/await
的方式,好比这样:
function FileListPlugin (options) {
this.options = options || {};
this.filename = this.options.filename || 'fileList.md'
}
FileListPlugin.prototype.apply = function (compiler) {
// 第三种 await/async
compiler.hooks.emit.tapPromise('FileListPlugin', async (compilation) => {
await new Promise(resolve => {
setTimeout(() => {
resolve()
}, 1000)
})
const fileListName = this.filename;
let len = Object.keys(compilation.assets).length;
let content = `# 一共有${len}个文件\n\n`;
for (let filename in compilation.assets) {
content += `- ${filename}\n`;
}
compilation.assets[fileListName] = {
source: function () {
return content;
},
size: function () {
return content.length;
}
}
})
}
module.exports = FileListPlugin;
复制代码
嘻嘻😁,貌似真的也不难。
话很少说,让咱们接着来看一个监听的案例。需求以下:
watch
模式的时候,监听每一次资源的改动"本次监听中止了哟~"
那么首先为了知足第一个条件,咱们得设计一条watch
的指令,以保证使用npm run watch
命令以后,会看到编译过程,可是不会退出命令行,而是实时监控文件。这也很简单,加一条脚本命令就能够了。
呆呆在霖呆呆向你发起了多人学习webpack-构建方式篇(2)中也有说的很详细了。
package.json:
{
"script": "webpack --watch --mode development"
}
复制代码
而后想想咱们的插件该如何设计,这时候就要知道咱们须要调用哪一个钩子函数了。
去官网上看一看,这个watchRun
就很符合呀:
AsyncSeriesHook
compiler
针对第三点,监听结束以后,watchClose
就能够了:
SyncHook
好的👌,让咱们开干吧。在此项目的plugins
文件夹下再新建一个叫作Watch-plugin
的插件。
先搭一下插件的架子吧:
plugins/Watch-plugin.js:
function WatcherPlugin (options) {
this.options = options || {};
}
WatcherPlugin.prototype.apply = function (compiler) {
compiler.hooks.watchRun.tapAsync('WatcherPlugin', (compiler, cb) => {
console.log('我但是时刻监听着的 🚀🚀🚀')
console.log(compiler)
cb()
})
compiler.hooks.watchClose.tap('WatcherPlugin', () => {
console.log('本次监听中止了哟~👋👋👋')
})
}
module.exports = WatcherPlugin;
复制代码
(额,这个火箭🚀呆呆是用Mac自带的输入法打出来的,其它输入法应该也有吧)
经过上面几个案例的讲解,这段代码你们应该都没有什么疑问了吧。
那么如今的问题就是如何知道哪些文件改变了。其实咱们在研究一个新东西的时候,若是没啥思路,不如就在已有的条件上先找一下,好比这里咱们就只知道一个compiler
,那么咱们就能够查找一下它里面的属性,看看有什么是咱们能用的吗。
也就是上面的这张图:
能够看到,有一个叫作watchFileSystem
的属性应该就是咱们想要的监听文件的属性了,打印出来看看?
好滴👌,那就先让我启动这个插件吧,也就是改一下webpack.config.js
那里的配置,因为上面几个案例都已经演示过了,呆呆这里就再也不累赘,直接跳过讲解这一步了。
直接让咱们来npm run watch
一下吧,控制台已经输出了它,但是因为咱们是须要监听文件的改变,因此虽然控制台输出了watchFileSystem
,可是这一次是初始化时打印的,也就是说咱们须要改动一下本地的一个资源而后保存再来看看效果。
例如,我随便改动一下src/index.js
中的内容而后保存。这时候就触发了监听事件了,让咱们来看一下打印的结果:
能够看到watchFileSystem
中确实有一个watch
属性,并且里面有一个fileWatchers
的列表,还有一个mtimes
对象。这两个属性引发了个人注意。貌似mtimes
对象就是咱们想要的了。
它是一个键值对,键名为改动的文件的路径,值为时间。
那么咱们就能够直接来获取它了:
function WatcherPlugin (options) {
this.options = options || {};
}
WatcherPlugin.prototype.apply = function (compiler) {
compiler.hooks.watchRun.tapAsync('WatcherPlugin', (compiler, cb) => {
console.log('我但是时刻监听着的 🚀🚀🚀')
let mtimes = compiler.watchFileSystem.watcher.mtimes;
let mtimesKeys = Object.keys(mtimes);
if (mtimesKeys.length > 0) {
console.log(`本次一共改动了${mtimesKeys.length}个文件,目录为:`)
console.log(mtimesKeys)
console.log('------------分割线-------------')
}
cb()
})
compiler.hooks.watchClose.tap('WatcherPlugin', () => {
console.log('本次监听中止了哟~👋👋👋')
})
}
module.exports = WatcherPlugin;
复制代码
好滴,接着:
npm run watch
src/index.js
,随便加个注释src/index.js
文件,打印结果以下:好滴👌,这样就实现了一个简单的文件监听功能。不过使用mtimes
只能获取到简单的文件的路径和修改时间。若是要获取更加详细的信息可使用compiler.watchFileSystem.watcher.fileWatchers
,可是我试了一下这里面的数组是会把node_modules
里的改变也算上的,例如这样:
因此若是针对于这道题的话,咱们能够写一个正则小小的判断一下,去除node_modules
文件夹里的改变,代码以下:
function WatcherPlugin (options) {
this.options = options || {};
}
WatcherPlugin.prototype.apply = function (compiler) {
compiler.hooks.watchRun.tapAsync('WatcherPlugin', (compiler, cb) => {
console.log('我但是时刻监听着的 🚀🚀🚀')
// let mtimes = compiler.watchFileSystem.watcher.mtimes;
// let mtimesKeys = Object.keys(mtimes);
// if (mtimesKeys.length > 0) {
// console.log(`本次一共改动了${mtimesKeys.length}个文件,目录为:`)
// console.log(mtimesKeys)
// console.log('------------分割线-------------')
// }
const fileWatchers = compiler.watchFileSystem.watcher.fileWatchers;
console.log(fileWatchers)
let paths = fileWatchers.map(watcher => watcher.path).filter(path => !/(node_modules)/.test(path))
if (paths.length > 0) {
console.log(`本次一共改动了${paths.length}个文件,目录为:`)
console.log(paths)
console.log('------------分割线-------------')
}
cb()
})
compiler.hooks.watchClose.tap('WatcherPlugin', () => {
console.log('本次监听中止了哟~👋👋👋')
})
}
module.exports = WatcherPlugin;
复制代码
另外呆呆在读 《深刻浅出Webpack》的时候,里面也有提到:
默认状况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些状况下项目可能须要引入新的文件,例如引入一个 HTML 文件。 因为 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会从新触发新的 Compilation。 为了监听 HTML 文件的变化,咱们须要把 HTML 文件加入到依赖列表中,为此可使用以下代码:
compiler.plugin('after-compile', (compilation, callback) => {
// 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时从新启动一次编译
compilation.fileDependencies.push(filePath);
callback();
})
复制代码
感兴趣的小伙伴能够本身去实验一下,呆呆这里就不作演示了。
再来看个案例,这个插件是用来检测咱们有没有使用html-webpack-plugin
插件的。
还记得咱们前面说的Compiler
对象中,包含了 Webpack 环境全部的的配置信息,包含 options
,hook
,loaders
,plugins
这些信息。
那么这样我就能够经过plugins
来判断是否使用了html-webpack-plugin
了。
因为功能不复杂,呆呆这就直接上代码了:
function DecideHtmlPlugin () {}
DecideHtmlPlugin.prototype.apply = function (compiler) {
compiler.hooks.afterPlugins.tap('DecideHtmlPlugin', compiler => {
const plugins = compiler.options.plugins;
const hasHtmlPlugin = plugins.some(plugin => {
return plugin.__proto__.constructor.name === 'HtmlWebpackPlugin'
})
if (hasHtmlPlugin) {
console.log('使用了html-webpack-plugin')
}
})
}
module.exports = DecideHtmlPlugin
复制代码
有须要注意的点⚠️:
afterPlugins
:设置完初始插件以后,执行插件。plugins
拿到的会是一个插件列表,包括咱们的自定义插件DecideHtmlPlugin
也会在里面some()
是Array.prototype
上的方法,用于判断某个数组是否有符合条件的项,只要有一项知足就返回true
,不然返回false
配置一下webpack.config.js
,来看看效果是能够的:
还记得上面👆的项目咱们用到的那个clean-webpack-plugin
,如今咱们本身来实现一个简易版的clean-webpack-plugin
吧,名称就叫Clean-plugin
。
同样的,首先仍是明确一下咱们的需求:
咱们须要设计这么一个插件,在每次从新编译以后,都会自动清理掉上一次残余的dist
文件夹中的内容,不过须要知足如下需求:
options
中有一个属性为exclude
,为一个数组,用来定义不须要清除的文件列表hash
命名)例如我第一次打包以后,生成的dist
目录结构是这样的:
/dist
|- main.f89e7ffee29ee9dbf0de.js
|- main.f97284d8479b13c49723.css
复制代码
而后我修改了一下js
文件并从新编译,新的目录结构应该是这样的:
/dist
|- main.e0c6be8f72d73a68f73a.js
|- main.f97284d8479b13c49723.css
复制代码
能够看到,若是咱们是用chunkhash
给输出文件命名的话,只改变js
文件,则js
文件的文件名会发生变化,而不会影响css
文件。
若是对三种hash
命名还不清楚的小伙伴,能够花上十分种看下个人这篇文章:霖呆呆的webpack之路-三种hash的区别,里面对三种hash
的使用场景以及区别都说的很清楚。
此时,咱们就须要将旧的js
文件给替换成新的,也就是只删除main.f89e7ffee29ee9dbf0de.js
文件。
而若是咱们在配置插件的时候加了exclude
属性的话,则不须要把这个属性中的文件给删除。例如若是我是这样配置的话:
module.exports = {
new CleanPlugin({
exclude: [
"main.f89e7ffee29ee9dbf0de.js"
]
})
}
复制代码
那么这时候就算你修改了js
文件,结果虽然会生成新的js
文件,可是也不会把旧的给删除,而是共存:
/dist
|- main.f89e7ffee29ee9dbf0de.js
|- main.e0c6be8f72d73a68f73a.js
|- main.f97284d8479b13c49723.css
复制代码
因此针对于上面这个需求,咱们先给本身几个灵魂拷问:
dist
文件夹中的全部文件options.exclude
中的文件名称,并合并为一个无重复项的数组(在这个过程当中咱们确定会碰到不少本身不知道的知识点,请不要慌,你们都是有这么一个不会到会的过程)
问题一
在哪一个钩子函数中执行,我以为能够在"done"
中,由于咱们其中的一个目的就是既能拿到旧的文件夹内容,又能拿到新的。而在这个阶段,表示已经编译完成了,因此是能够拿到最新的资源了。
问题二
获取旧的dist
文件夹内的内容。还记得咱们的dist
文件夹是怎么来的吗?它是在咱们webpack.config.js
这个文件中配置的output
项:
module.exports = {
output: {
path: path.resolve(__dirname, 'dist')
}
}
复制代码
因此很轻松的咱们能够经过compiler.options.output.path
就拿到这个旧的输出路径了,而后咱们须要去读取这个路径文件夹下的全部文件,也就是遍历dist
文件夹。
这边咱们须要用到一个叫recursive-readdir-sync
的东西,稍后咱们须要安装它,它的做用就是以递归方式同步读取目录路径的内容。(github地址为:github.com/battlejj/re…)
问题三
获取新生成的全部文件,也就是全部的资源。这点得看"done"
回调函数中的参数stats
了。若是你把这个参数打印出来看的话会发现它包括了webpack
中的不少配置,包括options
包括assets
等等。而这里咱们就是须要获取打包完以后的全部最新资源也就是assets
属性。
你觉得直接stats.assets
获取就完了吗?若是你试图这样去作的话,就会报错了。在webpack
中它鼓励你用stats.toJson().assets
的方式来获取。这点呆呆也不是很清楚缘由,你们能够看一下这里:
而后至于options.exclude
中的文件名称,这个在插件的构造函数中定义一个options
属性就能够拿到了。
合并没有重复项咱们可使用lodash.union
方法,lodash它是一个高性能的 JavaScript 实用工具库,里面提供了许多的方法来使咱们更方便的操做数组、对象、字符串等。而这里的union
方法就是能把多个数组合并成一个无重复项的数组,例如🌰:
_.union([2], [1, 2]);
// => [2, 1]
复制代码
至于为何要把这两个数组组合起来呢?那也是为了保证exclude
中定义的文件在后面比较的过程当中不会被删除。
问题四
将新旧文件列表作对比,得出最终须要删除的文件列表。
唔...其实最难的点应该就是在这里了。由于这里并非简单的文件名称字符串匹配,它须要涉及到路径问题。
例如,咱们前面说到能够经过compiler.options.output.path
拿到文件的输出路径,也就是dist
的绝对路径,咱们命名为outputPath
,它多是长这样的:
/Users/lindaidai/codes/webpack/webpack-example/webpack-custom-plugin/dist
复制代码
然后咱们会用一个叫recursive-readdir-sync
的东西去处理这个绝对路径,获取里面的全部文件:
recursiveReadSync(outputPath)
复制代码
这里获得的会是各个文件:
[
file /Users/lindaidai/codes/webpack/webpack-example/webpack-custom-plugin/dist/main.f89e7ffee29ee9dbf0de.js,
file /Users/lindaidai/codes/webpack/webpack-example/webpack-custom-plugin/dist/css/main.124248e814cc2eeb1fd4.css
]
复制代码
以上获得的列表就是旧的dist
文件夹中的全部文件列表。
然后,咱们须要获得新生成的文件的列表,也就是stats.toJson().assets.map(file => file.name)
和exclude
合并后的那个文件列表,咱们称为newAssets
。可是这里须要注意的就是newAssets
中的是各个新生成的文件的名称,也就是这样:
[
"main.e0c6be8f72d73a68f73a.js",
"main.124248e814cc2eeb1fd4.css"
]
复制代码
因此咱们须要作一些额外的路径转换的处理,再来进行比较。
而若是在路径前缀相同的状况下,咱们只须要把recursiveReadSync(outputPath)
处理以后的结果作一层过滤,排除掉newAssets
里的内容,那么留下来的就是须要删除的文件,也就是unmatchFiles
这个数组。
有点绕?让咱们来写下伪代码:
const unmatchFiles = recursiveReadSync(outputPath).filter(file => {
// 这里与 newAssets 作对比
// 过滤掉存在 newAssets 中的文件
})
// unmatchFiles 就是为咱们须要清理的全部文件
复制代码
在这个匹配的过程当中,咱们会须要用到一个minimatch
的工具库,它很适合用来作这种文件路径的匹配。
github地址能够看这里:github.com/isaacs/mini…
问题五
在上一步中咱们会获得须要删除的文件列表,这时候只须要调用一下fs
模块中的unlinkSync
方法就能够删除了。
例如:
// 删除未匹配文件
unmatchFiles.forEach(fs.unlinkSync);
复制代码
好滴,分析了这么多,是时候动手来写一写了,仍是基于以前的那个案例。让咱们先来安装一下上面提到的一些模块或者工具:
cnpm i --save-dev recursive-readdir-sync minimatch lodash.union
复制代码
唔。而后为了能看到以后修改文件有没有删除掉旧的文件这个效果,咱们能够来写一些css
的样式,而后用MiniCssExtractPlugin
这个插件去提取出css
代码,这样打包以后就能够放到一个单独的css
文件中了。
关于这个插件,不清楚的小伙伴你就理解它为下面这个场景:
个人src
下有一个index.js
和一个style.css
,若是在index.js
中引用了style.css
的话:
import './style.css';
复制代码
最终的css
代码是会被打包进js
文件中的,webpack
并不会那么智能的把它拆成一个单独的css
文件。
因此这时候就能够用MiniCssExtractPlugin
这个插件来单独的提早css
。(不过这个插件的主要做用仍是为了提取公共的css
代码哈,在这里咱们只是为了将css
提取出来)
更多有关MiniCssExtractPlugin
的功能能够看个人这篇介绍:霖呆呆的webpack之路-优化篇
好滴,首先让咱们来安装它,顺便安装一下另两个loader
:
cnpm i --save-dev style-loader css-loader mini-css-extract-plugin
复制代码
而后在src
目录下新建一个style.css
文件,并写点样式:
src/style.css:
.color_red {
color: red;
}
.color_blue {
color: blue;
}
复制代码
接着快速来配置一下webpack.config.js
:
(这里面有用到一个CleanPlugin
的插件,它是咱们接下来要建立的文件)
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanPlugin = require('./plugins/Clean-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: [
'./src/index.js',
'./src/style.css'
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanPlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
}
}
复制代码
这里有一点前面也提到了,就是关于output.filename
的命名和MiniCssExtractPlugin
中生成css
文件的命名,咱们采用contenthash
的方式,这样的话,若是咱们只改变了js
文件的话,那么从新打包以后,就只有js
文件的hash
会被从新生成,而css
不会。这也是为了以后看到效果。
最后,在plugins
文件夹下建立咱们的Clean-plugin.js
吧:
plugins/Clean-plugin.js:
const recursiveReadSync = require("recursive-readdir-sync");
const minimatch = require("minimatch");
const path = require("path");
const fs = require("fs");
const union = require("lodash.union");
function CleanPlugin (options) {
this.options = options;
}
// 匹配文件
function getUnmatchFiles(fromPath, exclude = []) {
const unmatchFiles = recursiveReadSync(fromPath).filter(file =>
exclude.every(
excluded => {
return !minimatch(path.relative(fromPath, file), path.join(excluded), {
dot: true
})
}
)
);
return unmatchFiles;
}
CleanPlugin.prototype.apply = function (compiler) {
const outputPath = compiler.options.output.path;
compiler.hooks.done.tap('CleanPlugin', stats => {
if (compiler.outputFileSystem.constructor.name !== "NodeOutputFileSystem") {
return;
}
const assets = stats.toJson().assets.map(asset => asset.name);
// 多数组合并而且去重
const newAssets = union(this.options.exclude, assets);
// 获取未匹配文件
const unmatchFiles = getUnmatchFiles(outputPath, newAssets);
// 删除未匹配文件
unmatchFiles.forEach(fs.unlinkSync);
})
}
module.exports = CleanPlugin;
复制代码
比较难的技术难点在「代码分析」中都已经说明了,这里主要说下:
path.relative()
:
path.relative()
方法根据当前工做目录返回 from
到 to
的相对路径。 若是 from
和 to
各自解析到相同的路径(分别调用 path.resolve()
以后),则返回零长度的字符串。
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// 返回: '../../impl/bbb'
复制代码
先来看看咱们如今的目录结构:
首先执行一遍npm run build
,生成以下内容:
而后修改一下src/index.js
中的内容,例如添加一行代码,以后再从新执行npm run build
:
能够看到,只有改变的index.js
被从新删除替换了,而css
文件没有。
再来验证一下options.exclude
,在webpack.config.js
中添加一个插件的参数,就用上一次生成的js
的名称吧:
module.exports = {
plugins: [
new CleanPlugin({
exclude: [
"main.e0c6be8f72d73a68f73a.js"
]
}),
]
}
复制代码
再去修改一下index.js
的内容,例如加两个注释,而后执行npm run build
,会发现此次旧的js
文件并不会被删除,而是会在原来的基础上添加一个新的js
文件。这也证实了咱们的exclude
属性是可用的:
知识无价,支持原创。
参考文章:
你盼世界,我盼望你无bug。这篇文章就介绍到这里。
可算是写完了,但愿这6个小小的插件案例可以帮助你对webpack
的执行机制有一个更深刻的了解,呆呆也会和你一块儿,一块儿加油⛽️。
(本章节教材案例GitHub地址: LinDaiDai/webpack-example/tree/webpack-custom-plugin ⚠️:请仔细查看README说明)
喜欢霖呆呆的小伙还但愿能够关注霖呆呆的公众号 LinDaiDai
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及本身的原创文章🎉
你的鼓励就是我持续创做的主要动力 😊.
相关推荐:
《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》
《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》
《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》
《【何不三连】作完这48道题完全弄懂JS继承(1.7w字含辛整理-返璞归真)》