Webpack之源码浅析

前言

用了很长时间webpack,lodaer、plugin也用的很多了,但始终好奇webpack的内部运行机制是怎样的,我花了些时间看了些webpack源码,在这里用文字将他们记录下来。

准备工作

创建三个文件 demo、webpack、webpack-cli 三个项目,其中

  1. demo 项目用于调用 demo/node_modules/webpack-cli 将 src/index.js 打包为 dist/main.js
  2. webpack 项目盛放的是从 github 下载的 webpack 源码,这里我用的版本是:5.30.0
  3. webpack-cli 项目盛放的是从 github 下载的 webpack-cli 源码,这里我用的版本是:4.2.0

思路

webpack-cli 默认会执行 node_modules 里的 JS 代码,我们可以篡改 node_modules 里的源码,我们也可以用 npm link 替换 node_modules 里的目录。

在 webpack 项目根目录里运行 npm link,在 webpack-cli 项目子目录里运行 npm link,在 demo 项目根目录里运行 npm link webpack webpack-cli。

 

提问

看源码的方法:

  • 声明不看
  • 构造器不看
  • if() 不看
  • if() else() 要看
  • try() catch() 要看

webpack-cli 是如何调用 wepack 的?

try {
    compiler = this.webpack(
        config.options,
        callback
            ? (error, stats) => {
                  if (error && this.isValidationError(error)) {
                      this.logger.error(error.message);
                      process.exit(2);
                  }

                  callback(error, stats);
              }
            : callback,
    );
} catch (error) {
    if (this.isValidationError(error)) {
        this.logger.error(error.message);
    } else {
        this.logger.error(error);
    }

    process.exit(2);
}

webpack-cli是通过调用this.webpack(...) => {...} 函数来创建的一个complier编译器。

  • webpack = require('webpack')
  • compiler = webpack(options, callback)
  • webpack-cli 就是这样调用 webpack 的

 

webpack 是如何分析 index.js 的?

找到webpack项目的package.json文件中的main目录:

"main": "lib/index.js",

打开lib/index.js目录,折叠所有,找到这一行代码:

const fn = lazyFunction(() => require("./webpack"));

我们知道了fn是通过"./webpack"得来的,我们看看webpack文件,找到createCompiler函数:

const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context);
	compiler.options = options;
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
	applyWebpackOptionsDefaults(options);
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();
	new WebpackOptionsApply().process(options, compiler);
	compiler.hooks.initialize.call();
	return compiler;
};

我们看到有一处申明了 new Compiler(options.context); 我们点击去看看:

都是声明,没有实质性的逻辑。那么我们跳出来继续看上段代码createCompiler函数。继续向下看,我们看到:

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

这个hooks是什么?

我们顺着代码找到了hooks的引用:

const {
	SyncHook,
	SyncBailHook,
	AsyncParallelHook,
	AsyncSeriesHook
} = require("tapable");

原来hooks调用了tapable这个库!这个库是webpack团队为了webpack专门写的一个事件/钩子库。

简单的用法实例:

// 定义一个事件/钩子
this.hooks.eventName = new SyncHook(["arg1", "arg2"]);
// 监听一个事件/钩子
this.hooks.eventName.tap('监听事由', fn)
// 触发一个事件/钩子
this.hooks.eventName.call('arg1', 'arg2')

再看webpack源码,我们就知道了,webpack就是一个事件驱动的框架,webpack负责搭架子,安排好每个流程,再由事件触发插件去做具体的事情。

那么我们就需要知道到会有哪些事件触发,而且这些事件触发的先后顺序。

webpack的流程至少有以下这几个事件钩子:

  • env
  • init
  • run 
  • beforeCompile
  • compile
  • compilation
  • make
  • finishMake
  • afterCompile
  • emit

 

读取 index.js 并分析和收集依赖是在哪个阶段?

在make阶段

搜索make.tap。我们找到了EntryPlugin,也就是入口插件的意思,应该就是我们要找的函数。

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

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

我们通过断点可以看到,entry的默认参数就是src中的index文件,并会打包成main文件,这都是webpack内置的。

 

make - finishMake 之间,做了什么?

EntryPlugin 的 addEntry 函数就是 make 阶段最重要的事情之一

我们代码跟到 factorizeQueue 就没有后面的代码了,怎么办?

