MangiDu / blog

0 stars 0 forks source link

从另一个角度了解Webpack:一个事件流插件系统 #13

Open MangiDu opened 5 years ago

MangiDu commented 5 years ago

由于本人能力有限,理解和描述难免会有偏差和错误,希望大家不吝指正。

(本文提及webpack的版本为4.29.x,其依赖于tapable的版本是1.1.0)

刚才提及的tapable这个名字应该会令大部分同学觉得陌生,但webpack这个目前使用率最高的打包工具就人人皆知了吧。 tapable实际上也是webpack团队的项目,它暴露了很多钩子类,并且可以被用于给插件创建钩子。 我认为tapable可以被称为webpack重要的基石之一,看过webpack解析博文的同学应该知道CompilerCompilation对象在其中的重要作用,而这二者都直接继承自tapable中提供的Tapable对象,并且直接使用了tapable提供的钩子对象的API。 (当然最新版本的tapable选择不再对外提供Tapable而仅暴露钩子类,猜测可能是针对webpack5.0做了优化和开发)

—— 什么?你说你没看过相关解析,也不知道CompilerCompilation他俩做了啥?那少年你大概有点out了... —— 什么?平时需求多太忙了没空看?好吧,我原谅你...

...还好我有一丢丢准备,不过会很简略,将就着看吧

webpack构建流程简述

webpack启动时,会一次性地初始化一个编译器Compiler的实例,在这个实例中包含了所有可自定义操作的配置(在插件自定义的子编译流程中,可以通过这个Compiler实例拿到主环境的所有信息),Compiler实例根据配置生成Compilation实例对象,Compilation实例代表了一次单一版本构建和生成编译资源的过程,其中包含了编译资源的信息,这个信息是个对象,以键值对的形式记录了所有静态资源的相关信息,最终由Compilation实例生成bundle。

webpack的事件流插件系统

这一节拆开分成事件流和插件系统两部分来讲。

先说插件系统【大喘气】背后的设计原理

亲,控制反转再多了解一下?

控制反转(Inversion of Control,缩写为IoC,也被称为依赖注入,Dependency Injection,DI)是面向对象编程中的一种设计原则,可以用来降低代码之间的耦合度,提高组件的可移植性。

传统应用架构中,低层组件设计成被高层组件使用,我们常在高层组件中使用new关键字来直接使用低层组件,这种结构可以逐步构建一个复杂的应用,但是高层组件直接依赖于低层组件去实现,将会限制高层次组件重用的可能性。

控制反转将低层组件从高层组件中分离,为其定义或持有高层组件所必须的行为和服务的接口,由公共的外部容器在运行时将高层组件作为某种依赖关系动态的注入低层组件中。传统结构中高层组件对低层组件的直接使用变成低层组件针对高层接口完成具体实现,此时是低层次组件直接依赖于高层次组件,控制权被反转。

看图说话更清楚~

示例图 高层对象A依赖于底层对象B的实现: 高层对象A依赖于底层对象B的实现

把高层对象A对底层对象的需求抽象为接口A,底层对象实现了接口A: 把高层对象A对底层对象的需求抽象为接口A,底层对象实现了接口A

如果还是觉得好像窗户纸还没被捅破,就再举个栗子好了~绝对生动形象!

情景:弯姐想去约会

class Girl {
    construtor({name = '佚名', age = 0}) {
        this.name = name
        this.age = age
    }
    date() {
        // 想约会需要一个男同学哇
        // 这个男同学怎么获取呢
    }
}

let 弯姐 = new Girl({name: '刘弯', age: 16})
弯姐.date()

Round 1:弯姐需要自己找,而且弯姐得对人家知根知底还要负一辈子责任,emmm...这样不好

class Girl {
    construtor({name = '佚名', age = 0}) {
        this.name = name
        this.age = age
    }
    date() {
        let boy = this.boy = new Boy()
        boy.date()
    }
}

let 弯姐 = new Girl({name: '刘弯', age: 16})
弯姐.date()

Round 2:兵哥和佳宇给弯姐介绍认识的男生,但是这种情况就会导致弯姐想约会就总得缠着他俩,他俩得一直为弯姐介绍,太辛苦了,也不好

class Girl {
    construtor({name = '佚名', age = 0}) {
        this.name = name
        this.age = age
    }
    date() {
        let boy = this.boy = 兵哥.introduceBoy() || 佳宇.introduceClassmate('boy')
        boy.date()
    }
}

let 弯姐 = new Girl({name: '刘弯', age: 16})
弯姐.date()

Round 3:弯姐不想自己找了,她这次坐等,告诉大家她需要一个男同学和她去约会,而且要求这个男生对姑娘得主动点,让大家帮她安排

