webpack plugin源码解析(三) banner-plugin

文章目录

  • 作用
  • 涉及 webpack API
    • 判断是否为入口 chunk
    • 判断文件名是否匹配 ModuleFilenameHelpers.matchObject
    • 创建注释内容 Template.toComment
    • 模版字符串生成 compilation.getPath
    • 合并文件内容
  • 实现
    • constructor
    • apply

作用

  • 在文件头部|尾部插入自定义的注释内容
new webpack.BannerPlugin({
    banner: 'hello world',
    raw: boolean, // true,不会变成注释
  	entryOnly: boolean, // 只针对入口 chunk
  	test: string | RegExp | [string, RegExp], // Include all modules that pass test assertion.
  	include: string | RegExp | [string, RegExp], // Include all modules matching any of these conditions.
  	exclude: string | RegExp | [string, RegExp], // Exclude all modules matching any of these conditions.
  	footer?: boolean, // true,插在文件尾部
}),

涉及 webpack API

判断是否为入口 chunk

for (const chunk of compilation.chunks) {
	if(chunk.canBeInitial()){ // 判断入口 chunk
	  // ...
	}
}

判断文件名是否匹配 ModuleFilenameHelpers.matchObject

ModuleFilenameHelpers.matchObject(optisn,filename) // filename: 'main.js'

// 实现
ModuleFilenameHelpers.matchObject = (obj, str) => {
	if (obj.test) {
		if (!ModuleFilenameHelpers.matchPart(str, obj.test)) {
			return false;
		}
	}
	if (obj.include) {
		if (!ModuleFilenameHelpers.matchPart(str, obj.include)) {
			return false;
		}
	}
	if (obj.exclude) {
		if (ModuleFilenameHelpers.matchPart(str, obj.exclude)) {
			return false;
		}
	}
	return true;
};

ModuleFilenameHelpers.matchPart = (str, test) => {
	if (!test) return true;
	test = asRegExp(test);
	if (Array.isArray(test)) {
		return test.map(asRegExp).some(regExp => regExp.test(str));
	} else {
		return test.test(str);
	}
};

// 将字符串转换成正则表达式
const asRegExp = test => {
	if (typeof test === "string") {
		test = new RegExp("^" + test.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")); // 将字符串变成正则表达式,匹配给定字符串中的和正则相关的特殊字符,统一在前面加上\进行转译
		// $&:表示前面正则匹配到的内容
		// \\:表示转译 \,使得其不能转译 $,从而使 '*.' => '/\*\./'
	}
	return test;
};

创建注释内容 Template.toComment

Template.toComment

static toComment(str) {
	if (!str) return "";
	return `/*! ${str.replace(/\*\//g;, "* /")} */`;
}

模版字符串生成 compilation.getPath

let res=compilation.getPath((data)=>`dawdaa ${data.name}`,{name:'gg'})
// "dawdaa gg"

合并文件内容

const { ConcatSource } = require("webpack-sources");

compilation.updateAsset(file, old => {
	let cached = cache.get(old);
	if (!cached || cached.comment !== comment) {
		const source = options.footer
			// 合并文件内容
			? new ConcatSource(old, "\n", comment)
			: new ConcatSource(comment, "\n", old);
		cache.set(old, { source, comment });
		return source;
	}
	return cached.source;
});

实现

constructor

class BannerPlugin {
	constructor(options) {
		if (typeof options === "string" || typeof options === "function") {
			options = {
				banner: options
			};
		}

		validate(options);

		this.options = options;

		const bannerOption = options.banner;
		if (typeof bannerOption === "function") {
			const getBanner = bannerOption;
			this.banner = this.options.raw
				? getBanner
				: data => wrapComment(getBanner(data));
		} else {
			const banner = this.options.raw // false 会将字符串变成注释
				? bannerOption
				: wrapComment(bannerOption);
			this.banner = () => banner;
		}
	}
}

wrapComment

  • 将字符串改造成注释
const wrapComment = str => {
	if (!str.includes("\n")) {
		return Template.toComment(str);  '/*! hello world */'
	}
	return `/*!\n * ${str  // 将一个字符串转换成一个多行注释的格式
		.replace(/\*\//g, "* /")
		.split("\n")
		.join("\n * ")
		.replace(/\s+\n/g, "\n")
		.trimEnd()}\n */`;
 	
 	// 比如:
 	//'Hello, world!\nThis is a test.';

	/*!
	 * Hello, world!
	 * This is a test.
	 */
	};

apply

apply(compiler) {
	const options = this.options;
	const banner = this.banner;
	// 文件名匹配规则方法
	const matchObject = ModuleFilenameHelpers.matchObject.bind(
		undefined,
		options
	);
	const cache = new WeakMap(); // 从这里创建缓存可以看出 apply 只执行一次,后续重新编译只会执行 tap 注册的方法

	compiler.hooks.compilation.tap("BannerPlugin", compilation => {
	compilation.hooks.processAssets.tap(
		{
			name: "BannerPlugin",
			stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
		},
		() => {
		    // 遍历生成的 chunk
			for (const chunk of compilation.chunks) {
				if (options.entryOnly && !chunk.canBeInitial()) {
					continue;
				}
				// 拿到每一个 chunk 包含的文件
				for (const file of chunk.files) {
				    // 匹配文件名和开发者传入的 test、include、exclude 配置
					if (!matchObject(file)) {
						continue;
					}
	 				// 要交给生成模版字符串的 data
					const data = {
						chunk,
						filename: file
					};
					// banner: () => 'hello world'
					// comment: "/*! hello world */"
					const comment = compilation.getPath(banner, data); 
					
					// 更新文件资源,插入注释信息
					compilation.updateAsset(file, old => {
						// 先读取缓存信息
						let cached = cache.get(old);
						// 如果没有添加过或者注释内容改变了,重新生成资源
						if (!cached || cached.comment !== comment) {
							const source = options.footer
								? new ConcatSource(old, "\n", comment)
								: new ConcatSource(comment, "\n", old);
							cache.set(old, { source, comment });
							// 返回更新后的文件内容
							return source;
						}
						// 返回缓存文件中的内容
						return cached.source;
					});
				}
			}
	}
}

你可能感兴趣的:(webpack,javascript,前端)