Skip to content

Latest commit

 

History

History
278 lines (209 loc) · 7.68 KB

mark.md

File metadata and controls

278 lines (209 loc) · 7.68 KB

webpack编译主流程初窥

本文基于webpack5.0.0-beta.7, 通过debug源码的方式,了解webpack的编译主流程,仅供参考。

准备工作

git clone [email protected]:webpack/webpack.git

为了不污染源码,在根目录新建debug文件夹📃,并创建如下文件:

  • 编译代码:
// debug/src/index.js

const name = "webpack";
console.log("很高兴认识你,", name);
  • webpack配置:
// debug/webpack.config.js

const path = require("path");
module.exports = {
	context: __dirname,
	mode: "development",
	devtool: "source-map",
	entry: "./src/index.js",
	output: {
		path: path.join(__dirname, "./dist")
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				use: ["babel-loader"],
				exclude: /node_modules/
			}
		]
	}
};

  • 启动文件:
// debug/start.js

const webpack = require("../lib/index.js"); // 直接使用源码中的webpack函数
const config = require("./webpack.config");
const compiler = webpack(config);
compiler.run((err, stats) => {
	if (err) {
		console.error(err);
	} else {
		console.log(stats);
	}
});

  • 在vscode配置debug入口,如下:
{
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动webpack调试程序",
      "program": "${workspaceFolder}/debug/start.js"
    }
  ]
}

至此,点击▶️,看看调试程序是否正常运行(若正常运行,会在debug/dist自动生成打包后的js文件)

接下来,就可以在源码进行断点调试啦~~ 🤔


源码解读

compiler.run()

cd webpack, 打开package.json, 找到main字段,可以看到webpack的主入口在lib/index.js

当调用了const compiler = webpack(config),其实就是创建一个Compiler实例,在这里我们发现,Compiler在webpack运行过程中,仅此创建一次,贯穿整个打包过程,下面为主要逻辑代码。

const createCompiler = options => {
    // ...
	const compiler = new Compiler(options.context);
	compiler.options = options;
	if (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);    
	return compiler;
};

当准备工作做好,内部钩子相关联的事件处于蓄势待发状态,接下来把我们进入Compiler, 看里面的run方法是什么回事🤔?

最终定位到this.compile(), 通过callAsync触发钩子,逻辑代码如下:

this.hooks.make.callAsync(compilation, err => {
    //...
	compilation.finish(err => {
		compilation.seal(err => {
            //...
			this.hooks.afterCompile.callAsync(compilation, err => {
				return callback(null, compilation);
			});
		});
	});
});

这里需要提下Tapable,Tapable的核心功能是根据不同钩子将注册的事件在触发时按序进行,是典型的发布者/订阅模式,在Tapable的帮助下,原本杂乱无章的事件,能够得到有效的控制。

process(options, compiler)

当调用this.hooks.make.callAsync其实触发了什么?为了找到答案,我们在vscode全局搜索hooks.make.tapAsync, 发现lib/EntryPlugin.js下有如下逻辑:

成熟的开源库,往往作者大佬进行了非常优雅的封装,代码的模块依赖也非常错中复杂,依靠搜索关键词可以快速找到答案。

compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
	const { entry, name, context } = this;

	const dep = EntryPlugin.createDependency(entry, name);
	compilation.addEntry(context, dep, name, err => {
		callback(err);
	});
});

先不管这里的逻辑是怎样的,思考下EntryPlugin是什么时候调用的?我们继续通过关键词new EntryPlugin, 在lib/EntryOptionPlugin.js找到EntryPlugin在此进行了实例化,继续回溯搜索new EntryOptionPlugin, 最终发现在lib/WebpackOptionsApply.js发现它的身影。

回到最初createCompiler方法,如下,compiler.options = new WebpackOptionsApply().process(options, compiler)这行代码,make钩子在process函数中就已经注册好了, 把一切准备工作都关联了起来。

const createCompiler = options => {
    // ...
	const compiler = new Compiler(options.context);
	compiler.options = options;
	if (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);    
	return compiler;
};

回到lib/EntryPlugin.js看看compiler.hooks.make.tapAsync都干了啥。其实就是运行compiliation.addEntry方法,继续探索compiliation.addEntry。

addEntry(context, entry, name, callback) {
    this.hooks.addEntry.call(entry, name);
    // entryDependencies中的每一项都代表了一个入口,打包输出就会有多个文件
    let entriesArray = this.entryDependencies.get(name)
	entriesArray.push(entry)
    this.addModuleChain(context, entry, (err, module) => {
        this.hooks.succeedEntry.call(entry, name, module);
        return callback(null, module);
    })
}

通过debug找出函数的调用顺序

this.addEntry --> this.addModuleChain --> this.handleModuleCreation --> this.addModule --> this.buildModule --> this._buildModule --> module.build(this指代compiliation)

在/lib/NormalModule.js下找到build方法,返回doBuild方法

build(options, compilation, resolver, fs, callback) {

		return this.doBuild(options, compilation, resolver, fs, err => {
			try {
				const result = this.parser.parse(
					this._ast || this._source.source(),
					{
						current: this,
						module: this,
						compilation: compilation,
						options: options
					},
					(err, result) => {
						if (err) {

						} else {
							handleParseResult(result);
						}
					}
				);
				if (result !== undefined) {
					// parse is sync
					handleParseResult(result);
				}
			} catch (e) {
				
			}
		});
	}

doBuild方法其实就是用适合的loader去加载模块资源,webpack只能识别js文件,通过doBuild后, 所有文件都转换了js文件。

接着通过传入的callback,执行this.parser.parse,this.parser是JavascriptParser的实例, 最终调用parse调用第三方库acorn对js源代码进行词法解析。

parse(code, options){ // 调用第三方插件acorn解析JS模块 let ast = acorn.parse(code) // 省略部分代码 if (this.hooks.program.call(ast, comments) === undefined) { this.detectStrictMode(ast.body) this.prewalkStatements(ast.body) this.blockPrewalkStatements(ast.body) // 这里webpack会遍历一次ast.body,其中会收集这个模块的所有依赖项,最后写入到module.dependencies中 this.walkStatements(ast.body) } }

compilation.seal()

至此从入口文件,webpack已经收集了所有的模块依赖,就等着打包输入啦~~(真累🥺)

compilation.seal最终将资源保存在compilation.assets, compilation.chunks中,所以在编写 webpack插件时候,经常会看到这两个的字段的身影。

compiler.hooks.emit.callAsync()

执行seal后,所以资源都保存到内存中了,最后调用emit钩子根据webpack.config的配置将文件输出到制定路径。

总结

写得比较模糊仓促,可能只有自己才能看得懂哈哈哈,anyway~~ 有空再回头补充下,感谢开源~~