博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
探寻 webpack 插件机制
阅读量:6489 次
发布时间:2019-06-24

本文共 4740 字,大约阅读时间需要 15 分钟。

img_c4125ea31357b73b0859b84b7e3dd2e4.jpe

webpack 可谓是让人欣喜又让人忧,功能强大但需要一定的学习成本。在探寻 webpack 插件机制前,首先需要了解一件有意思的事情,webpack 插件机制是整个 webpack 工具的骨架,而 webpack 本身也是利用这套插件机制构建出来的。因此在深入认识 webpack 插件机制后,再来进行项目的相关优化,想必会大有裨益。

webpack 插件

先来瞅瞅 webpack 插件在项目中的运用

const MyPlugin = require('myplugin')const webpack = require('webpack')webpack({
..., plugins: [new MyPlugin()] ...,})

那么符合什么样的条件能作为 webpack 插件呢?一般来说,webpack 插件有以下特点:

  1. 独立的 JS 模块,暴露相应的函数

  2. 函数原型上的 apply 方法会注入 compiler 对象

  3. compiler 对象上挂载了相应的 webpack 事件钩子

  4. 事件钩子的回调函数里能拿到编译后的 compilation 对象,如果是异步钩子还能拿到相应的 callback

下面结合代码来看看:

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 插件的基本轮廓就勾勒出来了,此时疑问点有几点,

  1. 疑问 1:函数的原型上为什么要定义 apply 方法?阅读后发现源码中是通过 plugin.apply() 调用插件的。
const webpack = (options, callback) => {
... for (const plugin of options.plugins) {
plugin.apply(compiler); } ...}
  1. 疑问 2:compiler 对象是什么呢?

  2. 疑问 3:compiler 对象上的事件钩子是怎样的?

  3. 疑问 4:事件钩子的回调函数里能拿到的 compilation 对象又是什么呢?

这些疑问也是本文的线索,让我们一个个探索。

compiler 对象

compiler 即 webpack 的编辑器对象,在调用 webpack 时,会自动初始化 compiler 对象,如下:

// webpack/lib/webpack.jsconst 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 主环境相关的内容。

compilation 对象

compilation 对象代表了一次单一的版本构建和生成资源。当运行 webpack 时,每当检测到一个文件变化,一次新的编译将被创建,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。

结合源码来理解下上面这段话,首先 webpack 在每次执行时会调用 compiler.run() ,接着追踪 传入的 compilation 参数,可以发现 compilation 来自构造函数 Compilation。

// webpack/lib/Compiler.jsconst Compilation = require("./Compilation");newCompilation(params) {
const compilation = new Compilation(this); ... return compilation;}

不得不提的 tapable 库

再介绍完 compiler 对象和 compilation 对象后,不得不提的是 这个库,这个库暴露了所有和事件相关的 pub/sub 的方法。而且函数 以及函数 都继承自 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.jsoptions => {
... 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 插件,关键在于想法。为了统计项目中 webpack 各包的有效使用情况,在 fork 的基础上对代码升级了一番,。效果如下:

img_02b2c58a2e810615e7273c7a3b1dcb2d.jpe

插件核心代码正是基于上文提到的 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 = `
AnalyzeWebpackPlugin
` compilation.assets[`${
self.opts.filename}`] = {
// 生成文件路径 source: () => html, size: () => html.length } callback() }) }}

参考资料

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:

作者:

出处:
本文版权归作者和博客园所有,欢迎转载,转载请标明出处。
如果您觉得本篇博文对您有所收获,请点击右下角的 [推荐],谢谢!

你可能感兴趣的文章
服务器管理助手Linux版(宝塔)新版安装,支持一键SSL配置
查看>>
开发原生的 Google 眼镜应用 【已翻译100%】(1/2)
查看>>
《树莓派Python编程入门与实战》——1.3 哪些树莓派外设是必须的
查看>>
《编译与反编译技术实战 》一3.2 词法分析器的手工实现
查看>>
《计算机存储与外设》----1.5 虚拟存储器和存储器管理
查看>>
《 Python树莓派编程》——3.4 利用Python进行编程
查看>>
从损坏的 Linux EFI 安装中恢复
查看>>
Git Rebase教程: 用Git Rebase让时光倒流
查看>>
柏林纪行(上):整体感受
查看>>
《Python数据科学指南》——1.14 返回一个函数
查看>>
《Python数据分析》一1.7 学习手册页
查看>>
Centos7 下建立 Docker 桥接网络
查看>>
《Hack与HHVM权威指南》——1.6 类型推理
查看>>
《CCNA学习指南:数据中心(640-911)》——导读
查看>>
《精通 ASP.NET MVC 5》----1.3 ASP.NET MVC的关键优点
查看>>
《JavaScript框架设计》——1.5 主流框架引入的机制——domReady
查看>>
《正则表达式经典实例(第2版)》——2.3 匹配多个字符之一
查看>>
深入实践Spring Boot1.3.1 Maven依赖管理
查看>>
API网关的iOS SDK已经支持 IPV6
查看>>
《iOS 8开发指南(第2版)》——第1章,第1.4节使用Xcode开发环境
查看>>