持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
得不得奖的无所谓希望能强迫自己闯一关╮( ̄▽ ̄)╭,上次更文未通关,这次继续
前言
记录 实现webpack plugin
的学习总结
有误请多多指正,附上女神图保命 [手动狗头]
编写的内容将收入专栏webpack学习专栏
学习已完成
- 1.什么是Plugin
- 2.编写自定义plugin
- 3.使用编写好的自定义plugin
- 4.开发中常用的plugin介绍与使用
什么是Plugin
Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果,Plugin 的出现就是为了丰富 Webpack 的 API。
一个最基础的 Plugin 的代码是这样的:
class BasicPlugin{
// 在构造函数中获取用户给该插件传入的配置
constructor(options){}
// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
// 指定一个挂载到 compilation 的钩子,回调函数的参数为 compilation 。
compiler.hooks.compilation.tap('BasicPlugin', (compilation) => {
// 现在可以通过 compilation 对象绑定各种钩子
compilation.hooks.optimize.tap('BasicPlugin', () => {
console.log('资源已经优化完毕。');
});
});
}
}
// 导出 Plugin
module.exports = BasicPlugin;
使用这个 Plugin 时,配置如下:
const BasicPlugin = require('./BasicPlugin.js');
module.export = {
plugins:[
new BasicPlugin({name:'tywd'}),
]
}
Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。
这就是 Plugin 的工作原理,实际开发中还有很多细节,继续往下看。
编写自定义plugin
在 Webpack 运行的生命周期中会广播出许多事件,在这些事件钩子中进行plugin操作, webpack 工作流程钩子参考
plugin基本结构
一个最基本的 plugin 需要包含这些部分,在开发插件时需要注意:
- 一个 JavaScript 类
- 一个 apply 方法,apply 方法在 webpack 装载这个插件的时候被调用,并且会传入 compiler 对象。只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
- 使用不同的 webpack 提供的 hooks 来指定自己需要发生的处理行为
- 在异步调用时,异步的事件会附带两个参数,第二个参数为回调函数 callback,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。或者需要通过 return Promise 的方式。在下面会介绍 tapAsync 和 tapPromise
传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
Compiler 和 Compilation
在开发 Plugin 时最常用的也是最重要的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。
-
Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;上面提到的 apply 方法传入的参数就是它。 在为 webpack 开发插件时,你可能需要知道每个钩子函数是在哪里调用的。想要了解这些内容,请在 webpack 源码中搜索
hooks.<hook name>.call
-
Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
Compiler 和 Compilation 提供了非常多的钩子供我们使用,这些方法的组合可以让我们在构建过程的不同时间获取不同的内容,具体可查看官方中文文档
事件流机制
Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable。
Webpack 的 Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条 webapck 机制中,去改变 webapck 的运作,使得整个系统扩展性良好。
Tapable 也是一个小型的 library,是 Webpack 的一个核心工具。类似于 node 中的 events 库,核心原理就是一个订阅发布模式。作用是提供类似的插件接口。 Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件
// 广播事件
compiler.apply('event-name', params)
compilation.apply('event-name', params)
// 监听事件
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})
同步与异步
plugin 的 hooks 是有同步和异步区分的
在同步的情况下,上面笔者使用 <hookName>.tap
的方式进行调用
而在异步 hook 内我们可以进行一些异步操作,并且有异步操作的情况下,请使用 tapAsync 或者 tapPromise 方法来告知 webpack 这里的内容是异步的
tapAsync
需要多传一个回调
class HelloPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('HelloPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('async')
callback()
}, 1000)
})
}
}
module.exports = HelloPlugin
tapPromise
需要返回一个 Promise 对象并且让它在结束的时候 resolve
class HelloPlugin {
apply(compiler) {
compiler.hooks.emit.tapPromise('HelloPlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('async')
resolve()
}, 1000)
})
})
}
}
module.exports = HelloPlugin
编写 plugin 常用 API 参考
参考 github.com/tywd/webpac… 下的的构建
在 github.com/tywd/webpac… 查看如何使用
判断 Webpack 使用了哪些插件
具体代码参考 custom-plugins/basic-plugin.js hasHtmlWebpackPlugin 方法
compiler 事件钩子 done 和 failed
具体代码参考,代码中有详细的注释 custom-plugins/end-webpack-plugin.js
class EndWebpackPlugin {
// 在构造函数中获取用户给该插件传入的配置
constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
compiler.hooks.done.tap('EndWebpackPlugin', (stats) => {
this.doneCallback(stats); // 在 done 事件中回调 doneCallback
})
compiler.hooks.failed.tap('EndWebpackPlugin', (err) => {
this.failCallback(err); // 在 failed 事件中回调 failCallback
})
}
}
// 导出 Plugin
module.exports = EndWebpackPlugin;
compiler 的 compilation 事件钩子 processAssets 和 emitAsset
具体代码参考,代码中有详细的注释 custom-plugins/filelist-plugin.js
// 一个简单的示例插件,生成一个叫做 assets.md 的新文件;文件内容是所有构建生成的文件的列表
// 参考 https://webpack.docschina.org/contribute/writing-a-plugin/#creating-a-plugin
class FileListPlugin {
static defaultOptions = {
outputFile: 'assets.md', // 输出的md文件名
};
// 需要传入自定义插件构造函数的任意选项
//(这是自定义插件的公开API)
constructor(options = {}) {
// 在应用默认选项前,先应用用户指定选项
// 合并后的选项暴露给插件方法
// 记得在这里校验所有选项 可参考 basic-plugin.js 使用 validate 方法
this.options = {
...FileListPlugin.defaultOptions,
...options
};
}
apply(compiler) {
const pluginName = FileListPlugin.name;
const { webpack } = compiler;
const { Compilation } = webpack;
const { RawSource } = webpack.sources;
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
},
(assets) => {
const content =
'# In this build:\n\n' +
Object.keys(assets)
.map((filename) => `- ${filename}`)
.join('\n');
compilation.emitAsset(
this.options.outputFile,
new RawSource(content)
);
}
);
})
}
}
module.exports = FileListPlugin
使用编写好的自定义plugin
参考上面 什么是Plugin
开发中常用的plugin
1. html-webpack-plugin
将一个页面模板打包到dist目录下,默认都是自动引入js or css
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './index.html', // 以本地的index.html文件为基础模板
filename: "index.html", // 输出到dist目录下的文件名称
}),
]
}
2. clean-webpack-plugin
用于每次打包dist目录删除
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin()
]
}
3. copy-webpack-plugin
用于将文件拷贝到某个目录下
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: "./main.js",
to: __dirname + "/dist/js",
toType: "dir"
}
]
})
]
}
上面配置中,将main.js拷贝到dist目录下的js里,toType默认是file,也可以设置为dir,因为我这dist目录下没有js目录。
4. mini-css-extract-plugin
都是将css样式提取出来,需要配合 css-loader
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "css/[name].css"
})
]
}
5. optimize-css-assets-webpack-plugin
用于压缩css样式
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin")
module.exports = {
plugins: [
new OptimizeCssAssetsWebpackPlugin(),
]
}
6. webpack.HotModuleReplacementPlugin
开启热模块更新,无需安装,webpack内置
const Webpack = require("webpack")
module.exports = {
plugins: [
new Webpack.HotModuleReplacementPlugin()
]
}
8. webpack.DefinePlugin
用于注入全局变量,一般用在环境变量上。无需安装,webpack内置
const Webpack = require("webpack")
module.exports = {
plugins: [
new Webpack.DefinePlugin({
STR: JSON.stringify("蛙人"),
"process.env": JSON.stringify("dev"),
name: "蛙人"
})
]
}
上面配置中,DefinePlugin接收一个对象,里面的key值对应一个value值,这个value值是一个代码片段,可以看上面name那个,会报错 蛙人 is not defined,这里需要注意,value值必须是一个变量或代码片段。
9. webpack.ProvidePlugin
用于定义全局变量,如100个页面都引入vue,每个页面都引入只会增加工作量,直接在webpackProvide挂载一个变量就行,不用再去一一引入。无需安装,webpack内置
const Webpack = require("webpack")
module.exports = {
plugins: [
new Webpack.ProvidePlugin({
"Vue": ["vue", "default"]
})
]
}
上面配置中,ProvidePlugin接收一个对象,key值是使用的变量,value值第一个参数是Vue模块,第二个参数默认取Es Module.default的属性。import默认引入进来是一个 Es Module的对象,里面有default这个属性就是实体对象
10. webpack.SplitChunksPlugin
以下两插件均为 Webpack 内置,无需安装。
- webpack4.0之前使用
webpack.optimize.CommonsChunkPlugin
- webpack4.0之后使用
optimization.SplitChunks
optimization.SplitChunks
配置
// main.js
import Vue from "vue"
console.log(Vue)
import("./news")
// news.js
import Vue from "vue"
console.log(Vue)
// webpack.config.js
module.exports = {
mode: "development",
entry: {
main: "./main.js"
},
output: {
filename: "[name].js",
path: __dirname + "/dist"
},
optimization: {
splitChunks: {
chunks: "all" // all是对所有的chunk都生效,默认只对async异步有效。
}
},
}
splitChunks默认情况下也有自动提取,默认要求如下:
- 被提取的模块来自node_module目录
- 模块大于30kb
- 按需加载时请求资源最大值小于等于5
- 首次加载时并行请求最大值小于等于3
11. webpack.IgnorePlugin
用于过滤打包文件,减少打包体积大小。无需安装,webpack内置
const Webpack = require("webpack")
module.exports = {
plugins: [
new Webpack.IgnorePlugin(/.\/lib/, /element-ui/)
]
}
12. uglifyjs-webpack-plugin
用于压缩js文件,针对webpack4版本以上。
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new UglifyJsPlugin({
test: /\.js(\?.*)?$/i,
exclude: /node_modules/
})
]
}
}
13. imagemin-webpack-plugin
图片压缩
const ImageminPlugin = require('imagemin-webpack-plugin').default
module.exports = {
plugins: [
new ImageminPlugin({
test: /\.(jpe?g|png|gif|svg)$/i
})
]
}
这个插件最好使用 cnpm i -D imagemin-webpack-plugin
, npm 好像会有些依赖拉不到,以至于运行时出错
14. VueLoaderPlugin
Vue文件转换
const { VueLoaderPlugin} = require('vue-loader'); // 来自于vue-loader
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
]
},
plugins: [
new VueLoaderPlugin(),
]
}
15. webpack-bundle-analyzer
打包分析
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerPort: 9091,
generateStatsFile: false
})
]
}
15. friendly-errors-webpack-plugin
美化控制台,良好的提示错误。终端打印较美观
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
module.exports = {
plugins: [
new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
notes: ['蛙人你好,系统正运行在http://localhost:' + devServer.port]
},
clearConsole: true,
})
],
}
写在最后
参考文章
# 吐血整理的webpack入门知识及常用loader和plugin
代码地址
以上的方式总结只是自己学习总结,有其他方式欢迎各位大佬评论
渣渣一个,欢迎各路大神多多指正,不求赞,只求监督指正( ̄. ̄)
有关文章经常被面试问到可以帮忙留下言,小弟也能补充完善完善一起交流学习,感谢各位大佬(~ ̄▽ ̄)~