一文掌握Webpack编译流程

本文深入探讨Webpack的事件流机制、编译流程和源码分析,详细解析从初始化到输出文件的每个阶段,包括Tapable、Compiler、Compilation、Module等关键概念,带你一步步理解Webpack如何构建和打包项目。
摘要由CSDN通过智能技术生成

本文概要

  • Webpack事件流机制

  • Webpack流程概览

  • Webpack流程图示

  • Webpack流程详解

  • Webpack执行流程源码分析

Webpack事件流机制

Webpack是基于事件流的插件集合,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,Tapable是一个类似Node.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统。Webpack中最核心的负责编译的Compiler和负责创建的捆绑包的Compilation都是Tapable实例。
Tapable库暴露很多Hook类,为插件提供挂载的钩子。

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

Tapable提供了同步和异步绑定钩子的方法,并且他们都有绑定事件以及执行事件对应的方法。

Async*
绑定: tapAsync/tapPromise/tap   执行: callAsync/promise
Sync*
绑定: tap                       执行: call

Tapable使用实际例子:

const { SyncHook } = require("tapable");
let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。

// 订阅
queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的
    console.log(name, name2, 1);
    return '1'
});
queue.tap('2', function (name) {
    console.log(name, 2);
});
queue.tap('3', function (name) {
    console.log(name, 3);
});

// 发布
queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数

// 执行结果:
/*
webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
webpack 2
webpack 3
*/

Webpack中Tapable的应用

if (Array.isArray(options)) {
		compiler = new MultiCompiler(
			Array.from(options).map(options => webpack(options))
		);
	} else if (typeof options === "object") {
		// 1 做初始的操作
		options = new WebpackOptionsDefaulter().process(options);

		compiler = new Compiler(options.context);
		compiler.options = options;
		// 2  必须插件有apply接受compiler 参数
		new NodeEnvironmentPlugin({
			infrastructureLogging: options.infrastructureLogging
		}).apply(compiler);
		// 插件接收compiler对象上的hooks,议案事件触发,插件也会执行操作
		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);
	}

Webpack流程概览

Webpack首先会把配置参数和命令行的参数及默认参数合并,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compiler的run来真正启动webpack编译构建过程,webpack的构建流程包括compile、make、build、seal、emit阶段,执行完这些阶段就完成了构建过程。

  • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

  • 开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory,不然后面碰到了就不知道如何处理了。

  • 编译模块: 进入 make 阶段,会从 entry 开始进行两步操作:第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。

  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

Webpack打包流程图示

Webpack流程详解

分为三个阶段: 初始化阶段,编译阶段,输出文件(chunk)。

初始化阶段:
  • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。

  • 初始化默认参数配置: new WebpackOptionsDefaulter().process(options)

  • 实例化Compiler对象:用上一步得到的参数初始化Compiler实例,Compiler负责文件监听和启动编译。Compiler实例中包含了完整的Webpack配置,全局只有一个Compiler实例。

  • 加载插件: 依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时给插件传入compiler实例的引用,以方便插件通过compiler调用Webpack提供的API。

  • 处理入口: 读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备。

new EntryOptionPlugin().apply(compiler)  new SingleEntryPlugin(context, item, name)  compiler.hooks.make.tapAsync
编译阶段
  • run阶段:启动一次新的编译。this.hooks.run.callAsync。

  • compile: 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。

  • compilation: 当Webpack以开发模式运行时,每当检测到文件变化,一次新的Compilation将被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation对象也提供了很多事件回调供插件做扩展。

  • make:一个新的 Compilation 创建完毕主开始编译  完毕主开始编译this.hooks.make.callAsync。

  • addEntry: 即将从 Entry 开始读取文件。

  • _addModuleChain: 根据依赖查找对应的工厂函数,并调用工厂函数的create来生成一个空的MultModule对象,并且把MultModule对象存入compilation的modules中后执行MultModule.build。

  • buildModules: 使用对应的Loader去转换一个模块。开始编译模块,this.buildModule(module)  buildModule(module, optional, origin,dependencies, thisCallback)。

  • build: 开始真正编译模块。

  • doBuild: 开始真正编译入口模块。

  • normal-module-loader: 在用Loader对一个模块转换完后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便Webpack后面对代码的分析。

  • program: 从配置的入口模块开始,分析其AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。

输出阶段
  • seal: 封装 compilation.seal seal(callback)。

  • addChunk: 生成资源 addChunk(name)。

  • createChunkAssets: 创建资源 this.createChunkAssets()。

  • getRenderManifest: 获得要渲染的描述文件 getRenderManifest(options)。

  • render: 渲染源码 source = fileManifest.render()。

  • afterCompile: 编译结束   this.hooks.afterCompile。

  • shouldEmit: 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。this.hooks.shouldEmit。

  • emit: 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。

this.emitAssets(compilation)  this.hooks.emit.callAsync const emitFiles = err this.outputFileSystem.writeFile
  • done: 全部完成     this.hooks.done.callAsync。

Webpack执行流程源码分析

注: 因webpack源码量较大,笔者在此依据上文执行流程关键节点进行源码分析,建议读者依据webpack源码对照本文进行阅读。

Step1: 初始化入口分析 entery-option

webpack内部会有一个默认的配置,在 webpack.js 入口处理函数中,初始化了所有的默认配置, 在WebpackOptionsDefaulter()中,配置了很多关于 resolve 和 resolveLoader 的配置。主要有三种 对模块的默认配置,输出output,解析optimization,加载resolve模块以及resolveLoader。我们首先看一下webpack.js 入口:

// ~/webpack/lib/webpack.js
       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.options = options;
		// 可以看出插件必须有apply以及接受compiler 参数
		new NodeEnvironmentPlugin({
			infrastructureLogging: options.infrastructureLogging
		}).apply(compiler);
		// 插件接收compiler对象上的hooks,议案事件触发,插件也会执行操作
		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);
	}

process方法将我们写的 webpack 的配置 和默认的配置合并。并且 process 过程里会注入关于 normal/context/loader 的默认配置的获取函数。

// ~/webpack/lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

我们接下来看下EntryOptionPlugin的实现:

// ~/webpack/lib/EntryOptionPlugin.js
const SingleEntryPlugin = require("./SingleEntryPlugin");
const MultiEntryPlugin = require("./MultiEntryPlugin");
const DynamicEntryPlugin = require("./DynamicEntryPlugin");

const itemToPlugin = (context, item, name) => {
	if (Array.isArray(item)) {
		return new MultiEntryPlugin(context, item, name);
	}
	return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugi
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值