两只小蚂蚁

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Webpack的核心原理应该是基于Tapable的支持复杂发布订阅流程控制的工具,内置大量的plugin,再结合loader做资源编译。

先上图:

1. 编译入口

version:webpack@4.46.0

webpack的编译命令一般是这样的:

webpack --config webpack.config.js

该命令会执行node_modules/webpack/bin/webpack.js文件

//webpack/bin/webpack.js
//...line:150 判断有cli
else if (installedClis.length === 1) {
	const path = require("path");
	const pkgPath = require.resolve(`${installedClis[0].package}/package.json`);
	// eslint-disable-next-line node/no-missing-require
	const pkg = require(pkgPath);
	// eslint-disable-next-line node/no-missing-require
  // 这里会加载webpack-cli/bin/cli.js
	require(path.resolve(
		path.dirname(pkgPath),
		pkg.bin[installedClis[0].binName]
	));
}

//webpack-cli/bin/cli.js 整体为IIFE
(function() {
  //此处忽略参数检查,环境检查等预处理
  ...
  		function processOptions(options) {
			...
			//忽略其他参数,关注编译入口
			const webpack = require("webpack");
			let lastHash = null;
			let compiler;
			try {
        //webpack初始化
				compiler = webpack(options);
			} catch (err) {
				...
			}
			...
      //如果没有配置中没有watch
			if (firstOptions.watch || options.watch) {
				...
			} else {
        //webpack执行
				compiler.run((err, stats) => {
					if (compiler.close) {
						compiler.close(err2 => {
							compilerCallback(err || err2, stats);
						});
					} else {
						compilerCallback(err, stats);
					}
				});
			}
		}
  	//实际调用
		processOptions(options);
})();

在执行compiler.run之前,先看看webpack初始化做了些什么。

2. Webpack初始化

//webpack/lib/webpack.js
const webpack = (options, callback) => {
	...
	let compiler;
	if (Array.isArray(options)) {
		compiler = new MultiCompiler(
			Array.from(options).map(options => webpack(options))
		);
	} else if (typeof options === "object") {
    //将用户本地的配置文件拼接上webpack内置的参数
		options = new WebpackOptionsDefaulter().process(options);
    //初始化compiler对象
		compiler = new Compiler(options.context);
		compiler.options = options;
    //注册NodeEnvironmentPlugin插件
		new NodeEnvironmentPlugin({
			infrastructureLogging: options.infrastructureLogging
		}).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);
				}
			}
		}
    //触发environment和afterEnvironment上注册的事件
		compiler.hooks.environment.call();
		compiler.hooks.afterEnvironment.call();
    //注册webpack内置插件,源码如下
		compiler.options = new WebpackOptionsApply().process(options, compiler);
	} else {
		throw new Error("Invalid argument: options");
	}
	//回调函数处理
  ...
	return compiler;
};

2.1 Compiler初始化

Compiler继承自Tapable,而Tapable是一个基于发布/订阅的复杂事件流注册及调用框架。这里不展开Tapable的实现细节。

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
      //初始化所有钩子--省略
			...
			beforeRun: new AsyncSeriesHook(["compiler"]),
			run: new AsyncSeriesHook(["compiler"])
      ...
		};
    //注册Compiler钩子更改options为异步
		this._pluginCompat.tap("Compiler", options => {
					...
					options.async = true;
			}
		});
		//内部变量初始化赋值
		this.name = undefined;
		this.outputPath = ""; 
    ...
	}
  //方法定义
	watch:fn;
  run:fn;
	compile:fn;
	...
}

2.2 内置Plugin注册

以target=web为例,会加载以下模板,此处省略大量代码,所以webpack体量并不小

class WebpackOptionsApply extends OptionsApply {
  process(options, compiler) {
		...
			switch (options.target) {
				case "web":
					JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
					FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin");
					NodeSourcePlugin = require("./node/NodeSourcePlugin");
					new JsonpTemplatePlugin().apply(compiler);
					new FetchCompileWasmTemplatePlugin({
						mangleImports: options.optimization.mangleWasmImports
					}).apply(compiler);
					new FunctionModulePlugin().apply(compiler);
					new NodeSourcePlugin(options.node).apply(compiler);
					new LoaderTargetPlugin(options.target).apply(compiler);
					break;
				case "webworker": 
				case "node":
				case "async-node":
				case "electron-renderer":
				case "electron-preload":
				default:
			}
		}
  	//这个plugin就是传说中生成AST语法树的acorn库所在的地方
    //AST太过底层,这里就不深入了
  	new JavascriptModulesPlugin().apply(compiler);
    ...
    //注册的对应钩子调用
		compiler.hooks.entryOption.call(options.context, options.entry);
    ...
    compiler.hooks.afterPlugins.call(compiler);
		...
		compiler.hooks.afterResolvers.call(compiler);
		return options;
	}
}

3. Webpack执行编译

//以下代码已省略hook调用逻辑
class Compiler extends Tapable {
	run(callback) {
    ...
		const startTime = Date.now();
		this.running = true;

		const onCompiled = (err, compilation) => {
			//done钩子调用
			if (this.hooks.shouldEmit.call(compilation) === false) {
					return finalCallback(null, stats);
			}
			//注入资源
			this.emitAssets(compilation, err => {
				if (compilation.hooks.needAdditionalPass.call()) {
							//这里可能会触发再次编译
							this.compile(onCompiled);
					return;
				}
				...
			});
		};
		//钩子调用
		...
		//调用compile方法
		this.compile(onCompiled);
	}
  
