webpack2源码装饰器模式

55 阅读3分钟

装饰器模式

装饰器模式向一个现有的对象添加新的功能,同时又不改变其结构,属于结构性模式。该模式创建了一个装饰类,用来包装原有的类,并给原有的类增加额外的功能。包装类和原有的类对外提供相同名称的接口,访问包装类就像访问原有类一样。如原有类有getNumber方法,则包装类也有getNumber方法,只不过包装类getNumber方法又加了额外的功能。 有一个Notifier类,用于发送email消息。如果想增加其他类型的发送消息, SMS、Facebook或Slash。则通常想到的是通过继承实现,继承email类,创建SMS子类、Facebook子类或Slash类。通过继承能够同时发送email和其他消息的一种。

339751113-e5673bb3-89d5-4c7c-80ea-fc4f56f6cfaa.png 如果想发送email消息,同时发送SMS、Facebook和Slash这3种消息的2种消息,则需要再创建支持另外两种消息的子类。这样会产生很多子类,导致子类膨胀。

339752365-1e00f625-2eed-4062-ba94-4efc2a9592e0.png 通过聚合的方式解决了子类膨胀问题,每一种消息有各自对应的类,通过依次包装扩展增加消息类型。此时只有email类,SMS类,Facebook类和Slash类。

  1. 如果想发送email和SMS消息,SMS类包装email类。new SMSNotifier(new emailNotifier())

339755336-806985e8-a993-44fb-b83a-a0dbf3ee5497.png

  1. 如果想发送email、SMS消息和Facebook消息,SMS类包装email类,Facebook类包装SMS类。 new FacebookNotifier(new SMSNotifier(new emailNotifier()))

339755358-042fdca8-0f4b-458f-b3c8-18c355959726.png

装饰器模式在webpack中的应用

下面看下模块在构建过程中涉及的几处XxxSource。 先是在模块的构建时module.doBuild -> runLoaders -> createSource(),createSource可能会返回多种类型的Source赋值给属性_source,假设这里返回OriginalSource。

// NormalModule.js
doBuild(options, compilation, resolver, fs, callback) {
	var module = this;
    //...
	runLoaders({/* */}, function(err, result) {
		
		source = asString(source);
                 //可能生成多种类型OriginalSource、SourceMapSourced,...
                 //假设这里生成 OriginalSource
		module._source = new OriginalSource(source, module.identifier());
		return callback();
	});
};

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

source(dependencyTemplates, outputOptions, requestShortener) {
	
        //...
        var source = new ReplaceSource(_source);
	//...
	return new CachedSource(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;
    }
}

function ReplaceSource(source, name) {
	Source.call(this);
	this._source = source;
	this._name = name;
	this.replacements = [];
}
ReplaceSource.prototype.source = function(options) {
	return this._replaceString(this._source.source());
};
ReplaceSource.prototype.replace = function(start, end, newValue) {
	this.replacements.push([start, end, newValue, this.replacements.length]);
};
ReplaceSource.prototype._replaceString = function(str) {
	var result = [str];
	this.replacements.forEach(function(repl) {
		var remSource = result.pop();
		var splitted1 = this._splitString(remSource, Math.floor(repl[1] + 1));
		var splitted2 = this._splitString(splitted1[0], Math.floor(repl[0]));
		result.push(splitted1[1], repl[2], splitted2[0]);
	}, this);
	result = result.reverse();
	return result.join("");
};

使用方式如下:

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

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

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

看到ReplaceSource通过包装原有Source(如这里的OriginalSource)实现功能增强,并且由于二者都实现了Source父类,因此对外接口一致。 通常扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。比如这里如果采用继承实现缓存功能,则需要给RawSource、OriginalSource等等都去继承一个子类如CacheRawSource、CacheOriginalSource等,导致子类膨胀。