Webpack源码分析

Webpack源码分析

在上一篇文章已经对webpack_cli启动过程进行了大致的分析,程序到最后就是执行webpack.run方法。现在对webpack的源码进行进一步的分析。

准备工作:
1、在github下载webpack的源码
2、在源码目录创建需要编译的源码和webpack.config.js配置

Webpack源码分析_第1张图片

webpack/webpack-main/examples/demo

// build.js

const webpack = require("../../lib/webpack");
const config = require("./webpack.config");
const compiler = webpack(config);

compiler.run((err, stats) => {
	if (err) {
		console.error(err);
	} else {
		console.log(stats);
	}
});


// webpack.config.js

const path = require("path");
const HtmlWebPackPlugin = require('html-webpack-plugin');
module.exports = {
	mode: "development",
	devtool: "source-map",
	context: path.resolve(__dirname, "."),
	entry: "./src/main.js",
	output: {
		filename: "bundle.js",
		path: path.resolve(__dirname, "./build")
	},
	module: {
		rules: [
			{
				test: /\.js$/,
				use: "babel-loader"
			}
		]
	},
	plugins: [
		new HtmlWebPackPlugin()
	]
};

// main.js
console.log("Hello Webpack");

3、配置node.js调试环境

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "/**"
            ],
            "program": "${workspaceFolder}/examples/demo/build.js",
        }
    ]
}

源码流程的分析

  • 构造函数的分析
const compiler = webpack(config)

1、根据是否有回调处理,然后调用create()函数

const { compiler, watch } = create();

2、在create中根据webpack.config.js配置的信息处理是数组还是单个对象配置

if (Array.isArray(options)) {
    compiler = createMultiCompiler();
} else {
    compiler = createCompiler(webpackOptions);
}

3、如果是数组就调用createMultiCompiler,在函数中遍历调用createCompiler

4、如果是单个对象就createCompiler处理

5、返回complier

下面就来详细分析createCompiler函数中做的操作?

1、通过getNormalizedWebpackOptions()处理options
2、合并默认的配置信息applyWebpackOptionsBaseDefaults()
3、创建const compiler = new Compiler(options.context, options);
Compiler构造函数中初始化参数,在this.hooks中创建很多hooks

this.hooks = Object.freeze({
		initialize: new SyncHook([]),
		...
})

4、初始化NodeEnvironmentPlugin

new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);

5、注册webpack.config.js中plugins中的插件

if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}

6、applyWebpackOptionsDefaults
7、调用environment、afterEnvironment hook

applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

8、new WebpackOptionsApply().process(options, compiler) 把webpack.config.js中的配置初始化为插件、赋值、一些hook的调用等

compiler.outputPath = options.output.path;
compiler.recordsInputPath = options.recordsInputPath || null;
compiler.recordsOutputPath = options.recordsOutputPath || null;
compiler.name = options.name;

if (options.externals) {
	const ExternalsPlugin = require("./ExternalsPlugin");
	new ExternalsPlugin(options.externalsType, options.externals).apply(
		compiler
	);
}

...省略

9、initialize hook的调用
10、返回compiler

compiler.hooks.initialize.call();
return compiler;

到这里构造函数的流程就已经完成了。

  • run函数的分析

run函数中大多数代码是hook函数的触发和回调处理。

run函数的整个流程如下:
1、this.hooks.beforeRun.callAsync
2、this.hooks.run.callAsync
3、this.compile(onCompiled)
4、onCompiled = (err, compilation) => {}
在onCompiled中代码执行顺序如下:

this.emitAssets(compilation, err => {} -> this.hooks.emit.callAsync(compilation, err => {}
-> this.hooks.done.callAsync() -> finalCallback -> this.hooks.afterDone.call(stats);

下面来分析下run函数的核心方法,即this.compile编译过程:
1、调用编译之前的hook this.hooks.beforeCompile.callAsync
2、调用编译的hook this.hooks.compile.call(params)
3、创建compilation

const compilation = this.newCompilation(params)

4、调用编译的hook

this.hooks.make.callAsync(compilation)

这个时候就会来到EntryPlugin的compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {}方法

后面分析这里做了什么操作?

说明:
如何能来到这里的呢?hooks.make是在什么时候注册的呢?

早在构造函数执行的时候new WebpackOptionsApply().process(options, compiler)中就注册了EntryOptionPlugin插件
new EntryOptionPlugin().apply(compiler);

技巧:先搜索源码中hooks.make监听的地方然后下断点
源码搜索new EntryPlugin().apply()的地方.然后下断点,然后查看函数调用栈找到EntryOptionPlugin.apply()方法

5、完成make的hook

this.hooks.finishMake.callAsync(compilation)

6、compilation.finish()
7、输出资源处理压缩优化等操作

compilation.seal()

8、调用完成之后的 hook

this.hooks.afterCompile.callAsync(compilation)

以上就是run方法整个流程的主干部分。

下面来分析编译过程的详细操作也就是EntryPlugin的compiler.hooks.make.tapAsync函数。

主要梳理方法的调用过程,不好理解的地方会做出说明。

1、compilation.addEntry()
2、 this._addEntryItem(context, entry, "dependencies", options, callback);
3、this.addModuleTree()
4、this.handleModuleCreation()
5、this.addModule()
6、this._handleModuleBuildAndDependencies()
7、this.buildModule(module, err => {}
8、this.buildQueue.add(module, callback);
9、_buildModule(module, callback) {}

补充说明:
this.buildQueue.add -> 如何调用到_buildModule()方法中的呢?

在Compilation的构造函数中有如下代码:
this.buildQueue = new AsyncQueue({
  name: "build",
  parent: this.factorizeQueue,
  processor: this._buildModule.bind(this)
});


查看AsyncQueue的源码实现:
this._processor = processor = this._buildModule;
调用add方法最终会触发this._buildModule方法。
add(item, callback) {} -> _ensureProcessing() -> _startProcessing() -> this._processor() 


10、_buildModule()
11、module.needBuild()
12、module.build()

build(options, compilation, resolver, fs, callback) {
	const AbstractMethodError = require("./AbstractMethodError");
	throw new AbstractMethodError();
}

build方法是一个抽象方法,所以调用的子类的方法,但是这个时候并不知道是哪个子类?
所以搜索build()方法全部下断点,得出这里调用的是NormalModule中的build()

12、NormalModule中的_doBuild()
13、runLoaders()
14、返回回调

以上就是整个编译过程的流程分析了。

高清无码大图下载

你可能感兴趣的:(前端,webpack,webpack源码,源码分析,流程)