10. 「webpack源码分析」webpack中涉及了哪些设计模式呢?

416 阅读9分钟

通过一个demo带你深入进入webpack@4.46.0源码的世界,分析构建原理,专栏地址,共有十篇。


经过前面9节的介绍,这里应该来个总结,....

然后应该是从webpack来获取哪些好的编程技能,大致如下

  • 性能相关
    • 并发,如Semaphore的引入
    • 懒执行,如 replaceSource中的replacements,tapable中懒编译
  • 算法相关
    • 递归转非递归: buildChunkGraph中的visitModuels
    • 计算多个集合的交集
  • 设计模式
    • 装饰器模式
    • 责任链模式
    • 模板方法模式
    • 工厂方法模式

下面重点说下被应用了的设计模式

由于小册不是重点说设计模式,下面并不会特别详细的介绍各设计模式的使用场景、实现、优缺点等。在进入下面内容之前你应该了解相关的设计模式的实现。

装饰器模式

装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构,属于结构型模式,它是作为现有的类的一个包装。该模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

装饰器模式理解参考,下面说下webpack中使用该模式的案例。

在webpack中存储模块内容的类有很多种,分别有不同的功能。并在webpack将这些类抽出到一个库中webpack-sources

image.png

其中Source为其他XxxSource的父类,该类是一个抽象类

class Source {
   source() {
      throw new Error("Abstract");
   }   
   //...
}

下面看下模块在构建过程中涉及的几处XxxSource。

先是在模块的构建时module.doBuild -> runLoaders -> createSource(),createSource可能会返回多种类型的Source赋值给属性_source,假设这里返回OriginalSource。

// NormalModule.js
doBuild(options, compilation, resolver, fs, callback) {
   const loaderContext = this.createLoaderContext(...);
   runLoaders(..., () => { /*...*/ this._source = this.createSource(...); })
} 

// 可能返回多种类型OriginalSource、SourceMapSourced,...
// 假设这里返回 OriginalSource
createSource(source, resourceBuffer, sourceMap) {
   if (!this.identifier) {
      return new RawSource(source);
   }
   //...
}

在createChunkAssets生成最终需要输出文件内容的时候,会调用normalModule.source()来生成模块的最终内容,逻辑如下。

// NormalModule.js
source(dependencyTemplates, runtimeTemplate, type = "javascript") {
   //... 缓存逻辑,如果有缓存并且hashDigest未变化则直接返回   
   const source = this.generator.generate(...);
   //...
   const cachedSource = new CachedSource(source);
   return cachedSource; // new CachedSource
}

originalSource() {
   return this._source;
} 

// JavascriptGenerator.js
generate(module, dependencyTemplates, runtimeTemplate) {
   const originalSource = module.originalSource(); 
   const source = new ReplaceSource(originalSource);
   this.sourceBlock(...);
   return source;
}

那normalModule.source()最终返回的source如下所示:

new CacheSource(new ReplaceSource(new OriginalSource(...));)

CacheSource和ReplaceSource在这里都是装饰类,对传入的source进行能力增强,显然ReplaceSource提供了对原始source进行修改的能力,CacheSource提供了缓存能力

下面看下作为装饰器的ReplaceSource是如何增强的

class OriginalSource extends Source {
    constructor(value, name) {
        super();
        this._value = value;
        this._name = name;
    }

    source() {
        return this._value;
    }
}

class ReplaceSource extends Source {
    constructor(source, name) {
        super();
        this._source = source;
        this._name = name;
        this.replacements = [];
    }

    replace(start, end, newValue, name) {
        this.replacements.push(new Replacement(start, end, newValue, this.replacements.length, name));
    }

    insert(pos, newValue, name) {
        this.replacements.push(new Replacement(pos, pos - 1, newValue, this.replacements.length, name));
    }

    source(options) {
        // 关键: 
        // 1. 首先是获取_source.source()
        // 2. 能力增强: _replaceString应用修改
        return this._replaceString(this._source.source());
    }

    _replaceString(str) {        
        this._sortReplacements();
        var result = [str];
        this.replacements.forEach(function (repl) {
            //...
        }, this);

        //...
        return resultStr;
    }

}

使用方式如下:

const originalSource = new OriginalSource('test data');
const replaceSource = new ReplaceSource(originalSource);

// 修改原始内容
replaceSource.insert(...);
replaceSource.replace(...)

// 获取修改后内容
replaceSource.source() 

看到ReplaceSource通过包装原有Source(如这里的OriginalSource)实现功能增强,并且由于二者都实现了Source父类,因此对外接口一致。

通常扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。比如这里如果采用继承实现缓存功能,则需要给RawSource、OriginalSource等等都去继承一个子类如CacheRawSource、CacheOriginalSource等,导致子类膨胀。

另外:装饰者模式和代理模式在实现上有些接近并且都是处于增强的目的,可以研究下异同点。

责任链模式

责任链是一种行为设计模式,可让您沿着处理程序链传递请求。收到请求后,每个处理程序决定要么处理请求,要么将其传递给链中的下一个处理程序。责任链模式理解参考。

normalModuleFactory创建normalModule实例过程中有说到normalResolver解析资源路径的过程,具体过程在enhanced-resolve库中分析了。

