Skip to content

webpack5

插件 loader ast ... webpack打包流程

webpack入口

  1. 执行npm run build --> 找到webpack/bin/webpack.js
  2. 上述文件就是判断webpack-cli是否暗转,如果安装则运行runCli方法
  3. 在runCli方法加载了webpack-cli/bin/cli.js
  4. 执行runCli,在runCli中处理命令行参数,依赖了commander,执行new WebpackCli的时候会触发action回调
  5. this.program.action(), this.program = program(commander)
  6. action回调中执行了loadCommandByName--->makeCommand-->runWebpack
  7. runWebpack的时候执行了createCompiler(),在createCompiler内部调用了webpack方法,
  8. webpack就是本地安装的webpack,接收配置文件和回调,最终生成一个compiler对象,这个compiler对象会在上述的调用过程中被返回,它就是webpack打包中的第一个核心
  9. 总结,webpack打包就是使用webpack函数接收config,然后调用run方法
js
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
    // Create a new instance of the CLI object
    const cli = new WebpackCLI();
    try {
        await cli.run(args);
    }
    catch (error) {
        cli.logger.error(error);
        process.exit(2);
    }
};
js
// 1.引入原生webpack和配置信息
const webpack = require('webpack')
const config = require('./webpack.config')
// 2.调用webpack方法,传入配置信息获取compiler实例
const compiler = webpack(config)
// 3. 调用compiler身上的run方法,让webpack工作
compiler.run((err, stats) => {
  console.log(111);
})

源码阅读总结

  1. webpack内部就是一个函数,有两个参数options和callback,一般只传一个options参数,callback传不传都会调用create方法,返回一个compiler
  2. compiler是create方法内部调用了createCompiler方法获取的
  3. 在createCompiler内部执行了new Compiler, compiler的是个单独的模块,在创建compiler之后,后续就是挂载插件,在webpack打包过程中,插件plugins是在compiler声明之后挂载
  4. 在源码中,webpack内部在挂载插件的时候其实就是将所有配置属性转为插件进行处理,例如entry: './src/index.js',内部转为了new EntryOptionPlugin.apply()

webpack函数

js
// webpack函数
// 两个参数,一个配置参数,一个回调
const webpack = (options, callback) => {
  const create = () => {
      // ......
			let compiler;
			if (Array.isArray(options)) {
				// ......
			} else {
				const webpackOptions = /** @type {WebpackOptions} */ (options);
        // 编译函数在下边代码块
				compiler = createCompiler(webpackOptions);

			}
			return { compiler, watchOptions };
  }
  // callback存在
  if (callback) {
			try {
				const { compiler } = create();
        compiler.run((err, stats) => {
          // ......
        });
				return compiler;
			} catch (err) {
        // ......
				return null;
			}
      // 不存在callback时
		} else {
			const { compiler } = create();
      // ......
			return compiler;
		}
}

createCompiler函数

js
const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
  // 下边介绍compiler类功能
	const compiler = new Compiler(
		/** @type {string} */ (options.context),
		options
	);
	// ......
	applyWebpackOptionsDefaults(options);
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();
  // 很重要在这个文件const WebpackOptionsApply = require("./WebpackOptionsApply");
  // 后续就是挂载插件,在webpack打包过程中,插件plugins是在compiler声明之后挂载,就在这个模块中
	new WebpackOptionsApply().process(options, compiler);
	compiler.hooks.initialize.call();
	return compiler;
};

new Compiler做了什么

  1. 定义了几个方法,finalCallback onCompiled run
  2. 内部最终都会执行run方法
  3. 在run方法内部执行compile方法
js
run(callback) {
		//! 最终的回调
		const finalCallback = (err, stats) => {
      // ......
		};
		// ! 完成之后的回调
		const onCompiled = (err, _compilation) => {
      // ......
		};
		// ! run方法内部调用了个run方法
		const run = () => {
			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);
            // 调用了compile方法
						this.compile(onCompiled);
					});
				});
			});
		};
		// ! 无论如何都会调用内部run方法
		if (this.idle) {
			this.cache.endIdle(err => {
				if (err) return finalCallback(err);
				this.idle = false;
				run();
			});
		} else {
			run();
		}
	}

compile做了什么

  1. 在它内部包含一个相对完整的打包流程
  2. 其中具体的打包模块操作是在make环节完成的
  3. make这里只是触发了之前订阅的一个监听时间,需要找到make这个钩子注册的位置(call -> tap)
js
	/**
	 *! compile 意思就是编译,它里边包含了webpack的打包流程
	 *! 在这之前已经做了:命令行参数的处理 optios + compiler的声明
	 *! beforeCompile compile make finishMake afterCompile
	 */
	compile(callback) {
		const params = this.newCompilationParams();
    // beforeCompile阶段
		this.hooks.beforeCompile.callAsync(params, err => {
			if (err) return callback(err);
			//! compile阶段 在beforeCompile声明了一个compilation
			this.hooks.compile.call(params);
			const compilation = this.newCompilation(params);
			const logger = compilation.getLogger("webpack.Compiler");
			logger.time("make hook");
      // make阶段
			this.hooks.make.callAsync(compilation, err => {
				logger.timeEnd("make hook");
				if (err) return callback(err);
				logger.time("finish make hook");
        // finishMake阶段
				this.hooks.finishMake.callAsync(compilation, err => {
					logger.timeEnd("finish make hook");
					if (err) return callback(err);
					process.nextTick(() => {
						logger.time("finish compilation");
						compilation.finish(err => {
							logger.timeEnd("finish compilation");
							if (err) return callback(err);
							logger.time("seal compilation");
							compilation.seal(err => {
								logger.timeEnd("seal compilation");
								if (err) return callback(err);
								logger.time("afterCompile hook");
                // afterCompile阶段
								this.hooks.afterCompile.callAsync(compilation, err => {
									logger.timeEnd("afterCompile hook");
									if (err) return callback(err);
									return callback(null, compilation);
								});
							});
						});
					});
				});
			});
		});
	}

