webpack 源码解读

536 阅读6分钟

by:沐沐

其实阅读 webpack 源码也是一个非常大的挑战,想说明清楚也是非常难,在我们团队做分享时候被 leader 点醒,画图下面一个图来形象的说明 webpack 核心部分内容,就先上一个图。

image-20220413092407115.png

注:图中的*,对于第一个图是接口,能够挂各种各样的设备来对水进行处理;第二个图是hooks,同步、异步、阻塞等各种类型hooks来对文件流进行处理

webpack本质:可以将其理解是一种基于事件流的编程范例,一系列的插件运行。

插件

这里先做一些先行步骤,先了解一些 webpack 用到的一些核心插件。

args

yargs主要用来处理参数系统插件(webpack-cli引入):提供命令和分组参数,分析命令行参数,对各个参数进行转换,组成编译配置项。

cli/command

对命令定制,解析参数,引用webpack,根据配置项进行编译和构建。

Tapable

主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。Tapable 库暴露了很多 Hook(钩子)类,为插件提供挂载的钩子

类似于 Node.js 的 EventEmitter 的库,主要是控制钩子函数的发布与订阅,控制着 webpack 的插件系统。暴露了很多 Hook(钩子)类,为插件提供挂载的钩子。

image-20220415193001553.png

注:这里可以想想本篇第一张对比图,是不是各种类型的钩子基本都可以在图中找到痕迹?

const {  
  SyncHook, //同步钩子  
  SyncBailHook, //同步熔断钩子  
  SyncWaterfallHook, //同步流水钩子  
  SyncLoopHook, //同步循环钩子  
  AsyncParallelHook, //异步并发钩子  
  AsyncParallelBailHook, //异步并发熔断钩子  
  AsyncSeriesHook, //异步串行钩子  
  AsyncSeriesBailHook, //异步串行熔断钩子  
  AsyncSeriesWaterfallHook //异步串行流水钩子 
} = require("tapable");

Compiler.js

class Compiler extends Tapable { 
  /* code */ 
}

Compilation.js

class Compilation extends Tapable {  
  /* code */ 
}

从执行命令开始

npm run start 后发生了什么?

开始执行

进入node_modules\.bin 查找 webpack.sh 或者 webpack.cmd 或者 webpack.ps1 文件(对于 windows 的 powershell,可以执行的是 ps1,这里应该是针对不同终端做的,比如 git 终端可以执行的是 shell)

#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent

$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
  # Fix case when both the Windows and Linux builds of Node
  # are installed in the same directory
  $exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
  & "$basedir/node$exe"  "$basedir/../webpack/bin/webpack.js" $args
  $ret=$LASTEXITCODE
} else {
  & "node$exe"  "$basedir/../webpack/bin/webpack.js" $args
  $ret=$LASTEXITCODE
}
exit $ret

最终执行文件是webpack.jswebpack/bin/webpack.js),然后可以查看源码:

process.exitCode = 0; // 1. 正常执行返回 const runCommand = (command, args) =>{...}; // 2. 运行某个命令 const isInstalled = packageName =>{...}; // 3. 判断某个包是否安装 const CLIs =[...]; // 4. webpack 可用的 cli: webpack-cli 和 webpack-command const installedClis = CLIs.filter(cli => cli.installed); // 5. 判断是否两个 cli 是否安装了 if (installedClis.length === 0){...}else if // 6. 根据安装数量进行处理 (installedClis.length)

发现这里会查找并进入 webpack-cli

webpack-cli

后面可以具体查看 webpack-cli/bin/config/config-yargs.js 等了解更多命令,这里主要使用了yargs进行参数处理

  1. Config options: 配置相关参数(文件名称、运行环境等)
  2. Basic options: 基础参数(entry 设置、debug 模式设置、watch 监听设置、devtool 设置)
  3. Module options: 模块参数,给 loader 设置扩展
  4. Output options: 输出参数(输出路径、输出文件名称)
  5. Advanced options: 高级用法(记录设置、缓存设置、监听频率、bail等)
  6. Resolving options: 解析参数(alias 和 解析的文件后缀设置)
  7. Optimizing options: 优化参数
  8. Stats options: 统计参数
  9. options: 通用参数(帮助命令、版本信息等)

webpack-cli 对配置文件和命令行参数进行转换最终生成配置选项参数 options,最终会根据配置参数实例化 webpack 对象,然后执行构建流程。

let compiler;
try {
	compiler = webpack(options);
} catch (err) {
	if (err.name === "WebpackOptionsValidationError") {
		if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
		else console.error(err.message);
		// eslint-disable-next-line no-process-exit
		process.exit(1);
	}
	throw err;
}

webpack

可以将其理解是一种基于事件流的编程范例,一系列的插件运行

进入webpack主要处理流程

webpack的编译都按照下面的钩子调用顺序执行

image-20220415193806705.png

开始