小册demo中的normalResolver会涉及下面这些插件,每个插件都有各自的功能,实际上就是责任划分。enhanced-resolve支持多种配置就是通过这种责任划分实现,最终通过一个链将各个责任进行连接。

image.png

看到每个插件都有source和target属性,其中target就是用来确定下一步的去向。整体执行流程的衔接由resolver.doResolve(...)实现。

注:通常标准的实现中每个责任者会持有下一个责任者的引用,由当前责任显示调用下一个责任者的处理。实际上,但是这里的关键在于职责分离,而后存在链式调用以形成执行链,至于如何实现这个链式能力,并不是固定的。 比如Java Web中Servlet Filter的链式调用( 参考),就是通过控制中心ApplicationFilterChain.doFilter方法实现的,和enhanced-resolve这里的实现思路几乎一致。

模板方法模式

在该模式中一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。

模板方法模式理解参考,下面说下webpack中使用该模式的案例。

tapable章节说到hook最终的执行代码是动态生成的。其中执行代码生成的过程就使用到了模板模式。先看下代码生成的过程。

// Hook.js
compile(options) {
   throw new Error("Abstract: should be overriden");
}

// HookCodeFactory.js
compile(options) {
   factory.setup(this, options);
   return factory.create(options);
}

// 只实现了content方法,create方法继承自父类HookCodeFactory
class SyncBailHookCodeFactory extends HookCodeFactory {
   content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) {
       //...
   }
}

class HookCodeFactory {
    create(options) {
        //..
        switch (this.options.type) {
            case "sync":
                fn = new Function(
                    this.args(),
                    '"use strict";\n' +
                    this.header() +
                    this.content({ /*...*/ })
                );
                break;
                //...
        }
        return fn;
    }
    
    args (...) { /*...*/ }
    
    header (...) { /*...*/ }
}

代码生成逻辑在codeFactory类中,当调用hook.call()时会调用到hook.compile()方法来动态生成代码。动态生成的过程是一致的,因此收敛到父类HookCodeFactory的create方法中,在子类中只是实现差异化的部分如这里的content(...)。

看到这里HookCodeFactory提供了执行模板new Function部分,子类实现差异化部分content(...)

该模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

工厂方法模式

在前面的小节中有说到模块实例的创建时提到两个类即NormalModuleFactory和NormalModule。创建一个NormalModule需要很多的信息(loaders,resource,parser,generator等等),看到NormalModule是一个复杂对象。

如果在Compilation中直接创建NormalModule实例,将获取这些信息的步骤放置在Compilation显然不够优雅(复杂,并且破坏职责单一原则),因此这里引入一个工厂屏蔽创建实例的细节。

如果只是看normalModuleFactory对于创建的normalModule过程的封装,可以认为是简单工厂模式。但是在webpack内部是存在多种类型的模块的如DllModule、MultiModule等,当然也会有对应的DllModuleFactory、MultiModuleFactory,对于这种场景使用工厂方法模式,如下。

// Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
    // ...
    // 工厂方法模式
    const moduleFactory = this.dependencyFactories.get(Dep);
    moduleFactory.create(...); // XxxModuleFactory
}

// NormalModuleFactory.js
create(data, callback) {
    //.... 很长的一段代码,做了很多工作
    
    new NormalModule(...);
}

其中dependencyFactories可能会获取到多种模块工厂(normalModuleFactory、dllModuleFactory、multiModuleFactory)。

这里可以进一步优化为:简单工厂模式 + 工厂方法模式。参考

// 新增抽象类
class ModuleFactory () {
    create (){
        // throw new Error("abstract method");
    }  
    
    // 简单工厂模式
    static getModule (...) {
        // 工厂方法模式:找到具体工厂后,调用工厂方法create创建模块
        const moduleFactory = this.dependencyFactories.get(Dep);
        return moduleFactory.create(...); // XxxModuleFactory
    }
}

// Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
    // 调用端通过简单工厂模式进行创建
    ModuleFactory.getModule(...)
}

// class NormalModuleFactory extends ModuleFactory
// class DllModuleFactory extends ModuleFactory
// ...

给ModuleFactory增加一个静态方法static getModule实现简单工厂模式(一个工厂类,ModuleFactory),而在该静态方法内部实现工厂方法模式(多个工厂类)

由于我们这里只有一个抽象产品类Module,该模式属于工厂方法模式,如果存在多个抽象产品类则需要使用抽象工厂模式。


工厂模式分为三种,参考

  1. 简单工厂:作用就是把对象的创建放到一个工厂类中,通过参数来创建不同的对象,其缺点是一旦有了新的实现类,就需要修改工厂实现,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
  2. 工厂方法模式:解决了简单工厂不易扩展的问题。它可以创建一个工厂接口和多个工厂实现类,这样如果增加新的功能,只需要添加新的工厂类就可以,不需要修改之前的代码。另外,工厂方法模式还可以和模板方法模式结合一起,将他们共同的基础逻辑抽取到父类中,其它的交给子类去实现。工厂方法模式针对场景是:只有一个抽象产品类(比如上面案例中的Module),具体工厂类只能创建一个具体产品类的实例。
  3. 抽象工厂模式:它能创建一系列相关的对象,而无需指定其具体类。抽象工厂模式有多个抽象产品类,具体工厂类可以创建多个具体产品类的实例。

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:cloud.tencent.com/developer/s…