【前端打包工具webpack】插件机制的核心源码解读

195 阅读5分钟

tapable 简单介绍

  • 本身是基于发布订阅模式思想的实现
  • 能够不依赖业务并在业务不同阶段注册事件去做某些事情
  • webpack的plugin就是以tapable为核心实现的
  • tapable是一个单独的包,也就意味着可以使用在任何适合的项目中

总体API概览

tapable-api.png

SyncHook 使用

const { SyncHook } = require('tapable');
// 创建实例
const hook = new SyncHook(['arg1', 'arg2']);
// 注册事件1
hook.tap('event1', (arg1, arg2) => { console.log('arg1:',arg1) });
// 注册事件2
hook.tap('event2', (arg1, arg2) => { console.log('arg2:', arg2) });
// 执行事件
hook.call('参数一''参数二');
// 打印结果
arg1:参数一 
arg2:参数二
  1. 创建 SyncHook 的实例对象
  2. 调用实例tap注册事件
  3. 调用实例call方法, 会执行上一步的回调

源码解读

"use strict"
// 引入父类 Hook
const Hook = require('./Hook');
// 引入基础类 HookCodeFactory
const HookCodeFactory = require('./HookCodeFactory');
// 此处可先忽略,后面会讲到
class SyncHookCodeFactory extends HookCodeFactory {
    content({ onError, onDone, rethrowIfPossible }) {
        return this.callTapsSeries({
            onError: (i, err) => onError(err),
            onDone,
            rethrowIfPossible
        })
    }
}
const factory = new SyncHookCodeFactory();
// 此处可暂时忽略
// const TAP_ASYNC = () => {
//    throw new Error("tapAsync is ... SyncHook")
// }
// 此处可暂时忽略
// const TAP_PROMISE = () => {
//    throw new Error("tapPromise is ... SyncHook")
// }
// 覆盖父类 Hook 中的compile 方法
const COMPILE = function(options) {
    factory.setup(this, options);
    return factory.create(options);
}
// 定义 SyncHook 构造函数
function SyncHook(args = [], name = undefined) {
    const hook = new Hook(args, name);
    hook.constructor = SyncHook;
    // hook.tapAsync = TAP_ASYNV;
    // hook.tapPromise = TAP_PROMISE;
    hook.compile = COMPILE:
    return hook;
}
// 原型链置为null, 保护原型的干净
SyncHook.prototype = null;
module.exports = SyncHook;

SyncHook 的构造函数核心三个逻辑

  1. 创建 Hook 实例
  2. 覆盖 Hook 实例 compile 方法
  3. 返回 Hook 实例

其他逻辑: 在继承 HookCodeFactory 的 SyncHookCodeFactory 类中添加 content 方法

注:想要理解这段代码,就要弄明白 Hook类以及HookCodeFactory类都做了些什么,为了方便理解,这里只列出有用到的方法和属性

Hook 类,也是几个主要API的父类

// 方便理解,这里会在源码基础上做些删减
const CALL_DELEGATE = function(...args) {
    this.call = this._createCall("sync");
    return this.call(...args);
};
class Hook {
    constructor(args = [], name = undefined) {
        // 接收实例化时传入的第一个参数
        this._args = args;
        // 接收实例化时传入的第二个参数
        this.name = name;
        // this.taps = [];
        this._call = CALL_DELEGATE;
        this.call = CALL_DELEGATE;
        // this._x = underfined;
        this.compile = this.compile;
        this.tap = this.tap;
    }
    compile(options) {
        throw new Error("Abstract: should be overridden")
    }
    _createCall(type) {
        return this.compile({
            taps: this.taps,
            interceptors: this.interceptors,
            args: this._args,
            type:type,
        })
    }
    // type参数可选范围 sync(同步)/async(异步)/promise(异步)
    _tap(type, options, fn) {
        // 参数非空判断省略
        if (typeof options === 'string') {
            options = {
                name: options.trim()
            }
        }
        options = Object.assign({ type, fn }, options);
        // options = this._runRegisterInterceptors(options);
        this._insert(options);
    }
    // 这里可能会有有疑惑,为什么不直接把 _tap 的函数体写在这里? 原因如下:
    // 因为 Hook 类是核心 API 的父类,核心 API 中有同步也有异步
    // 所以也要定义异步的注册事件的方法例如:tapAsync
    tap(options, fn) {
        this._tap('sync', options, fn)
    }
    // tapAsync(options, fn) {
    //     this._tap("async", options, fn);
    // }
    // 将_call 赋值给call
    _resetCompilation() {
        this.call = this._call;
    }
    _insert(item) {
        this._resetCompilation();
        // before/stage 逻辑先忽略,核心逻辑如下
        let i = this.taps.length;
        // 当this.taps为[]时,此时while不会走
        while (i > 0) {
             i--; 
             const x = this.taps[i]; // 获取this.taps末位下标的值
             this.taps[i + 1] = x; // 相当于this.taps数组push了自身的末位下标值
             i++; // 将下标改为原有this.taps的长度值(也就是末位下标+1)
             break;
        }
        // 将传入的参数赋值给this.taps数组的某个下标
        this.taps[i] = item;
    }
}