factorizeModule(options, callback) {
	this.factorizeQueue.add(options, callback);
}

我们在当前文件夹搜 factorizeQueue

this.factorizeQueue = new AsyncQueue({
	name: "factorize",
	parent: this.addModuleQueue,
	processor: this._factorizeModule.bind(this)
});

 原来 factorizeQueue 是一个异步的任务队列,任务队列发现有任务就会自动执行。上面的代码会自动执行this._factorizeModule.bind(this)。我们来看一下这个会自动执行的函数。

_factorizeModule(
	{
		currentProfile,
		factory,
		dependencies,
		originModule,
		contextInfo,
		context
	},
	callback
) {
	if (currentProfile !== undefined) {...}
	factory.create(
		(err, result) => {...}
	);
}

factory.create 是什么?

 

factory.create 是什么东西?

factory字面意思是工厂,我们全局搜索一下发现factory就是NormalModuleFactory,常规模块工厂,简称nmf。

我们全局搜一下NormalModuleFactory,找到了对应文件:

NormalModuleFactory.create做了什么,也就是nmf.create做了什么?

 

nmf.create 做了什么?

找到了两句有用的代码:this.hooks.beforeResolve.call,this.hooks.factorize.call。

搜索两者对应的tap,发现factorize.tap 里面有重要代码,它触发 resolve,而resolve主要在收集loaders

它触发了 resolve,而 resolve 主要是在收集 loaders,然后它触发了 createModule事件,得到了 createdModule。

也就是说 nmf.create 得到了一个module对象,等于factory.create 得到了一个module对象。

我们回到factorizeModule,就发现,这个函数就是为了创建一个module对象,它的callback中的newModule就是新创建的module,随后调用this.addModule()函数。 

 

addModule 做了什么?

主要做的是把 module 添加到 compilation.modules 里,并且还通过检查id防止重复添加。

将module添加完成后,接着就调用了this.buildModule()

 

buildModule 做了什么?

 

buildModule调用了module.build(),来到NormalModule.js 看 build 源码,发现了 runLoaders。

然后我们来到 processResult(),发现了 _source =  ... 和 _ast = null,这明显是要把 _source变成 _ast了(这也是上面第二个问题:webpack如何分析index.js 的答案)。

然后来到 doBuild 的回调,发现了 this.parser.parse()

之前我在写简易打包器博客的时候说过,parse 就是把 code 变成 ast。那么问题来了,parser是什么,parse() 的源码在哪?

我们继续找代码就会发现 parser 来自于 acorn 库。acorn库需要编译原理知识,这里就不讨论了。

 

webpack 如何知道 index.js 依赖了哪些文件的?

目前我们知道 webpack 会对 index.js 进行 parse,得到 ast。那么接下来 webpack 应该会 traverse 这个 ast,寻找import 语句。

阅读源码后发现:

其中 blockPrewalkStatement() 对 ImportDeclaration 进行了检查。逐行检查,一旦发现 import ‘xxx’,就发触发 import 钩子,对应的监听函数会处理依赖,其中 walkStatements() 对 importExpression 进行了检查,一旦发现 import('xxx'),就会触发 importCall 钩子,对应的监听函数就会把对应的文件放到需要处理的模块中。

 

怎么把 modules 合并成一个文件的?

主要看 compilation.seal(),该函数会创建 chunks、为每个 chunk 进行 codeGeneration,然后为每个 chunk 创建 asset。

seal() 之后,emitAssets()、emitFiles() 会创建文件,最终得到 dist/main.js 和其他 chunk 文件。

 

总结

  • 使用 hooks 把 主要阶段固定下来
  • 插件自己选择阶段做事
  • 入口是由入口插件(EntryPlugin.js)搞定的 src/index.js
  • webpack 使用 Tapable 作为事件中心,将打包分为 env、compile、make、seal、emit 等几个阶段
  • 在 make 阶段借助 acorn 对源代码进行了 parse

 

webpack怎么分析依赖和打包的?

使用 Javascript Parser 对 index.js 进行 parse 得到 ast,然后遍历 ast,发现依赖声明就将其添加到 module 的 dependencies 或 blocks 中。

seal阶段,webpack 将 module 转为 chunk,可能会把多个 module 通过 codeGeneration 合并为一个 chunk。

seal之后,为每个 chunk 创建文件,并写到硬盘上。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值