查看 webpack.js 可以发现,如果 options 是数组则调用 new MultiCompiler(),然后递归 webpack 方法;否则调用 new Compiler();然后compiler.options = new WebpackOptionsApply().process(options, compiler) 绑定 options。这里可以看到几个核心部分的关系。

let compiler;
  if (Array.isArray(options)) {
    compiler = new MultiCompiler(
      Array.from(options).map(options => webpack(options))
    );
  } else if (typeof options === "object") {
    options = new WebpackOptionsDefaulter().process(options);  // 参数处理

    compiler = new Compiler(options.context); // Compiler
    compiler.options = options;
    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);
        }
      }
    }
    compiler.hooks.environment.call();  // 环境
    compiler.hooks.afterEnvironment.call();
    compiler.options = new WebpackOptionsApply().process(options, compiler);  // 参数
  } else {
    throw new Error("Invalid argument: options");
  }

处理参数:

这里可以查看 webpack 源码中的 WebpackOptionsApply.js 文件,发现里边是各种的配置参数到 plugin 的转换。

// ...
const JavascriptModulesPlugin = require("./JavascriptModulesPlugin");
const JsonModulesPlugin = require("./JsonModulesPlugin");
// 各种plugin引入
class WebpackOptionsApply extends OptionsApply {
  process(options, compiler) {
    if (typeof options.target === "string") {
	  let JsonpTemplatePlugin;
	  let FetchCompileWasmTemplatePlugin;
	  let ReadFileCompileWasmTemplatePlugin;
	  // plugin 变量声明
      switch (options.target) {
        case "web":
          JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin");
          FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin");
          NodeSourcePlugin = require("./node/NodeSourcePlugin");
          // ...
      }
      // ...
    }
    // ...
  }
  // ...
}

开始编译:

compiler.run((err, stats) => {
	if (compiler.close) {
		compiler.close(err2 => {
			compilerCallback(err || err2, stats);
		});
	} else {
		compilerCallback(err, stats);
	}
});

编译

compiler.run:

可以具体查看 run 方法:

this.hooks.beforeRun.callAsync(this, err => {
	if (err) return finalCallback(err);
	this.hooks.run.callAsync(this, err => { // run
		if (err) return finalCallback(err);
		this.readRecords(err => {
			if (err) return finalCallback(err);
			this.compile(onCompiled);
		});
	});
});

onCompiled:

const onCompiled = (err, compilation) => { }

this.compile:

compile(callback) {
	const params = this.newCompilationParams();
	this.hooks.beforeCompile.callAsync(params, err => {
		if (err) return callback(err);
		this.hooks.compile.call(params);
		const compilation = this.newCompilation(params);
		this.hooks.make.callAsync(compilation, err => {  // make
			if (err) return callback(err);
			compilation.finish(err => {
				if (err) return callback(err);
				compilation.seal(err => {  // seal
					if (err) return callback(err);
					this.hooks.afterCompile.callAsync(compilation, err => {
						if (err) return callback(err);
						return callback(null, compilation);
					});
				});
			});
		});
	});
}

Compiler

相关 hooks 有,流程相关:

  • (before-)run
  • (before-/after-)compile
  • make
  • (after-)emit
  • done

监听相关:

  • watch-run
  • watch-close

也可以查看Compiler.js的constructor注意:

class Compiler extends Tapable {
	constructor(context) {
		super();
		this.hooks = {
			/** @type {SyncBailHook<Compilation>} */
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<Stats>} */
			
            /* 一堆的 hooks 定义 */
            
			/** @type {SyncBailHook<string, Entry>} */
			entryOption: new SyncBailHook(["context", "entry"])
		};
		// TODO webpack 5 remove this
		this.hooks.infrastructurelog = this.hooks.infrastructureLog;
		// ...
	}
}

Compilation

这也是 webpack 比较核心的一个功能,Compiler 调用 Compilation 生命周期方法,这里可以通过查看源码。

  • addEntryaddModuleChain(添加入口)
  • finish (上报模块错误)
  • sea(构建完成,内容输出、生成和优化这一块内容)
class Compiler extends Tapable {
 	run(callback) {
        // ...
		const onCompiled = (err, compilation) => {
			// ...
			this.emitAssets(compilation, err => {
				// ...
				if (compilation.hooks.needAdditionalPass.call()) {
					// ...
					this.hooks.done.callAsync(stats, err => {
						if (err) return finalCallback(err);

						this.hooks.additionalPass.callAsync(err => {
							if (err) return finalCallback(err);
							this.compile(onCompiled);
						});
					});
					return;
				}
				// ...
			});
		};

		this.hooks.beforeRun.callAsync(this, err => {
			if (err) return finalCallback(err);

			this.hooks.run.callAsync(this, err => {
				if (err) return finalCallback(err);

				this.readRecords(err => {
					if (err) return finalCallback(err);

					this.compile(onCompiled);
				});
			});
		});
    }
    // ...
	createCompilation() {
		return new Compilation(this);
	}

