zoukankan      html  css  js  c++  java
  • webpack-插件机制杂记

    系列文章

    Webpack系列-第一篇基础杂记
    webpack系列-插件机制杂记

    前言

    webpack本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。或许我们在日常的开发需求中并不需要自己动手写一个插件,然而,了解其中的机制也是一种学习的方向,当插件出现问题时,我们也能够自己来定位。

    Tapable

    Webpack的插件机制依赖于一个核心的库, Tapable
    在深入webpack的插件机制之前,需要对该核心库有一定的了解。

    Tapable是什么

    tapable 是一个类似于nodejs 的EventEmitter 的库, 主要是控制钩子函数的发布与订阅。当然,tapable提供的hook机制比较全面,分为同步和异步两个大类(异步中又区分异步并行和异步串行),而根据事件执行的终止条件的不同,由衍生出 Bail/Waterfall/Loop 类型。

    Tapable的使用 (该小段内容引用文章

    基本使用

    const {
      SyncHook
    } = require('tapable')
    
    // 创建一个同步 Hook,指定参数
    const hook = new SyncHook(['arg1', 'arg2'])
    
    // 注册
    hook.tap('a', function (arg1, arg2) {
    	console.log('a')
    })
    
    hook.tap('b', function (arg1, arg2) {
    	console.log('b')
    })
    
    hook.call(1, 2)
    

    钩子类型
    image

    image

    BasicHook:执行每一个,不关心函数的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。

    BailHook:顺序执行 Hook,遇到第一个结果result!==undefined则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。

    什么样的场景下会使用到 BailHook 呢?设想如下一个例子:假设我们有一个模块 M,如果它满足 A 或者 B 或者 C 三者任何一个条件,就将其打包为一个单独的。这里的 A、B、C 不存在先后顺序,那么就可以使用 AsyncParallelBailHook 来解决:

    x.hooks.拆分模块的Hook.tap('A', () => {
       if (A 判断条件满足) {
         return true
       }
     })
     x.hooks.拆分模块的Hook.tap('B', () => {
       if (B 判断条件满足) {
         return true
       }
     })
     x.hooks.拆分模块的Hook.tap('C', () => {
       if (C 判断条件满足) {
         return true
       }
     })
    
    

    如果 A 中返回为 true,那么就无须再去判断 B 和 C。
    但是当 A、B、C 的校验,需要严格遵循先后顺序时,就需要使用有顺序的 SyncBailHook(A、B、C 是同步函数时使用) 或者 AsyncSeriseBailHook(A、B、C 是异步函数时使用)。

    WaterfallHook:类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook
    当一个数据,需要经过 A,B,C 三个阶段的处理得到最终结果,并且 A 中如果满足条件 a 就处理,否则不处理,B 和 C 同样,那么可以使用如下

    x.hooks.tap('A', (data) => {
       if (满足 A 需要处理的条件) {
         // 处理数据 data
         return data
       } else {
         return
       }
     })
    x.hooks.tap('B', (data) => {
       if (满足B需要处理的条件) {
         // 处理数据 data
         return data
       } else {
         return
       }
     })
     x.hooks.tap('C', (data) => {
       if (满足 C 需要处理的条件) {
         // 处理数据 data
         return data
       } else {
         return
       }
     })
    

    LoopHook:不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暂时没看到具体使用 Case)

    Tapable的源码分析

    Tapable 基本逻辑是,先通过类实例的 tap 方法注册对应 Hook 的处理函数, 这里直接分析sync同步钩子的主要流程,其他的异步钩子和拦截器等就不赘述了。

    const hook = new SyncHook(['arg1', 'arg2'])
    

    从该句代码, 作为源码分析的入口,

    class SyncHook extends Hook {
        // 错误处理,防止调用者调用异步钩子
    	tapAsync() {
    		throw new Error("tapAsync is not supported on a SyncHook");
    	}
        // 错误处理,防止调用者调用promise钩子
    	tapPromise() {
    		throw new Error("tapPromise is not supported on a SyncHook");
    	}
        // 核心实现
    	compile(options) {
    		factory.setup(this, options);
    		return factory.create(options);
    	}
    }
    

    从类SyncHook看到, 他是继承于一个基类Hook, 他的核心实现compile等会再讲, 我们先看看基类Hook

    // 变量的初始化
    constructor(args) {
    	if (!Array.isArray(args)) args = [];
    	this._args = args;
    	this.taps = [];
    	this.interceptors = [];
    	this.call = this._call;
    	this.promise = this._promise;
    	this.callAsync = this._callAsync;
    	this._x = undefined;
    }
    

    初始化完成后, 通常会注册一个事件, 如:

    // 注册
    hook.tap('a', function (arg1, arg2) {
    	console.log('a')
    })
    
    hook.tap('b', function (arg1, arg2) {
    	console.log('b')
    })
    

    很明显, 这两个语句都会调用基类中的tap方法:

    tap(options, fn) {
        // 参数处理
    	if (typeof options === "string") options = { name: options };
    	if (typeof options !== "object" || options === null)
    		throw new Error(
    			"Invalid arguments to tap(options: Object, fn: function)"
    		);
    	options = Object.assign({ type: "sync", fn: fn }, options);
    	if (typeof options.name !== "string" || options.name === "")
    		throw new Error("Missing name for tap");
    	// 执行拦截器的register函数, 比较简单不分析
    	options = this._runRegisterInterceptors(options);
    	// 处理注册事件
    	this._insert(options);
    }
    

    从上面的源码分析, 可以看到_insert方法是注册阶段的关键函数, 直接进入该方法内部

    _insert(item) {
        // 重置所有的 调用 方法
    	this._resetCompilation();
    	// 将注册事件排序后放进taps数组
    	let before;
    	if (typeof item.before === "string") before = new Set([item.before]);
    	else if (Array.isArray(item.before)) {
    		before = new Set(item.before);
    	}
    	let stage = 0;
    	if (typeof item.stage === "number") stage = item.stage;
    	let i = this.taps.length;
    	while (i > 0) {
    		i--;
    		const x = this.taps[i];
    		this.taps[i + 1] = x;
    		const xStage = x.stage || 0;
    		if (before) {
    			if (before.has(x.name)) {
    				before.delete(x.name);
    				continue;
    			}
    			if (before.size > 0) {
    				continue;
    			}
    		}
    		if (xStage > stage) {
    			continue;
    		}
    		i++;
    		break;
    	}
    	this.taps[i] = item;
    }
    }
    

    _insert主要是排序tap并放入到taps数组里面, 排序的算法并不是特别复杂,这里就不赘述了, 到了这里, 注册阶段就已经结束了, 继续看触发阶段。

    hook.call(1, 2)  // 触发函数
    

    在基类hook中, 有一个初始化过程,

    this.call = this._call; 
    
    Object.defineProperties(Hook.prototype, {
    	_call: {
    		value: createCompileDelegate("call", "sync"),
    		configurable: true,
    		writable: true
    	},
    	_promise: {
    		value: createCompileDelegate("promise", "promise"),
    		configurable: true,
    		writable: true
    	},
    	_callAsync: {
    		value: createCompileDelegate("callAsync", "async"),
    		configurable: true,
    		writable: true
    	}
    });
    

    我们可以看出_call是由createCompileDelegate生成的, 往下看

    function createCompileDelegate(name, type) {
    	return function lazyCompileHook(...args) {
    		this[name] = this._createCall(type);
    		return this[name](...args);
    	};
    }
    

    createCompileDelegate返回一个名为lazyCompileHook的函数,顾名思义,即懒编译, 直到调用call的时候, 才会编译出正在的call函数。

    createCompileDelegate也是调用的_createCall, 而_createCall调用了Compier函数

    _createCall(type) {
    	return this.compile({
    		taps: this.taps,
    		interceptors: this.interceptors,
    		args: this._args,
    		type: type
    	});
    }  
    compile(options) {
    	throw new Error("Abstract: should be overriden");
    }
    

    可以看到compiler必须由子类重写, 返回到syncHook的compile函数, 即我们一开始说的核心方法

    class SyncHookCodeFactory extends HookCodeFactory {
    	content({ onError, onResult, onDone, rethrowIfPossible }) {
    		return this.callTapsSeries({
    			onError: (i, err) => onError(err),
    			onDone,
    			rethrowIfPossible
    		});
    	}
    }
    
    const factory = new SyncHookCodeFactory();
    
    class SyncHook extends Hook {
        ...
    	compile(options) {
    		factory.setup(this, options);
    		return factory.create(options);
    	}
    }
    

    关键就在于SyncHookCodeFactory和工厂类HookCodeFactory, 先看setup函数,

    setup(instance, options) {
      // 这里的instance 是syncHook 实例, 其实就是把tap进来的钩子数组给到钩子的_x属性里.
      instance._x = options.taps.map(t => t.fn);
    }
    

    然后是最关键的create函数, 可以看到最后返回的fn,其实是一个new Function动态生成的函数

    create(options) {
      // 初始化参数,保存options到本对象this.options,保存new Hook(["options"]) 传入的参数到 this._args
      this.init(options);
      let fn;
      // 动态构建钩子,这里是抽象层,分同步, 异步, promise
      switch (this.options.type) {
        // 先看同步
        case "sync":
          // 动态返回一个钩子函数
          fn = new Function(
            // 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在
            // 注意这里this.args返回的是一个字符串,
            // 在这个例子中是options
            this.args(),
            '"use strict";
    ' +
              this.header() +
              this.content({
                onError: err => `throw ${err};
    `,
                onResult: result => `return ${result};
    `,
                onDone: () => "",
                rethrowIfPossible: true
              })
          );
          break;
        case "async":
          fn = new Function(
            this.args({
              after: "_callback"
            }),
            '"use strict";
    ' +
              this.header() +
              // 这个 content 调用的是子类类的 content 函数,
              // 参数由子类传,实际返回的是 this.callTapsSeries() 返回的类容
              this.content({
                onError: err => `_callback(${err});
    `,
                onResult: result => `_callback(null, ${result});
    `,
                onDone: () => "_callback();
    "
              })
          );
          break;
        case "promise":
          let code = "";
          code += '"use strict";
    ';
          code += "return new Promise((_resolve, _reject) => {
    ";
          code += "var _sync = true;
    ";
          code += this.header();
          code += this.content({
            onError: err => {
              let code = "";
              code += "if(_sync)
    ";
              code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));
    `;
              code += "else
    ";
              code += `_reject(${err});
    `;
              return code;
            },
            onResult: result => `_resolve(${result});
    `,
            onDone: () => "_resolve();
    "
          });
          code += "_sync = false;
    ";
          code += "});
    ";
          fn = new Function(this.args(), code);
          break;
      }
      // 把刚才init赋的值初始化为undefined
      // this.options = undefined;
      // this._args = undefined;
      this.deinit();
    
      return fn;
    }
    

    最后生成的代码大致如下, 参考文章

    "use strict";
    function (options) {
      var _context;
      var _x = this._x;
      var _taps = this.taps;
      var _interterceptors = this.interceptors;
    // 我们只有一个拦截器所以下面的只会生成一个
      _interceptors[0].call(options);
    
      var _tap0 = _taps[0];
      _interceptors[0].tap(_tap0);
      var _fn0 = _x[0];
      _fn0(options);
      var _tap1 = _taps[1];
      _interceptors[1].tap(_tap1);
      var _fn1 = _x[1];
      _fn1(options);
      var _tap2 = _taps[2];
      _interceptors[2].tap(_tap2);
      var _fn2 = _x[2];
      _fn2(options);
      var _tap3 = _taps[3];
      _interceptors[3].tap(_tap3);
      var _fn3 = _x[3];
      _fn3(options);
    }
    

    ok, 以上就是Tapabled的机制, 然而本篇的主要对象其实是基于tapable实现的compile和compilation对象。不过由于他们都是基于tapable,所以介绍的篇幅相对短一点。

    compile

    compile是什么

    compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。

    也就是说, compile是webpack的整体环境。

    compile的内部实现

    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>} */
          ......
          ......
          some code
        };
        ......
        ......
        some code
    }
    

    可以看到, Compier继承了Tapable, 并且在实例上绑定了一个hook对象, 使得Compier的实例compier可以像这样使用

    compiler.hooks.compile.tapAsync(
      'afterCompile',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);
    
        // 使用 webpack 提供的 plugin API 操作构建结果
        compilation.addModule(/* ... */);
    
        callback();
      }
    );
    

    compilation

    什么是compilation

    compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

    compilation的实现

    class Compilation extends Tapable {
    	/**
    	 * Creates an instance of Compilation.
    	 * @param {Compiler} compiler the compiler which created the compilation
    	 */
    	constructor(compiler) {
    		super();
    		this.hooks = {
    			/** @type {SyncHook<Module>} */
    			buildModule: new SyncHook(["module"]),
    			/** @type {SyncHook<Module>} */
    			rebuildModule: new SyncHook(["module"]),
    			/** @type {SyncHook<Module, Error>} */
    			failedModule: new SyncHook(["module", "error"]),
    			/** @type {SyncHook<Module>} */
    			succeedModule: new SyncHook(["module"]),
    
    			/** @type {SyncHook<Dependency, string>} */
    			addEntry: new SyncHook(["entry", "name"]),
    			/** @type {SyncHook<Dependency, string, Error>} */
    		}
    	}
    }
    

    具体参考上面提到的compiler实现。

    编写一个插件

    了解到tapablecompilercompilation之后, 再来看插件的实现就不再一头雾水了
    以下代码源自官方文档

    class MyExampleWebpackPlugin {
      // 定义 `apply` 方法
      apply(compiler) {
        // 指定要追加的事件钩子函数
        compiler.hooks.compile.tapAsync(
          'afterCompile',
          (compilation, callback) => {
            console.log('This is an example plugin!');
            console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);
    
            // 使用 webpack 提供的 plugin API 操作构建结果
            compilation.addModule(/* ... */);
    
            callback();
          }
        );
      }
    }
    

    可以看到其实就是在apply中传入一个Compiler实例, 然后基于该实例注册事件, compilation同理, 最后webpack会在各流程执行call方法。

    compiler和compilation一些比较重要的事件钩子

    compier

    事件钩子 触发时机 参数 类型
    entry-option 初始化 option - SyncBailHook
    run 开始编译 compiler AsyncSeriesHook
    compile 真正开始的编译,在创建 compilation 对象之前 compilation SyncHook
    compilation 生成好了 compilation 对象,可以操作这个对象啦 compilation SyncHook
    make 从 entry 开始递归分析依赖,准备对每个模块进行 build compilation AsyncParallelHook
    after-compile 编译 build 过程结束 compilation AsyncSeriesHook
    emit 在将内存中 assets 内容写到磁盘文件夹之前 compilation AsyncSeriesHook
    after-emit 在将内存中 assets 内容写到磁盘文件夹之后 compilation AsyncSeriesHook
    done 完成所有的编译过程 stats AsyncSeriesHook
    failed 编译失败的时候 error SyncHook

    compilation

    事件钩子 触发时机 参数 类型
    normal-module-loader 普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数。 loaderContext module SyncHook
    seal 编译(compilation)停止接收新模块时触发。 - SyncHook
    optimize 优化阶段开始时触发。 - SyncHook
    optimize-modules 模块的优化 modules SyncBailHook
    optimize-chunks 优化 chunk chunks SyncBailHook
    additional-assets 为编译(compilation)创建附加资源(asset)。 - AsyncSeriesHook
    optimize-chunk-assets 优化所有 chunk 资源(asset)。 chunks AsyncSeriesHook
    optimize-assets 优化存储在 compilation.assets 中的所有资源(asset) assets AsyncSeriesHook

    总结

    插件机制并不复杂,webpack也不复杂,复杂的是插件本身..
    另外, 本应该先写流程的, 流程只能后面补上了。

    引用

    不满足于只会使用系列: tapable
    webpack系列之二Tapable
    编写一个插件
    Compiler
    Compilation
    compiler和comnpilation钩子
    看清楚真正的 Webpack 插件

  • 相关阅读:
    Linux驱动学习时间、延迟及延缓操作3
    Windows 系统下Git安装图解
    [整理]Android Intent和PendingIntent的区别
    C++篇实现MD5算法
    重温数据结构——(2)
    重温数据结构——(1)
    红黑树——1.介绍与查找
    Ubuntu Telnet 服务
    文本框垂直居中
    文本框透明无边框
  • 原文地址:https://www.cnblogs.com/Darlietoothpaste/p/10474871.html
Copyright © 2011-2022 走看看