webpack 可谓是让人欣喜又让人忧,功能强大但须要必定的学习成本。在探寻 webpack 插件机制前,首先须要了解一件有意思的事情,webpack 插件机制是整个 webpack 工具的骨架,而 webpack 自己也是利用这套插件机制构建出来的。所以在深刻认识 webpack 插件机制后,再来进行项目的相关优化,想必会大有裨益。css
先来瞅瞅 webpack 插件在项目中的运用html
const MyPlugin = require('myplugin') const webpack = require('webpack') webpack({ ..., plugins: [new MyPlugin()] ..., })
那么符合什么样的条件能做为 webpack 插件呢?通常来讲,webpack 插件有如下特色:webpack
下面结合代码来看看:git
function MyPlugin(options) {} // 2.函数原型上的 apply 方法会注入 compiler 对象 MyPlugin.prototype.apply = function(compiler) { // 3.compiler 对象上挂载了相应的 webpack 事件钩子 4.事件钩子的回调函数里能拿到编译后的 compilation 对象 compiler.plugin('emit', (compilation, callback) => { ... }) } // 1.独立的 JS 模块,暴露相应的函数 module.exports = MyPlugin
这样子,webpack 插件的基本轮廓就勾勒出来了,此时疑问点有几点,github
plugin.apply()
调用插件的。const webpack = (options, callback) => { ... for (const plugin of options.plugins) { plugin.apply(compiler); } ... }
这些疑问也是本文的线索,让咱们一个个探索。web
compiler 即 webpack 的编辑器对象,在调用 webpack 时,会自动初始化 compiler 对象,源码以下:api
// webpack/lib/webpack.js const Compiler = require("./Compiler") const webpack = (options, callback) => { ... options = new WebpackOptionsDefaulter().process(options) // 初始化 webpack 各配置参数 let compiler = new Compiler(options.context) // 初始化 compiler 对象,这里 options.context 为 process.cwd() compiler.options = options // 往 compiler 添加初始化参数 new NodeEnvironmentPlugin().apply(compiler) // 往 compiler 添加 Node 环境相关方法 for (const plugin of options.plugins) { plugin.apply(compiler); } ... }
终上,compiler 对象中包含了全部 webpack 可配置的内容,开发插件时,咱们能够从 compiler 对象中拿到全部和 webpack 主环境相关的内容。app
compilation 对象表明了一次单一的版本构建和生成资源。当运行 webpack 时,每当检测到一个文件变化,一次新的编译将被建立,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。框架
结合源码来理解下上面这段话,首先 webpack 在每次执行时会调用 compiler.run()
(源码位置),接着追踪 onCompiled 函数传入的 compilation 参数,能够发现 compilation 来自构造函数 Compilation。异步
// webpack/lib/Compiler.js const Compilation = require("./Compilation"); newCompilation(params) { const compilation = new Compilation(this); ... return compilation; }
再介绍完 compiler 对象和 compilation 对象后,不得不提的是 tapable 这个库,这个库暴露了全部和事件相关的 pub/sub 的方法。并且函数 Compiler 以及函数 Compilation 都继承自 Tapable。
事件钩子其实就是相似 MVVM 框架的生命周期函数,在特定阶段能作特殊的逻辑处理。了解一些常见的事件钩子是写 webpack 插件的前置条件,下面列举些常见的事件钩子以及做用:
钩子 | 做用 | 参数 | 类型 |
---|---|---|---|
after-plugins | 设置完一组初始化插件以后 | compiler | sync |
after-resolvers | 设置完 resolvers 以后 | compiler | sync |
run | 在读取记录以前 | compiler | async |
compile | 在建立新 compilation 以前 | compilationParams | sync |
compilation | compilation 建立完成 | compilation | sync |
emit | 在生成资源并输出到目录以前 | compilation | async |
after-emit | 在生成资源并输出到目录以后 | compilation | async |
done | 完成编译 | stats | sync |
完整地请参阅官方文档手册,同时浏览相关源码 也能比较清晰地看到各个事件钩子的定义。
拿 emit 钩子为例,下面分析下插件调用源码:
compiler.plugin('emit', (compilation, callback) => { // 在生成资源并输出到目录以前完成某些逻辑 })
此处调用的 plugin 函数源自上文提到的 tapable 库,其最终调用栈指向了 hook.tapAsync(),其做用相似于 EventEmitter 的 on,源码以下:
// Tapable.js options => { ... if(hook !== undefined) { const tapOpt = { name: options.fn.name || "unnamed compat plugin", stage: options.stage || 0 }; if(options.async) hook.tapAsync(tapOpt, options.fn); // 将插件中异步钩子的回调函数注入 else hook.tap(tapOpt, options.fn); return true; } };
有注入必有触发的地方,源码中经过 callAsync 方法触发以前注入的异步事件,callAsync 相似 EventEmitter 的 emit,相关源码以下:
this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); this.outputFileSystem.mkdirp(outputPath, emitFiles); });
一些深刻细节这里就不展开了,说下关于阅读比较大型项目的源码的两点体会,
结合上述知识点的分析,不难写出本身的 webpack 插件,关键在于想法。为了统计项目中 webpack 各包的有效使用状况,在 fork webpack-visualizer 的基础上对代码升级了一番,项目地址。效果以下:
插件核心代码正是基于上文提到的 emit 钩子,以及 compiler 和 compilation 对象。代码以下:
class AnalyzeWebpackPlugin { constructor(opts = { filename: 'analyze.html' }) { this.opts = opts } apply(compiler) { const self = this compiler.plugin("emit", function (compilation, callback) { let stats = compilation.getStats().toJson({ chunkModules: true }) // 获取各个模块的状态 let stringifiedStats = JSON.stringify(stats) // 服务端渲染 let html = `<!doctype html> <meta charset="UTF-8"> <title>AnalyzeWebpackPlugin</title> <style>${cssString}</style> <div id="App"></div> <script>window.stats = ${stringifiedStats};</script> <script>${jsString}</script> ` compilation.assets[`${self.opts.filename}`] = { // 生成文件路径 source: () => html, size: () => html.length } callback() }) } }