	newCompilation(params) {
		const compilation = this.createCompilation();
        // ...
		return compilation;
	}

	compile(callback) {
       // ...
		this.hooks.beforeCompile.callAsync(params, err => {
			if (err) return callback(err);

			this.hooks.compile.call(params);

			const compilation = this.newCompilation(params);

			this.hooks.make.callAsync(compilation, err => {
				if (err) return callback(err);

				compilation.finish(err => {
					if (err) return callback(err);

					compilation.seal(err => {
						if (err) return callback(err);

						this.hooks.afterCompile.callAsync(compilation, err => {
							if (err) return callback(err);

							return callback(null, compilation);
						});
					});
				});
			});
		});
   }
}

详细钩子可以查看 Compilation.js 源码中的 constructor

class Compilation extends Tapable {
	/**
	 * Creates an instance of Compilation.
	 * @param {Compiler} compiler the compiler which created the compilation
	 */
	constructor(compiler) {
		super();
		this.hooks = {
			/** @type {SyncHook<Module>} */
			buildModule: new SyncHook(["module"]),
			/** @type {SyncHook<Module>} */
			rebuildModule: new SyncHook(["module"]),
			 
            /* 同样一堆 hooks 定义 */
            
			/** @type {SyncBailHook<Chunk[]>} */
			optimizeExtractedChunksAdvanced: new SyncBailHook(["chunks"]),
			/** @type {SyncHook<Chunk[]>} */
			afterOptimizeExtractedChunks: new SyncHook(["chunks"])
		};
		// ...
	}
	// ...
}

ModuleFactory

开始进入模块构建阶段,Compiler 中会创建两个 ModuleFactory(模块工厂对象),分别是 NormalModuleFactoryContextModuleFactory(其实他们两个也是继承自 Tapable)。

createNormalModuleFactory() {
	const normalModuleFactory = new NormalModuleFactory(
		this.options.context,
		this.resolverFactory,
		this.options.module || {}
	);
	this.hooks.normalModuleFactory.call(normalModuleFactory);
	return normalModuleFactory;
}

createContextModuleFactory() {
	const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
	this.hooks.contextModuleFactory.call(contextModuleFactory);
	return contextModuleFactory;
}

newCompilationParams() {
	const params = {
		normalModuleFactory: this.createNormalModuleFactory(),
		contextModuleFactory: this.createContextModuleFactory(),
		compilationDependencies: new Set()
	};
	return params;
}

compile(callback) {
	const params = this.newCompilationParams();
	this.hooks.beforeCompile.callAsync(params, err => {
	});
}

NormalModuleFactory 是指比如代码中经常写 module.exports,直接导出,然后可以通过 requireimport 来引入;

ContextModuleFactory 是前面的一些路径之类的。

Module

包含比较多的模块,但是都是继承自 Module class,常见的有:

  • NormalModule(普通模块)
  • ContextModule./src/a
  • ExternalModulemodule.exports = jQuery
  • DelegatedModulemanifest
  • MultiModuleentry: ['a', 'b']

这里就是 Compiler 调用 Compilation 的各种方法,处理 js/ts 的模块(也就是 importrequest等),进行打包。

compile(callback) {
	const params = this.newCompilationParams();
	this.hooks.beforeCompile.callAsync(params, err => {
		if (err) return callback(err);
		this.hooks.compile.call(params);
		const compilation = this.newCompilation(params);
		this.hooks.make.callAsync(compilation, err => {  // make
			if (err) return callback(err);
			compilation.finish(err => {
				if (err) return callback(err);
				compilation.seal(err => {
					if (err) return callback(err);
					this.hooks.afterCompile.callAsync(compilation, err => {
						if (err) return callback(err);
						return callback(null, compilation);
					});
				});
			});
		});
	});
}

总结

回归头部第一张图片,其实 webpack 主流程就是类似于水流,不同地方挂载不同的 hooks,来对文件流进行处理,也就是 webpack 就是对 hooks 调用过程。

plugin

写过 plugin 会发现,写插件主要核心就是绑定一些 hooks,然后 webpack 主流程会在合适的时机来触发,达到对文件流的处理。

module

这一块其实就是对文件模块的解析、处理。

loader

这个是处理不同文件的加载方式,用来解析文件的,只有能正确解析的进来的文件才能当做文件流处理。

招贤纳士

青藤前端团队是一个年轻多元化的团队,坐落在有九省通衢之称的武汉。我们团队现在由 20+ 名前端小伙伴构成,平均年龄26岁,日常的核心业务是网络安全产品,此外还在基础架构、效率工程、可视化、体验创新等多个方面开展了许多技术探索与建设。在这里你有机会挑战类阿里云的管理后台、多产品矩阵的大型前端应用、安全场景下的可视化(网络、溯源、大屏)、基于Node.js的全栈开发等等。

如果你追求更好的用户体验,渴望在业务/技术上折腾出点的成果,欢迎来撩~ yan.zheng@qingteng.cn