Plugin 基础
基础使用
- webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景;在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果;
- 一个最基础的 Plugin 的代码是这样的:
class BasicPlugin { // 在构造函数中获取用户给该插件传入的配置 constructor(options) { } // webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象 apply(compiler) { compiler.plugin('compilation', function (compilation) { // 在 compilation 阶段的事件 }); } } // 导出 Plugin module.exports = BasicPlugin;
- 在使用这个 Plugin 时,相关配置代码如下:
const BasicPlugin = require('./BasicPlugin.js'); module.exports = { plugins: [ new BasicPlugin(options), ], };
- webpack 启动后,在读取配置的过程中会先执行
new BasicPlugin(options)
初始化一个 BasicPlugin 实例;在初始化 compiler 对象后,再调用basicPlugin.apply(compiler)
给插件实例传入 compiler 对象;插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数)
监听到 webpack 广播出来的事件;并且可以通过 compiler 对象去操作 webpack;
Plugin 的职责
-
Plugin 的职责是扩展 webpack 的自动化能力,而不仅仅是文件的转换;
-
它们可以在 webpack 的整个生命周期中执行特定的任务,如打包优化、资源管理和环境变量注入等;Plugin 的功能非常强大,可以覆盖构建的各个方面;
Plugin 实现原理
-
Plugin 是一个具有 apply 方法的 JavaScript 对象;
-
apply 方法会被 webpack 编译器调用,并且在整个编译生命周期中可以访问编译器对象;以下是 Plugin 的执行步骤:
- 初始化插件:webpack 配置中定义的插件会被实例化;
- 调用 apply 方法:每个插件实例的 apply 方法会被调用,并传入编译器对象;
- 挂钩到编译生命周期:插件可以通过编译器对象在 webpack 编译生命周期的不同阶段挂钩执行自定义逻辑;
常用 Plugin 盘点
-
HtmlWebpackPlugin
:生成 HTML 文件,并自动注入打包后的资源const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', // 模板文件 }), ], };
-
CleanWebpackPlugin
:每次构建前清理输出目录const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { plugins: [ new CleanWebpackPlugin(), ], };
-
MiniCssExtractPlugin
:将 CSS 提取到单独的文件中const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', // 输出的 CSS 文件名 }), ], };
-
TerserWebpackPlugin
:压缩 JavaScript 代码const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [new TerserPlugin()], }, };
-
DefinePlugin
:创建在编译时可以配置的全局常量const webpack = require('webpack'); module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), }), ], };
-
CopyWebpackPlugin
:将文件或文件夹复制到构建的输出目录const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { plugins: [ new CopyWebpackPlugin({ patterns: [ { from: 'source', to: 'dest' }, { from: 'other', to: 'public' }, ], }), ], };
-
HotModuleReplacementPlugin
:启用模块热替换 (HMR)const webpack = require('webpack'); module.exports = { devServer: { contentBase: './dist', hot: true, }, plugins: [ new webpack.HotModuleReplacementPlugin(), ], };
-
BundleAnalyzerPlugin
:可视化 webpack 输出文件的大小const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin(), ], };
手写 Plugin
-
实现代码:
JavaScriptJavaScript// FileListPlugin.js module.exports = class FileListPlugin { constructor(filename = "filelist.txt") { this.filename = filename; } apply(compiler) { compiler.hooks.emit.tap("FileListPlugin", complation => { var fileList = []; // complation.assets 获取文件列表 for (const key in complation.assets) { var content = `【${key}】大小:${complation.assets[key].size() / 1000}KB`; fileList.push(content); } var str = fileList.join("\n\n"); complation.assets[this.filename] = { source() { return str }, size() { return str.length; } } }) } }
// webpack.config.js var FileListPlugin = require("./plugins/FileListPlugin"); module.exports = { mode: "development", devtool: "source-map", plugins: [ new FileListPlugin("文件列表.md") ] }
-
生成的文件
文件列表.md
【main.js】大小:4.076KB 【main.js.map】大小:3.7KB
Plugin 进阶
Compiler 和 Compilation
-
在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 webpack 之间的桥梁;Compiler 和 Compilation 的含义如下:
- Compiler 对象包含了 webpack 环境所有的配置信息,包含 options、loaders、plugins 这些信息,这个对象在 webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 webpack 实例;
- Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等;
- 当 webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建;Compilation 对象也提供了很多事件回调供插件做扩展;
- 通过 Compilation 也能读取到 Compiler 对象;
-
Compiler 和 Compilation 的区别在于:Compiler 代表了整个 webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译;
事件流
-
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果;
- 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理;
- 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理;
-
webpack 通过 Tapable 来组织这条复杂的生产线;
- webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作;
- webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好;
-
webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似;
- Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听;
- 事件,方法如下:
// 广播出事件 compiler.apply('event-name', params); // 监听名称为 event-name 的事件 compiler.plugin('event-name', function (params) { // 处理逻辑 });
-
同理,compilation.apply 和 compilation.plugin 使用方法和上面一致;
-
在开发插件时,可能会不知道该如何下手,因为不知道该监听哪个事件才能完成任务;在开发插件时,还需要注意以下几点:
- 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用;
- 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用;也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件;
- 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 webpack,才会进入下一处理流程;例如:
compiler.plugin('emit', function (compilation, callback) { // 支持处理逻辑 // 处理完毕后执行 callback 以通知 Webpack // 如果不执行 callback,运行流程将会一直卡在这不往下执行 callback(); });
常用 API
读取输出资源、代码块、模块及其依赖
-
有些插件可能需要读取 webpack 的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理;
-
在 emit 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容;
-
插件代码如下:
class Plugin { apply(compiler) { compiler.plugin('emit', function (compilation, callback) { // compilation.chunks 存放所有代码块,是一个数组 compilation.chunks.forEach(function (chunk) { // chunk 代表一个代码块 // 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块 chunk.forEachModule(function (module) { // module 代表一个模块 // module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组 module.fileDependencies.forEach(function (filepath) { // 处理每个依赖文件路径 }); }); // webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件 chunk.files.forEach(function (filename) { // compilation.assets 存放当前所有即将输出的资源 // 调用一个输出资源的 source() 方法能获取到输出资源的内容 let source = compilation.assets[filename].source(); }); }); // 这是一个异步事件,要记得调用 callback 通知 webpack 本次事件监听处理结束。 callback(); }); } }
监听文件变化
-
在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:
// 当依赖的文件发生变化时会触发 watch-run 事件 compiler.plugin('watch-run', (watching, callback) => { // 获取发生变化的文件列表 const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes; // changedFiles 格式为键值对,键为发生变化的文件路径 if (changedFiles[filePath] !== undefined) { // filePath 对应的文件发生了变化 } callback(); });
-
默认情况下 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(); });
修改输出资源
-
有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,
- 因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好,需要输出的资源即将输出;
- 因此 emit 事件是修改 webpack 输出资源的最后时机;
-
所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容;
-
设置 compilation.assets 的代码如下:
compiler.plugin('emit', (compilation, callback) => { // 设置名称为 fileName 的输出资源 compilation.assets[fileName] = { // 返回文件内容 source: () => { // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer return fileContent; }, // 返回文件大小 size: () => { return Buffer.byteLength(fileContent, 'utf8'); }, }; callback(); });
-
读取 compilation.assets 的代码如下:
compiler.plugin('emit', (compilation, callback) => { // 读取名称为 fileName 的输出资源 const asset = compilation.assets[fileName]; // 获取输出资源的内容 asset.source(); // 获取输出资源的文件大小 asset.size(); callback(); });
判断 webpack 使用哪些插件
-
在开发一个插件时可能需要根据当前配置是否使用了其它某个插件而做下一步决定,因此需要读取 webpack 当前的插件配置情况;
-
以判断当前是否使用了 ExtractTextPlugin 为例,可以使用如下代码:
// 判断当前配置是否使用了 ExtractTextPlugin, // compiler 参数即为 webpack 在 apply(compiler) 中传入的参数 function hasExtractTextPlugin(compiler) { // 当前配置所有使用的插件列表 const plugins = compiler.options.plugins; // 去 plugins 中寻找有没有 ExtractTextPlugin 的实例 return plugins.find(plugin => plugin.__proto__.constructor === ExtractTextPlugin) != null; }
插件机制 Tapable
Tapable 是一个轻量级的库,用于创建和管理插件钩子 (hooks),它在 webpack 中广泛应用,用于实现插件系统;
Tapable 提供了一种机制,允许插件在特定的生命周期阶段插入自定义逻辑,从而扩展应用程序的功能;
Tapable 基本概念
-
Tapable 提供了多种类型的钩子,每种钩子都有不同的执行方式和用途;
-
主要的钩子类型包括:
钩子 描述 SyncHook 同步钩子,不需要返回值 SyncBailHook 同步钩子,如果任意一个回调函数返回非 undefined 值,则中止剩下的回调执行 SyncWaterfallHook 同步钩子,上一个回调函数的返回值会作为参数传递给下一个回调函数 SyncLoopHook 同步钩子,如果任意一个回调函数返回 true,则这个函数会被重新执行 AsyncSeriesHook 异步串行钩子,依次执行每个回调函数 AsyncSeriesBailHook 异步串行钩子,如果任意一个回调函数返回非 undefined 值,则中止剩下的回调执行 AsyncSeriesWaterfallHook 异步串行钩子,上一个回调函数的返回值会作为参数传递给下一个回调函数 AsyncParallelHook 异步并行钩子,同时执行所有回调函数 AsyncParallelBailHook 异步并行钩子,如果任意一个回调函数返回非 undefined 值,则中止剩下的回调执行
Tapable 示例
-
创建和使用同步钩子
JavaScripttextconst { SyncHook } = require('tapable'); // 1. 创建一个同步钩子 const hook = new SyncHook(['arg1', 'arg2']); // 2. 注册回调函数 hook.tap('FirstPlugin', (arg1, arg2) => { console.log('FirstPlugin:', arg1, arg2); }); hook.tap('SecondPlugin', (arg1, arg2) => { console.log('SecondPlugin:', arg1, arg2); }); // 3. 触发钩子 hook.call('Hello', 'World');
// ------输出------ FirstPlugin: Hello World SecondPlugin: Hello World
-
创建和使用异步钩子
JavaScripttextconst { AsyncSeriesHook } = require('tapable'); // 创建一个异步串行钩子 const asyncHook = new AsyncSeriesHook(['arg1', 'arg2']); // 注册回调函数 asyncHook.tapAsync('FirstAsyncPlugin', (arg1, arg2, callback) => { setTimeout(() => { console.log('FirstAsyncPlugin:', arg1, arg2); callback(); }, 1000); }); asyncHook.tapAsync('SecondAsyncPlugin', (arg1, arg2, callback) => { setTimeout(() => { console.log('SecondAsyncPlugin:', arg1, arg2); callback(); }, 500); }); // 触发钩子 asyncHook.callAsync('Hello', 'World', () => { console.log('All plugins are done.'); });
// ------输出------ FirstAsyncPlugin: Hello World SecondAsyncPlugin: Hello World All plugins are done.
-
创建和使用带返回值的异步钩子
JavaScripttextconst { AsyncSeriesBailHook } = require('tapable'); // 创建一个异步串行钩子,带返回值 const asyncBailHook = new AsyncSeriesBailHook(['arg1', 'arg2']); // 注册回调函数 asyncBailHook.tapAsync('FirstAsyncBailPlugin', (arg1, arg2, callback) => { setTimeout(() => { console.log('FirstAsyncBailPlugin:', arg1, arg2); callback(null, 'FirstResult'); }, 1000); }); asyncBailHook.tapAsync('SecondAsyncBailPlugin', (arg1, arg2, callback) => { setTimeout(() => { console.log('SecondAsyncBailPlugin:', arg1, arg2); callback(null, 'SecondResult'); }, 500); }); // 触发钩子 asyncBailHook.callAsync('Hello', 'World', (err, result) => { console.log('Result:', result); });
// ------输出------ FirstAsyncBailPlugin: Hello World Result: FirstResult
在 webpack 中的应用
-
这个插件通过 emit 和 done 钩子在 webpack 编译的不同阶段插入自定义逻辑;
-
示例代码
class MyPlugin { apply(compiler) { // 使用 emit 异步钩子 compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { console.log('MyPlugin is working during emit phase!'); callback(); }); // 使用 done 同步钩子 compiler.hooks.done.tap('MyPlugin', (stats) => { console.log('MyPlugin is working during done phase!'); }); } } module.exports = MyPlugin;
vue-router📝 基础使用
上一篇