Webpack原生插件bannerPlugin的源码简析

@CoderMing 2018-07-18 04:35:16发表于 CoderMing/blog Webpack源码解读

最近被问到了bannerPlugin的实现方法,这是要考察我们对于webpack的运行机制的了解程度。然而我一点也没了解过,于是就有了下文👇

webpack插件的格式

官方链接:编写一个插件
我们知道插件在webpack的配置文件中是通过 plugins数组来声明,数组里面均为插件对象所创建的实例:

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    filename: 'vendor-[hash].min.js',
  }),
  new ExtractTextPlugin({
    filename: 'build.min.css',
    allChunks: true,
  }),
  ...
]

下面这张图是官方给的插件的demo:

// 一个 JavaScript 命名函数。
function MyExampleWebpackPlugin() {

};

// 在插件函数的 prototype 上定义一个 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定一个挂载到 webpack 自身的事件钩子。
  compiler.plugin('webpacksEventHook', function(compilation /* 处理 webpack 内部实例的特定数据。*/, callback) {
    console.log("This is an example plugin!!!");

    // 功能完成后调用 webpack 提供的回调。
    callback();
  });
};

我们来分析下插件的执行过程:

  1. webpack.config.js引入插件构造函数,然后在plugins参数中创建实例,同时将配置项传入构造函数中。
  2. 插件的构造函数被调用,它会根据传入的参数创建对应的实例。
  3. 插件的原型链上的apply函数被调用,用来注册各种钩子函数。(源码地址:webpack/Compiler.js at master · webpack/webpack · GitHub)(但这里叫做childCompiler,我没找到其它执行插件挂载的函数的地方)
  4. 运行阶段会触发这些钩子函数,然后进行相应的处理。

BannerPlugin源码简析

首先上地址:webpack/BannerPlugin.js at master · webpack/webpack · GitHub
首先我们可以看到这个插件的大致格式并非官方demo的function构造函数,而是**用了ES6引入的Class语法糖。这种格式对于写构造函数来说是非常方便易懂的。**推荐大家以后都用这种格式写构造函数。
加下来,我们简单地看下头部有哪些包,看不懂没关系,接着往下看,看到引用的格式大致就懂了。

首先我要讲下wrapComment这个函数:

const wrapComment = str => {
	if (!str.includes("\n")) {
		return Template.toComment(str);
	}
	return `/*!\n * ${str
		.replace(/\*\//g, "* /")
		.split("\n")
		.join("\n * ")}\n */`;
};

很明显能看出来这是将你要展示的文字包装成注释的函数。大致的流程是:先判断文字是否是一行,如果是一行,直接调用Template.toComment函数即可。如果不是的话,就手动拼接成块级的注释代码。

其次再看constructor构造函数:

constructor(options) {
		if (arguments.length > 1) {
			throw new Error(
				"BannerPlugin only takes one argument (pass an options object)"
			);
		}

		validateOptions(schema, options, "Banner Plugin");

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

		this.options = options || {};

		if (typeof options.banner === "function") {
			const getBanner = this.options.banner;
			this.banner = this.options.raw
				? getBanner
				: data => wrapComment(getBanner(data));
		} else {
			const banner = this.options.raw
				? this.options.banner
				: wrapComment(this.options.banner);
			this.banner = () => banner;
		}
	}

首先对options进行判断,只允许出现一个参数。
然后validateOptions这个函数的大致作用是验证参数是否合法。不过我不是很了解schema的知识,也就无力分析了。
接下来进行类型判断。如果传入参数是string or options的话就走下去,不然的话就没有banner了(不会报错)。
在接下来就在实例对象下挂载参数。这里做了个判断,我们可以猜想出raw的作用是判断字符串是否已经被包裹成注释的格式,没包裹的话就包裹一遍。如果是函数的话就执行一道,字符串的话就直接挂载。

接下来看核心的apply函数:

apply(compiler) {
		const options = this.options;
		const banner = this.banner;
		const matchObject = ModuleFilenameHelpers.matchObject.bind(
			undefined,
			options
		);

		compiler.hooks.compilation.tap("BannerPlugin", compilation => {
			compilation.hooks.optimizeChunkAssets.tap("BannerPlugin", chunks => {
				for (const chunk of chunks) {
					if (options.entryOnly && !chunk.canBeInitial()) {
						continue;
					}

					for (const file of chunk.files) {
						if (!matchObject(file)) {
							continue;
						}

						let basename;
						let query = "";
						let filename = file;
						const hash = compilation.hash;
						const querySplit = filename.indexOf("?");

						if (querySplit >= 0) {
							query = filename.substr(querySplit);
							filename = filename.substr(0, querySplit);
						}

						const lastSlashIndex = filename.lastIndexOf("/");

						if (lastSlashIndex === -1) {
							basename = filename;
						} else {
							basename = filename.substr(lastSlashIndex + 1);
						}

						const data = {
							hash,
							chunk,
							filename,
							basename,
							query
						};

						const comment = compilation.getPath(banner(data), data);

						compilation.assets[file] = new ConcatSource(
							comment,
							"\n",
							compilation.assets[file]
						);
					}
				}
			});
		});
	}

前面几行拿局部变量缓存参数,然后ModuleFilenameHelpers 这个我查了下,大概的作用是确保文件格式正确。
然后就是hooks了。首先是绑定了解析配置文件期间调用BannerPlugin函数的hooks,然后又绑定了编译途中生成文件时的hooks。
然后在这个hooks内有个循环,这个循环里,有很多操作了:
第一个判断语句可能是多入口要做的一些事,然而我并不懂hhh
第二个for里面,是对文件进行操作的函数了。大概是做很多判断以及匹配,然后找到对应的bundle文件。找到对应文件后,将compilation.assets[file]的内容进行替换,加入前面的注释。

大概这个插件的流程就是这些了~

看完这些发现,其实webpack还有好多东西,短期的速成是没有效果的,要成为一个“webpack高级工程师”,还需要深挖点源码,还需要动手多实现点东西。(主要还是太菜了