make钩子

思考:去哪找 make 钩子的注册

注册时机

  • make 是用来实现 webpack 打包的,而打包最初需要的应该是明确入口,入口是在 options 中配置的
  • 在 compiler 对象的创建阶段就已经将所有的 options 都转为了插件,便于在整个生命周期中进行使用
  • 定位 new Compiler 再次找到 process 方法的调用,定位 entry 的处理, 这里需要注意 make 是由 compiler.run 引起的一个流程,因此配置转plugin 是在 new Compiler 时完成,换句话说 make 钩子触发之前, 相关的钩子注册工作已经完成了。所以可以正常使用

注册位置

  • process 负责将 options 插件化,其中就包含了 Entry 的处理
  • 在上述的方法中执行了 new EntryOptionPlugin().apply 操作,在apply 内部触发了 applyEntryOption 静态方法
  • 在 applyEntryOption 这个静态方法中对 entry 进行了判断和处理, 我们一般传入是非函数
  • 对 entry 进行遍历, 然后执行 new EntryPlugin(context, entry, options).apply 方法,在内部执行了 make 的 tap

make 钩子任务

追溯到 webpack 的打包流程, make 钩子被触发时就会执行这里的任务

  • 在 make 钩子注册的回调任务当中执行了 addEntry 方法,该方法是属于 compilation 的成员方法
  • 在 addEntry 当中执行了 _addEntryItem 私有方法
  • 上述的私有方法中调用了 addModuleTree 处理依赖树
  • 在 addModuleTree 方法中调用了 handleModuleCreation 方法来处理并创建 module
  • 在上述方法中调用了 factorizeModule 来获取 newModule
  • 获取 newModule 之后调用 addModule 将 newModule 添加至相应的队列中
  • 同时还调用了 buildModule 将 module 添加至相应的队列,队列的身上存在处理模块的 processor
  • 通过执行 processor 的 _buildModule 方法来完成最终的 build , 内部执行了 module.build 方法进行最终实现

所以make钩子的注册的任务核心就是完成 模块的 build 操作

build 流程

报错

  • 查看 build 具体的实现时发现定位到了 Module 当中的错误提示,这是因为 Module 是一个抽象,专门用于继承,内部并没有真正的实现 build 方法
  • 此处真正实现 build 方法的是 NormalModule 模块,它内部有一个 build

runLoaders

  • 分析 build 执行发现它的内部又执行了 doBuild 操作
  • 在 doBuild 方法内部执行了 runLoaders 方法,且这个方法是一个外部方法,第三方库导入此处不研究
  • 执行 runLoaders 的目的最终也是为了读取相应路径的文件内容,然后交由 processResult 处理
  • 执行 processResult 时,不论过程如何最终都会执行 callback , 此 callback 就是在执行 doBuild 的时候传入的
  • 在 callback 内部有一个核心的 parse 操作,该步是将 loader 读取到的内容交由 ast 语法树进行实现(acorn库)
  • 之所以需要 ast 是因为要判断当前被打包模块是否依赖了其它模块,如果依赖了则可以递归进行处理

到此模块的 build 算是结束了,余下的就是对 build 的内容进入处理

seal 流程

在 make 工作完成之后会经历 finishMake 阶段,之后调用了 seal

核心就是将 webpack 打包之后的 modules 数据保存起来

seal 流程

  • compilation 在之前已经将本次打包需要处理的模块都放到了 modules 当中
  • 遍历处理好的 modules ,然后依据配置生成不同的 chunk
  • 不同的 chunk 可能会存在不同的优化处理
  • 将优化处理好的 chunk 保存在 compilation 的 assets 属性上

seal 逻辑

  • 生成相应的 chunk 图
  • 触发各种 optimize 相关的钩子
  • 优化操作完成之后,执行 optimizeChunkModules 方法的回调
  • 在上述方法的内部执行 codeGeneration 实现最终 code 的生成工作

codeGeneration 方法

  • 内部最终调用了 _runCodeGenerationJobs ,传入 callback
  • _runCodeGenerationJobs 方法内部使用了asyncLib 库遍历jobs ,同时也传入了 callback
  • 不论上述的处理结果如何,最终都执行了调用之处传入的 callback ,而这个 callback 是我们调用 codeGeneration 方法时传入的
  • 在 codeGeneration 的 callback 当中我们会执行 createChunkAssets 方法来实现最终 chunkAssets的生成

createChunkAssets 方法

  • 定义 manifest 对象,将所有的数据都保存在这个对象上
  • 调用 manifest 对象的 render 方法,拿到最终的 source
  • 拿到 source 之后执行 emitAsset 方法将 source 写入到相应的 file

emitAsset 方法

  • 核心就是将打包之后的资源放到 assets 属性上
  • this.assets[file] = source

emitAsset 流程

  1. 上述的操作执行完成之后就开始执行 finalCallback 操作, 而这个 callback 就是调用 seal 方法时传入
  2. seal 方法的调用是在 compiler 内完成
  3. 调用 seal 方法后会执行 afterCompile 钩子, 此时会执行 callback ,这个 callback 就是 compile 调用时传入
  4. 调用 compile 时传入的就是 onCompiled
  5. 执行 onCompiled 回调的时候会执行 emitAssets 方法,将打包后资源真正的写入到磁盘操作

Released under the MIT License.