JavaScript
浅谈Webpack4 plugins 实现原理
前言
在 wabpack 中核心功能除了 loader 应该就是 plugins 插件了,它是在webpack执行过程中会广播一系列事件,plugin 会监听这些事件并通过 webpack Api 对输出文件做对应的处理, 如 hmlt-webpack-plugin 就是对模板魔剑 index.html 进行拷贝到 dist 目录的
认识
先来通过源码来认识一下 plugins 的基本结构
https://github.com/webpack/webpack/blob/webpack-4/lib/Compiler.js 551行
// 创建一个编译器 createChildCompiler( compilation, compilerName, compilerIndex, outputOptions, plugins // 里边就有包含插件 ) { // new 一个 编译器 const childCompiler = new Compiler(this.context); // 寻找存在的所有 plugins 插件 if (Array.isArray(plugins)) { for (const plugin of plugins) { // 如果存在, 就调用 plugin 的 apply 方法 plugin.apply(childCompiler); } } // 遍历寻找 plugin 对应的 hooks for (const name in this.hooks) { if ( ![ "make", "compile", "emit", "afterEmit", "invalid", "done", "thisCompilation" ].includes(name) ) { // 找到对应的 hooks 并调用, if (childCompiler.hooks[name]) { childCompiler.hooks[name].taps = this.hooks[name].taps.slice(); } } } // .... 省略 .... return childCompiler; }
通过上述源码可以看出来 plugin 本质就是一个类, 首先就是 new 一个 compiler 类,传入当前的上下文,然后判断是否存在,存在则直接调用对应 plugin 的 apply 方法,然后再找到对应 plugin 调用的 hooks 事件流 , 发射给对应 hooks 事件
hooks 哪里来的 ?
https://github.com/webpack/webpack/blob/webpack-4/lib/Compiler.js 42行
// 上述的 Compiler 类继承自 Tapable 类,而 Tapable 就定义了这些 hooks 事件流 class Compiler extends Tapable { constructor(context) { super(); this.hooks = { /** @type {SyncBailHook<Compilation>} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook<Stats>} */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook<Compiler>} */ beforeRun: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compiler>} */ run: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compilation>} */ emit: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<string, Buffer>} */ assetEmitted: new AsyncSeriesHook(["file", "content"]), /** @type {AsyncSeriesHook<Compilation>} */ afterEmit: new AsyncSeriesHook(["compilation"]), /** @type {SyncHook<Compilation, CompilationParams>} */ thisCompilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<Compilation, CompilationParams>} */ compilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<NormalModuleFactory>} */ normalModuleFactory: new SyncHook(["normalModuleFactory"]), /** @type {SyncHook<ContextModuleFactory>} */ contextModuleFactory: new SyncHook(["contextModulefactory"]), /** @type {AsyncSeriesHook<CompilationParams>} */ beforeCompile: new AsyncSeriesHook(["params"]), /** @type {SyncHook<CompilationParams>} */ compile: new SyncHook(["params"]), /** @type {AsyncParallelHook<Compilation>} */ make: new AsyncParallelHook(["compilation"]), /** @type {AsyncSeriesHook<Compilation>} */ afterCompile: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<Compiler>} */ watchRun: new AsyncSeriesHook(["compiler"]), /** @type {SyncHook<Error>} */ failed: new SyncHook(["error"]), /** @type {SyncHook<string, string>} */ invalid: new SyncHook(["filename", "changeTime"]), /** @type {SyncHook} */ watchClose: new SyncHook([]), /** @type {SyncBailHook<string, string, any[]>} */ infrastructureLog: new SyncBailHook(["origin", "type", "args"]), // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook} */ environment: new SyncHook([]), /** @type {SyncHook} */ afterEnvironment: new SyncHook([]), /** @type {SyncHook<Compiler>} */ afterPlugins: new SyncHook(["compiler"]), /** @type {SyncHook<Compiler>} */ afterResolvers: new SyncHook(["compiler"]), /** @type {SyncBailHook<string, Entry>} */ entryOption: new SyncBailHook(["context", "entry"]) }; // TODO webpack 5 remove this this.hooks.infrastructurelog = this.hooks.infrastructureLog; // 通过 tab 调用对应的 comiler 编译器,并传入一个回调函数 this._pluginCompat.tap("Compiler", options => { switch (options.name) { case "additional-pass": case "before-run": case "run": case "emit": case "after-emit": case "before-compile": case "make": case "after-compile": case "watch-run": options.async = true; break; } }); // 下方省略 ...... }
好了,了解过基本的结构之后,就可以推理出 plugin 基本的结构和用法了,就是下边这样
// 定义一个 plugins 类 class MyPlugins { // 上边有说 new 一个编译器实例,会执行实例的 apply 方法,传入对应的 comiler 实例 apply (compiler) { // 调用 new 出来 compiler 实例下的 hooks 事件流,通过 tab 触发,并接收一个回调函数 compiler.hooks.done.tap('一般为插件昵称', (默认接收参数) => { console.log('进入执行体'); }) } } // 导出 module.exports = MyPlugins
ok, 以上就是一个简单的 模板 ,我们来试试内部的钩子函数,是否会如愿以偿的被调用和触发
配置 webpack
let path = require('path') let DonePlugin = require('./plugins/DonePlugins') let AsyncPlugins = require('./plugins/AsyncPlugins') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'build.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new DonePlugin(), // 内部同步 hooks new AsyncPlugins() // 内部异步 hooks ] }
同步 plugin 插件模拟调用
class DonePlugins { apply (compiler) { compiler.hooks.done.tap('DonePlugin', (stats) => { console.log('执行: 编译完成'); }) } } module.exports = DonePlugins
异步 plugin 插件模拟调用
class AsyncPlugins { apply (compiler) { compiler.hooks.emit.tapAsync('AsyncPlugin', (complete, callback) => { setTimeout(() => { console.log('执行:文件发射出来'); callback() }, 1000) }) } } module.exports = AsyncPlugins
最后编译 webpack 可以看到编译控制台,分别打印 执行: 编译完成,执行:文件发射出来,说明这样是可以调用到 hooks 事件流的,并且可以触发。
实践出真知
了解过基本结构和使用的方式了,现在来手写一个 plugin 插件,嗯,就来一个文件说明插件吧,我们日常打包,可以打包一个 xxx.md 文件到 dist 目录,来做一个打包说明,就来是实现这么一个小功能
文件说明插件
class FileListPlugin { // 初始化,获取文件的名称 constructor ({filename}) { this.filename = filename } // 同样的模板形式,定义 apply 方法 apply (compiler) { compiler.hooks.emit.tap('FileListPlugin', (compilation) => { // assets 静态资源,可以打印出 compilation 参数,还有很多方法和属性 let assets = compilation.assets; // 定义输出文档结构 let content = `## 文件名 资源大小\r\n` // 遍历静态资源,动态组合输出内容 Object.entries(assets).forEach(([filename, stateObj]) => { content += `- ${filename} ${stateObj.size()}\r\n` }) // 输出资源对象 assets[this.filename] = { source () { return content; }, size () { return content.length } } }) } } // 导出 module.exports = FileListPlugin
webpack 配置
let path = require('path') let HtmlWebpackPlugin = require('html-webpack-plugin') // plugins 目录与node_modules 同级, 自定义 plugins , 与 loader 类似 let FileListPlugin = require('./plugins/FileListPlugin') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'build.js', path: path.resolve(__dirname, 'dist') }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', filename: 'index.html' }), new FileListPlugin({ filename: 'list.md' }) ] }
ok,通过以上配置,我们再打包的时候就可以看到,每次打包在 dist 目录就会出现一个 xxx.md 文件,而这个文件的内容就是我们上边的 content
到此这篇关于浅谈Webpack4 plugins 实现原理的文章就介绍到这了,更多相关Webpack4 plugins 内容请搜索 以前的文章或继续浏览下面的相关文章希望大家以后多多支持 !