  compile(callback) {
    //Compilation参数生成
		const params = this.newCompilationParams();
    //钩子调用
    ...
			//构建单次Compilation对象实例,调用finish和seal方法
			const compilation = this.newCompilation(params);
			//注意:SingleEntryPlugin和MultiEntryPlugin都订阅了make钩子
			this.hooks.make.callAsync(compilation, err => {
				compilation.finish(err => {
					compilation.seal(err => {
							return callback(null, compilation);
				});
			});
		});
	}
}

class SingleEntryPlugin {
	apply(compiler) {
		compiler.hooks.compilation.tap(
			"SingleEntryPlugin",
			(compilation, { normalModuleFactory }) => {
				compilation.dependencyFactories.set(
					SingleEntryDependency,
					normalModuleFactory
				);
			}
		);
		//订阅hooks.make消息并触发addEntry
		compiler.hooks.make.tapAsync(
			"SingleEntryPlugin",
			(compilation, callback) => {
				const { entry, name, context } = this;

				const dep = SingleEntryPlugin.createDependency(entry, name);
				compilation.addEntry(context, dep, name, callback);
			}
		);
	}
}

所以最核心的工作还是由Compilation来完成的。

4. Compilation单次编译

class Compilation extends Tapable {
	constructor(compiler) {
		this.hooks = {
			buildModule: new SyncHook(["module"]),
			rebuildModule: new SyncHook(["module"]),
      //省略其他hook
      ...
    }
    //变量初始化
    this.modules = []; //记录了所有解析后的模块
		this.chunks = []; //记录了所有chunk
    this.assets = {}; //记录了所有要生成的文件
    ...
  }
  getModule:fn,
  waitForBuildingFinished:fn,
  buildModule:fn,
  ...
  //看来finish并没有做什么事情
  finish(callback) {
		const modules = this.modules;
		this.hooks.finishModules.callAsync(modules, err => {
			for (let index = 0; index < modules.length; index++) {
				const module = modules[index];
				this.reportDependencyErrorsAndWarnings(module, [module]);
			}
			callback();
		});
	}
  
	addEntry(context, entry, name, callback) {
    this._addModuleChain(...){
      this.addModule(...);
    	this.buildModule(...);
    }
  }
}

5. Loader的调用

其中的module是由NormalModuleFactory或者MultiModuleFactory创建的NormalModule或MultiModule。

//NormalModule.js
class NormalModule extends Module {
  build(options, compilation, resolver, fs, callback) {
  	return this.doBuild(options, compilation, resolver, fs, err => {...})
  }
  doBuild(options, compilation, resolver, fs, callback) {
  	const loaderContext = this.createLoaderContext(...);
  	runLoaders(
			{
				resource: this.resource,
				loaders: this.loaders,
				context: loaderContext,
				readResource: fs.readFile.bind(fs)
			},
			(err, result) => {...}
		);
  }
}

6. Seal过程

  1. 优化我们编译过后的代码(代码混淆、分包优化等等)

  2. 生成我们最后需要的打包过后的文件

class Compilation extends Tapable {
  ...
  //seal过程,省略大部分hook调用
  seal(callback) {
    ...
		for (const preparedEntrypoint of this._preparedEntrypoints) {
			const module = preparedEntrypoint.module;
			const name = preparedEntrypoint.name;
			const chunk = this.addChunk(name);
			const entrypoint = new Entrypoint(name);
			entrypoint.setRuntimeChunk(chunk);
			entrypoint.addOrigin(null, name, preparedEntrypoint.request);
			this.namedChunkGroups.set(name, entrypoint);
			this.entrypoints.set(name, entrypoint);
			this.chunkGroups.push(entrypoint);

			GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
			GraphHelpers.connectChunkAndModule(chunk, module);

			chunk.entryModule = module;
			chunk.name = name;

			this.assignDepth(module);
		}
		buildChunkGraph(
			this,
			/** @type {Entrypoint[]} */ (this.chunkGroups.slice())
		);
		this.sortModules(this.modules); 
		this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
			...
			this.applyModuleIds(); 
			this.sortItemsWithModuleIds(); 
			this.applyChunkIds(); 
			this.sortItemsWithChunkIds();
			this.createHash();
			this.createModuleAssets();
			if (this.hooks.shouldGenerateChunkAssets.call() !== false) {
				this.hooks.beforeChunkAssets.call();
				this.createChunkAssets();
			}
			this.summarizeDependencies();
			...
		});
	}
}

7. 结束

以上就是整个webpack源码的大致流程,由于细节太多省略很多hook的通知代码。

由此可知Compiler和Compilation的关系是:

  • 每个 Webpack 的配置,对应一个 Compiler 对象,记录着整个 Webpack 的生命周期;
  • 在构建的过程中,每次构建都会产生一次Compilation实例,Compilation 则是构建周期的产物。

根据以上流程就知道,如果自己需要写一个Plugin或者loader应该怎么去做了。

7.1 Plugin

  1. 只需要提供apply方法供webpack加载
  2. 订阅对应的hook
  3. 在hook触发后处理自己需要的逻辑,如更改输出内容,额外输出内容等等
const { compilation } = require("webpack");

const pluginName="consolePlugin";
class consolePlugin{
    apply(compiler){
        compiler.hooks.run.tap(pluginName,compilation=>{
            console.log("The webpack build progress is starting!!!");
            // 这个实现的功能就是简单的打印这句话
        })
    }
}
module.exports=consolePlugin;

7.2 Loader

来个最简单的,在所有匹配的模块最后加个end的注释,如下:

Loader可以很简单但是也可以很复杂,因为编译原理可不是一个简单的东西,比如babel,因为这些不在webpack本身,这里就不说了。

module.exports = source =>{
    var result = source + " //end";
    return result;
}
posted on 2021-12-27 18:32  两只小蚂蚁  阅读(148)  评论(0编辑  收藏  举报