class Girl {
    construtor({name = '佚名', age = 0}) {
        this.name = name
        this.age = age
    }
    date() {
        // 逛街吃饭看电影...
    }
}
class Boy {
    construtor({name = '佚名', age = 0}) {
        this.name = name
        this.age = age
    }
    dateWith(girl) {
        girl.date()
    }
}

let 弯姐 = new Girl({name: '刘弯', age: 16})
let 小哥哥 = new Boy({name: '小哥哥', age: 18})
小哥哥.dateWith(弯姐)

对于插件系统这个模式来讲,控制反转做到的则是在运行时动态地为当前插件提供其需求的主依赖对象,并且做到二者间的解耦。

回头看webpack,人家这方面做得非常好【但是会有负作用:主流程对插件注册、调用是分离的,导致插件的完整生命流程被切碎,源码阅读困难度+++】

webpack的插件系统本质?

其实webpack本质上就是一个插件系统。为什么这么说?前面我提到了CompilerCompilation都直接继承自tapable中提供的Tapable对象并且直接使用了其他钩子对象。 而在webpack内部就定义了大量了自有插件,供CompilerCompilation使用。

这一点从源码看可能会更清晰一些,如果翻看webpacklib/webpackOptionsApply.js这个文件会发现五百多行代码里四百行都是插件的声明和注册 :joy:

事件流又是个什么情况?

Q: 这一小节的标题是webpack的事件流插件系统,这个事件流是啥样的?
A: Compiler Hooks-依照事件注册顺序 Compilation Hooks-依照事件注册顺序 lib/Compiler.jslib/Compilation.js的源码稍微瞅瞅~

Q: webpack的插件是怎么运行起来的呢?跟事件流有什么关系? A: 看tapable的API去!

tapable部分钩子解析

(只准备梳理Hook、HookCodeFactory,和SyncBailHook、SyncHook、AsyncParallelHook、AsyncSeriesHook这几种被用到的钩子)

Hook和HookCodeFactory,这俩货是其他所有钩子对象的基础

简单情况下:生成的SyncHook代码 使用情景

let hook = new SyncHook(['name'])
hook.tap('second', (name) => {console.log(name)})
hook.tap({
    name: 'first',
    before: 'second'
}, (name) => {console.log(name)})

hook.call('first name')

setup中设置了_x“隐私”属性

instance._x = options.taps.map(t => t.fn);

生成执行代码前,在_insert方法中利用taps的before和stage的值进行比较并排序(决定执行顺序) 生成的执行代码

function (name) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(name);
    var _fn1 = _x[1];
    _fn1(name);
}

SyncBailHook

function (name) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    var _result0 = _fn0(name);
    if (_result0 !== undefined) {
        return _result0;;
    } else {
        var _fn1 = _x[1];
        var _result1 = _fn1(name);
        if (_result1 !== undefined) {
            return _result1;;
        } else {}
    }
}

注意需要使用tapAsync AsyncParallelHook

function (name, _callback) {
    "use strict";
    var _context;
    var _x = this._x;
    do {
        var _counter = 2;
        var _done = () => {
            _callback();
        };
        if (_counter <= 0) break;
        var _fn0 = _x[0];
        _fn0(name, _err0 => {
            if (_err0) {
                if (_counter > 0) {
                    _callback(_err0);
                    _counter = 0;
                }
            } else {
                if (--_counter === 0) _done();
            }
        });
        if (_counter <= 0) break;
        var _fn1 = _x[1];
        _fn1(name, _err1 => {
            if (_err1) {
                if (_counter > 0) {
                    _callback(_err1);
                    _counter = 0;
                }
            } else {
                if (--_counter === 0) _done();
            }
        });
    } while (false);
}

AsyncSeriesHook

function (name, _callback) {
    "use strict";
    var _context;
    var _x = this._x;
    var _fn0 = _x[0];
    _fn0(name, _err0 => {
        if (_err0) {
            _callback(_err0);
        } else {
            var _fn1 = _x[1];
            _fn1(name, _err1 => {
                if (_err1) {
                    _callback(_err1);
                } else {
                    _callback();
                }
            });
        }
    });
}

Q: 外部声明的插件是什么时候被注册的呢?
A: 以最常见的导出单一对象的配置方式说明,在初始化Compiler对象之后较早的时候就被注册了。

webpack.js:37

options = new WebpackOptionsDefaulter().process(options);

compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
            plugin.call(compiler, compiler);
        } else {
            plugin.apply(compiler);
        }
    }
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);

Q: 他们怎么去兼顾自有插件和第三方插件的呢?
A: 亲,乖乖再看一遍上面的内容哦 【手动微笑

更多分析

待续...

参考