前言
本篇文章为webpack系列文章的第三篇,主要内容是对webpack的plugin进行详细的讲解,从使用,到原理,再到自己开发一个plugin,对每个过程都会进行详细的分析介绍。如果你对webpack了解的还比较少,建议你先阅读以下往期文章。
如何使用webpack实现模块化打包
webpack的核心机制之loader的秘密
介绍
plugin是webpack的一个插件机制,它为项目的构建提供了更加广泛的能力,loader的作用只是实现各种资源的转换,使得任何资源都可以模块化的被打包。而plugin可以解决其他的更多的自动化打包工作,plugin的范围更大,作用也更强。
- 可以自动打包生成html文件,并自动引入打包后的结果
- 打包前清除原dist文件中的内容
- 可以将我们需要的但是并没有引入静态资源一同打包到dist文件中
- 对打包的结果进行特殊的处理
- 压缩打包后的内容,对打包结果可以进行更细的自定义操作
- …
plugin的作用还远不止这些,可以看出plugin的重要性,下面我们来看几个常用的plugin,然后去深入它的原理,最后自己写一个简单的plugin。
情景再现
情景1
通过往期的学习我们知道,每次打包后的内容默认会输出到dist文件夹中,如果在原dist文件夹中存在相同文件名,则会覆盖原文件,使得打包结果始终为最新的内容。但是有没有想过这样的场景,如果原dist中存在某些文件已经被我们遗弃,也就是说后面我们不需要这些文件了,但是他们还是存在于dist中,这种情况下我们还要去提取出来哪些使我们需要的文件,这个过程显然是很繁琐的。
那如何解决这个问题呢,如果我们在打包输出前清除掉dist中原来的内容,那么打包后的内容必然都是我们需要的。我们每次手动的去清理也比较麻烦,如果有个插件可以在我们打包前自动帮我们清理就好了。
clean-webpack-plugin这个插件就可以实现这个功能,它是一个第三方npm包,我们只需要安装就可以使用。
npm install clean-webpack-plugin
在webpack.config.js中引入,我们只需要用到它其中的CleanWebpackPlugin就行。
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
使用插件是在plugins属性中,它是一个数组,表示可以使用多个插件,我们只需要将上面的对象实例化后写在数组中即可。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js',
},
plugins:[
new CleanWebpackPlugin(),
]
}
现在我们执行打包命令
可以看到在原dist中含有很多个文件,我这里打包后会自动的清理掉。
情景2
在我们打包完成后会生成结果文件,我们需要手动的在index.html中去引入,如果结果文件名改变了,还需要手动的去修改引入的文件名。如果有个插件可以在打包完成后自动的生成一个html文件,并自动引入script标签就好了。其实这些功能我们可以使用html-webpack-plugin插件来实现。
npm install clean-webpack-plugin
引入
const HtmlWebpackPlugin = require('html-webpack-plugin')
配置
...
plugins:[
new HtmlWebpackPlugin(),
]
...
我们可以在实例化时传入配置参数,这个插件提供了很多灵活的配置项。
- title:标题
- meta:meta标签
- filename:输出的文件名
- template:使用已有模板,有时在index.html中含有一些其他的内容,可以选用该html文件为模板。
- minify:压缩html文件
- favicon:favicon的路径
- …
这里我只列出了其中的部分配置,更多详情可以去查阅该插件的说明文档。
new HtmlWebpackPlugin({
title: '测试标题',
meta:{
keywords: 'webpack,plugin'
},
filename: 'webpack.html',
template: 'index.html',
minify:{ //压缩html文件
caseSensitive: true, //是否对大小写敏感,默认false
collapseBooleanAttributes: true, //是否简写boolean格式的属性如:disabled="disabled" 简写为disabled 默认false
collapseWhitespace: true, //是否去除空格,默认false
minifyCSS: true, //是否压缩html里的css(使用clean-css进行的压缩) 默认值false;
minifyJS: true, //是否压缩html里的js(使用uglify-js进行的压缩)
preventAttributesEscaping: true, //Prevents the escaping of the values of attributes
removeAttributeQuotes: true, //是否移除属性的引号 默认false
removeComments: true, //是否移除注释 默认false
removeCommentsFromCDATA: true, //从脚本和样式删除的注释 默认false
removeEmptyAttributes: true, //是否删除空属性,默认false
removeOptionalTags: false, // 若开启此项,生成的html中没有 body 和 head,html也未闭合
removeRedundantAttributes: true, //删除多余的属性
removeScriptTypeAttributes: true, //删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
removeStyleLinkTypeAttributes: true, //删除style的类型属性, type="text/css" 同上
useShortDoctype: true, //使用短的文档类型,默认false
},
favicon: './public/logo.ico'
}),
在配置中我选用了自己的index.html作为模板,原有的index.html内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wepack-plugins</title>
</head>
<body>
<div>
<span>我是原html中的内容</span>
</div>
</body>
</html>
在index.js中动态加入了一个标签
var dom = document.createElement('h2')
dom.innerHTML = '我是外部新增的内容'
document.body.append(dom)
执行打包,可以在dist下面出现一个webpack.html文件,这是因为我们在配置中配置了输出文件名为webpack.html。在配置中还对html进行了压缩配置,所以看到的是被压缩后的结果。
我们取消html压缩配置,重新打包
可以看到我们的配置全部生效,包括标题、meta等信息。在浏览器中预览结果完全符合我们的预期。
情景3
最后再来介绍一个插件叫做copy-webpack-plugin,看名字就知道它与复制有关。我们在项目中会用到很多静态资源,但有些可能并没有被直接的使用,我们希望这些资源可以一同的被打包到dist中。
新建一个public文件夹,假如说这个文件夹中的所有内容我们都希望被打包。为了测试结果,在里面我们新建一个static.txt文件,这个文件我们没有在任何地方使用到。
它接收的是一个数组,因为可能存在多个不同的目录
const CopyWebpackPlugin = require('copy-webpack-plugin')
...
new CopyWebpackPlugin(['./public']),
执行打包
通过上面几个案例,是不是可以看出plugin的强大能力和它的重要地位了,总有一个插件可以满足我们的需求。有人会说了,万一我就是没有找到适合我的需求的插件呢?
这个世界上本是没有轮子的,没有轮子我们就要学会去造轮子,下面我们就来造一个自己的轮子。
要学会造轮子
在造轮子之前我们必须要知道它的原理,plugin相比loader还有一点很大的不同,loader只工作于模块的加载环节,而plugin即可可以作用于打包过程的每一个环节,有点像vue中的生命周期,我们可以在一个合适的周期进行相应的操作。webpack的插件机制就是我们常说的钩子机制,整个打包过程可以分为多个环节,为了便于插件的扩展,webpack机会在每个环节都提供了一个钩子,我们就可以利用这些钩子来造轮子。
webpack为我们提供了哪些hooks呢?
- entry-option 初始化 option
- run 开始编译
- compile 真正开始的编译,在创建 compilation 对象之前
- compilation 生成好了 compilation 对象
- make 从 entry 开始递归分析依赖,准备对每个模块进行 build
- after-compile 编译 build 过程结束
- emit 在将内存中 assets 内容写到磁盘文件夹之前
- after-emit 在将内存中 assets 内容写到磁盘文件夹之后
- done 完成所有的编译过程
- failed 编译失败的时候
- …
更多hooks请参考官方文档:https://www.webpackjs.com/api/compiler-hooks/
现在我们知道了生产线,我们在生产线的不同阶段做不同的事情。但是我们还是不知道怎么造轮子啊。
webpack要求我们的插件必须是一个函数,或者是一个包含apply的对象。一般来说我们都会定义一个类型,然后在这个类型中定义apply方法,最后再通过这个类型来创建一个实例对象去使用这个插件。
const pluginName = 'myplugin'
module.exports = class myplugin {
apply(){}
}
这个apply方法接收一个叫compiler的参数对象,这个对象是webpack工作中最核心的对象,包含了此次打包构建的所有配置信息,我们就可以通过这个对象去注册钩子函数。
const pluginName = 'myplugin'
module.exports = class myplugin {
apply(compiler){
compiler.hooks.run.tap(pluginName, () =>{
{
console.log('开始编译');
}
})
}
}
我们想在run阶段输出‘开始编译’这句话,在webpack.config.js中引入并配置
const myplugin = require('./myplugin')
...
plugins:[
new myplugin()
]
...
在控制台可以看到在开始阶段输出了内容,说明我们的plugin生效了,这只是测试,接下来我们就来实现点功能。
我们在使用node或者development模式下,打包后的js文件中,前面会有许多这样的注释符,看起来很不舒服。
我们可以自己来造个轮子让webpack打包后的内容中没有这些东西。
第一步,我们要找到合适的环节执行我们要进行的操作。通过查看API文档,可以找到emit这个hooks很符合我们的场景,它在生成资源到output目录之前执行,也就是在还没有输出打包文件时执行。
我们的思路是这样的,在即将输出文件的前面,获取到要输出的文件内容,找到以js为后缀的文件,然后去掉里面的注释符,最后再重新将处理后的内容替换原来要输出的内容。
./remove-comments-plugin
const pulginName = 'RemoveCommentsPlugin'
module.exports = class RemoveCommentsPlugin {
apply(compiler) {
compiler.hooks.emit.tap(pulginName, (compilation) => {
for (const name in compilation.assets) {
if (name.endsWith('js')) {
let contents = compilation.assets[name].source()
let noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
compilation.assets[name] = {
source: () => noComments,
size: () => noComments.length
}
}
}
})
}
}
它接收一个叫 compilation 的参数对象,这个对象可以理解为本次运行打包的上下文,它包含了所有打包过程中产生的结果。它的assets属性中包含的就是所有打包后将要输出的文件,source()方法可以获取到文件内的内容。
这里我们遍历所有即将要输出的文件,使用endsWith(‘js’)方法匹配到js文件(这是ES6的一个方法),然后使用正则将所有的注释符替换为空字符串,最后将修改后的内容覆盖掉原来的内容。这里要注意一点,在改变文件内容后需要重新计算文件的大小,否则size的值可能会与实际值不匹配。
const RemoveCommentsPlugin from './remove-comments-plugin'
...
plugins:[
new RemoveCommentsPlugin()
]
...
查看打包后的结果,已经不存在注释符。
对于有些插件需要传递配置参数,这个也很简单,我们只需要在构造函数中进行接收即可。
const pulginName = 'RemoveCommentsPlugin'
module.exports = class RemoveCommentsPlugin {
constructor(params){
this.config = {}
for (const key in params) {
if (this.config.hasOwnProperty(key)) {
this.config[key] = params[key]
}
}
}
apply(compiler) {
compiler.hooks.emit.tap(pulginName, (compilation, params) => {
for (const name in compilation.assets) {
if (name.endsWith('js')) {
let contents = compilation.assets[name].source()
let noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
compilation.assets[name] = {
source: () => noComments,
size: () => noComments.length
}
}
}
})
}
}
每次都不知道怎么写结语,这都不重要,重要到的可以学到东西,学以致用就够了。
最后觉得我的文章对你有所帮助的话,希望可以收藏、点赞、关注,你们的支持是我最大的动力