结合SyncHook 的使用做逻辑梳理

  1. 创建Hook 实例时,在实例上挂载了一些属性和方法
  2. 注册事件tap方法时,实际是调用this._insert方法,修改this.taps的值
  3. 执行call方法时,实际是调用this.compile返回的函数,并将参数传入

注:this.compile并不是Hook类中定义的compile方法,而是SyncHook类中定义的COMPILE方法,该方法涉及到了HookCodeFactory类

HookCodeFactory 类

class HookCodeFactory {
    constructor(config) {
        this.config = config;
        this.options = undefined;
        this._args = undefined;
    }
    setup(instance, options) {
        instance._x = options.taps.map(t => t.fn)
    }
    create(options) {
        this.init(options);
        let fn;
        switch(this.options.type) {
            case 'sync': 
                fn = new Function(
                    this.args(), 
                    '"use strict";\n' +
                    this.header() +
                    this.contentWithInterceptors({
                        onError: err => `throw ${err};\n`,
                        onResult: result => `return ${result};\n`,
                        resultReturns: true,
                        onDone: () => "",
                        rethrowIfPossible: true
                    })
                );
                break;
            // ... 其他先暂时省略
        }
        this.deinit();
        return fn;
    }
    init(options) {
        this.options = options;
        this._args = options.args.slice()
    }
    // 将 this._args 转为字符串,并返回该字符串
    args({before, after} = {}) {
        let allArgs = this._args;
        if (allArgs.length === 0) {
            return ''
        }
        return allArgs.join(', ')
    }
    // 返回一个拼接的字符串
    header() {
        // 其他分支逻辑先忽略
        let code = '';
        code += 'var _context;\n';
        code += 'var _x = this._x;\n';
        return code;
    }
    contentWithInterceptors(options) {
        // 忽略其他逻辑
        // this.content 就是SyncHookCodeFactory类中定义的方法
        return this.content(options)
    }
    // 其实是调用了HookCodeFactory中的callTapsSeries方法
    // content({ onError, onDone, rethrowIfPossible }) {
    //    return this.callTapsSeries({
    //            onError: (i, err) => onError(err),
    //            onDone,
    //            rethrowIfPossible
    //    });
    // }
    // 这里为了方便理解,去掉了创建异步函数体的兼容代码等,只保留关键代码
    callTapsSeries({
         // 这个方法是调用create方法 new Function()时穿过来的onDone
         onDone, 
         // ...其他先省略
    }) {
        let code = '';
        let current = onDone;
        // 循环注册事件回调函数的数组,并根据下标拼接字符串
        for(let j = this.options.taps.length - 1; j >= 0; j--) {
            let i = j;
            const done = current;
            const content = this.callTap(i, { onDone });
            current = () => content;
        }
        code += current();
        return code;
    }
    // 这里不关心注册事件回调函数的内部逻辑
    // 只是拼接成函数执行的字符串,并将参数传递进去
    // 拼接后的字符串例子:_fn1('参数一', '参数二')
    callTap(tapIndex, {onDone}) {
        let code = '';
        code += `var _fn${tapIndex} = _x[${idx}];\n`;
        const tap = this.options.taps[tapIndex];
        switch(tap.type) {
            case 'sync':
                // 只是拼接成函数执行的字符串,并将参数传递进去
                code += `_fn${tapIndex}(${this.args()});\n`;
                if (onDone) {
                    code += onDone();
                }
                break;
        }
        return code;
    }
}

结合使用的例子分析 SyncHookCodeFactory

  1. SyncHook模块加载就会实例化SyncHookCodeFactory
  2. SyncHook的实例注册事件阶段SyncHookCodeFactory基本无关
  3. SyncHook的实例调用call方法时SyncHookCodeFactory的逻辑
  • 执行setup方法,将注册事件的回调函数收集到数组中,并赋给SyncHookCodeFactory实例_x属性
  • 接着执行SyncHookCodeFactory实例create方法调用new Function创建函数
  • 执行new Function, 传入参数,调用callTapsSeries 最终生成所有注册事件的回调函数执行字符串

到这里SyncHook的基本使用就梳理完了,为了方便理解,放一张流程图给大家

tapable.synchook.png

总结

  • Hook类作为父类,主要将注册事件的回调函数相关信息挂载到taps属性上
  • HookCodeFactory 主要就是根据Hook的taps的属性动态生成一个函数,函数体内就是taps的所有函数执行的字符串代码
  • SyncHook 覆盖Hook的compile方法,在HookCodeFactory类上添加content方法
  1. 每一个类都是有自己独立的功能,从代码设计上来说做到了解耦
  2. 使用类的extends方法实现在父类的基础上增加属性以及方法
  3. this指针的巧妙变化
  4. 动态创建函数

注:建议手写下源码,更容易理解这种思路以及代码设计技巧