webpack5.0 的打包构建那些事

先简单了解下概念

entry:指定一个入口起点(或多个入口起点)。默认值为 ./src

webpack.config.js

ini 复制代码
module.exports = {
  entry: './path/to/my/entry/file.js'
};

多入口

css 复制代码
module.exports = {
entry: {   
  pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }
};

**output:**告诉 webpack 在哪里输出它所创建的bundles,以及如何命名这些文件,默认值为 ./dist

lua 复制代码
const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js'
  }
};

loader:

loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。

  2. use 属性,表示进行转换时,应该使用哪个 loader。

    const path = require('path');module.exports = { entry: './path/to/my/entry/file.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-first-webpack.bundle.js' }, module: { rules: [ { test: /.txt$/, use: 'raw-loader' } ] }};

配置的loader,是告诉webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在对它打包之前,先使用 raw-loader 转换一下

plugins:插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量

javascript 复制代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
   plugins: [
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'})  ]
};

模块解析(module resolution)

resolver 是一个库(library),用于帮助找到模块的绝对路径。一个模块可以作为另一个模块的依赖模块,然后被后者引用

webpack 中的解析规则

使用 enhanced-resolve,webpack 能够解析三种文件路径:

绝对路径

arduino 复制代码
import "/home/me/file";

import "C:\\Users\\me\\file";

由于我们已经取得文件的绝对路径,因此不需要进一步再做解析。

相对路径

arduino 复制代码
import "../src/file1";
import "./file2";

在这种情况下,使用 importrequire 的资源文件(resource file)所在的目录被认为是上下文目录(context directory)。在 import/require 中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。

模块路径

arduino 复制代码
import "module";
import "module/lib/file";

模块将在 resolve.modules 中指定的所有目录内搜索。 你可以替换初始模块路径,此替换路径通过使用 resolve.alias 配置选项来创建一个别名。

一旦根据上述规则解析路径后,解析器(resolver)将检查路径是否指向文件或目录。如果路径指向一个文件:

  • 如果路径具有文件扩展名,则被直接将文件打包。
  • 否则,将使用 [resolve.extensions] 选项作为文件扩展名来解析,此选项告诉解析器在解析中能够接受哪些扩展名(例如 .js, .jsx)。

如果路径指向一个文件夹,则采取以下步骤找到具有正确扩展名的正确文件:

  • 如果文件夹中包含 package.json 文件,则按照顺序查找 resolve.mainFields 配置选项中指定的字段。并且 package.json 中的第一个这样的字段确定文件路径。

  • 如果 package.json 文件不存在或者 package.json 文件中的 main 字段没有返回一个有效路径,则按照顺序查找 resolve.mainFiles 配置选项中指定的文件名,看是否能在 import/require 目录下匹配到一个存在的文件名。

  • 文件扩展名通过 resolve.extensions 选项采用类似的方法进行解析。

    const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装 const webpack = require('webpack'); // 用于访问内置插件

    module.exports = { entry: './path/to/my/entry/file.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'my-first-webpack.bundle.js' }, module: { rules: [ { test: /.txt$/, use: 'raw-loader' } ] }, plugins: [ new webpack.optimize.UglifyJsPlugin(), new HtmlWebpackPlugin({template: './src/index.html'})

    ], resolve: { // 路径别名, 懒癌福音 alias: { shared: path.resolve(__dirname, './packages/shared'), events: path.resolve(__dirname, './packages/events') }, extensions: ['.web.js', '.js', '.jsx'] //后缀名自动补全 } };

接下来通过调试源代码,深入理解

Webpack =内容转换+资源合并

  1. 初始化阶段:

*** 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数**

swift 复制代码
{  
"name": "ltq-webpack-core",
"version": "1.0.0",
"description": "",  
"main": "index.js",  
"scripts": {    
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --mode development"  
}, 
"devDependencies": {    
"acorn-walk": "^6.1.1",
 "markdown-loader": "^4.0.0",    
"tapable": "^1.1.0",    
"webpack": "^5.0.0",    
"webpack-cli": "^5.0.0"  
 }, 
 "dependencies": {
    "acorn": "^6.0.4"
  }
}

使用node 调试工具 ndb,执行

arduino 复制代码
ndb npm run dev

第一步先执行webpack 脚本,判断webpack-cli 有没有安装

如果安装了webpack-cli ,require(cli),脚本执行

执行 runCli(process.argv);

ini 复制代码
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
    const cli = new WebpackCLI();
    try {
        await cli.run(args);
    }
    catch (error) {
        cli.logger.error(error);
        process.exit(2);
    }
};

先创建WebpackCLI,调用run,在run中调用createCompiler 创建,下面是run 方法中主要代码:

typescript 复制代码
 async run(args, parseOptions) {
    // ...
       this.program.action(async (options, program) => {
            const { operands, unknown } = this.program.parseOptions(program.args);
            const defaultCommandToRun = getCommandName(buildCommandOptions.name);
            let commandToRun = operand;
            let commandOperands = operands.slice(1);
            if (isKnownCommand(commandToRun)) {
                await loadCommandByName(commandToRun, true);
            }
            await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
                from: "user",
            });
        });
    await this.program.parseAsync(args, parseOptions);
}

其中this.program.action的回调函数,存放在this._actionHandler,后面会调用

kotlin 复制代码
 action(fn) {
    const listener = (args) => {
      const actionArgs = args.slice(0, expectedArgsCount);
      if (this._storeOptionsAsProperties) {
        actionArgs[expectedArgsCount] = this; // backwards compatible "options"
      } else {
        actionArgs[expectedArgsCount] = this.opts();
      }
      actionArgs.push(this);

      return fn.apply(this, actionArgs);
    };
    this._actionHandler = listener;
    return this;
  }

重点看:await this.program.parseAsync(args, parseOptions);

// 准备用户参数,解析执行命令

kotlin 复制代码
 // command.js
 async parseAsync(argv, parseOptions) {
    const userArgs = this._prepareUserArgs(argv, parseOptions);
    await this._parseCommand([], userArgs);

    return this;
  }

_parseCommand(operands, unknown) {
      actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs));
      if (this.parent) {
        actionResult = this._chainOrCall(actionResult, () => {
          this.parent.emit(commandEvent, operands, unknown); // legacy
        });
      }
      actionResult = this._chainOrCallHooks(actionResult, 'postAction');
      return actionResult;
    }

actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs));

该方法就是调用上面存储的this._actionHandler回调,在回调中 await loadCommandByName(commandToRun, true);

dart 复制代码
const loadCommandByName = async (commandName, allowToInstall = false) => {
            const isBuildCommandUsed = isCommand(commandName, buildCommandOptions);
            const isWatchCommandUsed = isCommand(commandName, watchCommandOptions);
            if (isBuildCommandUsed || isWatchCommandUsed) {
                await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {
                    this.webpack = await this.loadWebpack();
                    return this.getBuiltInOptions();
                }, async (entries, options) => {
                    if (entries.length > 0) {
                        options.entry = [...entries, ...(options.entry || [])];
                    }
                    await this.runWebpack(options, isWatchCommandUsed);
                });
            }
         //...
        };

在loadCommandByName 方法中调用 makeCommand方法,该方法先loadWebpack加载webpack

scss 复制代码
async makeCommand(commandOptions, options, action) {
        if (options) {
            options = await options();
            for (const option of options) {
                this.makeOption(command, option);
            }
        }
        command.action(action);
        return command;
    }

options 为函数

kotlin 复制代码
async () => {
             this.webpack = await this.loadWebpack();
             return this.getBuiltInOptions();
           }

command.action 方法执行后,将该方法存储在this._actionHandler,等待后面执行:

ini 复制代码
async (entries, options) => {
                    if (entries.length > 0) {
                        options.entry = [...entries, ...(options.entry || [])];
                    }
                    await this.runWebpack(options, isWatchCommandUsed);
                });

执行完成后,回到主函数,接着执行

csharp 复制代码
await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {
                from: "user",
            });

该方法会调用runWebpack方法,

javascript 复制代码
async runWebpack(options, isWatchCommand) {
        const callback = (error, stats) => {
           // ...
        };
        compiler = await this.createCompiler(options, callback); //创建compiler
        if (!compiler) {
            return;
        }
        const isWatch = (compiler) => Boolean(this.isMultipleCompiler(compiler)
            ? compiler.compilers.some((compiler) => compiler.options.watch)
            : compiler.options.watch);
        if (isWatch(compiler) && this.needWatchStdin(compiler)) {
            process.stdin.on("end", () => {
                process.exit(0);
            });
            process.stdin.resume();
        }
    }

在 compiler = await this.createCompiler(options, callback);

创建compiler,加载webpack.config.js 打包配置项,构建配置项

javascript 复制代码
//webpack-cli.js
async createCompiler(options, callback) {
        let config = await this.loadConfig(options);
        config = await this.buildConfig(config, options);
        let compiler;
        compiler = this.webpack(config.options, callback);
        return compiler;
    }
  1. 创建编译器对象 :用上一步得到的参数创建 Compiler 对象
ini 复制代码
this.loadConfig(options);加载项目定义的webpack.config.js 配置文件

const webpack = ((
	options,
	callback
) => {
	const create = () => {
		let compiler;
		let watch = false;
		let watchOptions;
		if (Array.isArray(options)) {
			compiler = createMultiCompiler(options);
			watch = options.some(options => options.watch);
			watchOptions = options.map(options => options.watchOptions || {});
		} else {
			compiler = createCompiler(options);
			watch = options.watch;
			watchOptions = options.watchOptions || {};
		}
		return { compiler, watch, watchOptions };
	};
	const { compiler, watch, watchOptions } = create();
	
	compiler.run((err, stats) => {
		compiler.close(err2 => {
			callback(err || err2, stats);
			});
		});
	}
        return compiler;
});



class Compiler {
	/**
	 * @param {string} context the compilation path
	 */
	constructor(context) {
		this.hooks = Object.freeze({
			/** @type {SyncHook<[]>} */
			initialize: new SyncHook([]),

			/** @type {SyncBailHook<[Compilation], boolean>} */
			shouldEmit: new SyncBailHook(["compilation"]),
			/** @type {AsyncSeriesHook<[Stats]>} */
			done: new AsyncSeriesHook(["stats"]),
			run: new AsyncSeriesHook(["compiler"]),
			/** @type {AsyncSeriesHook<[Compilation]>} */
			emit: new AsyncSeriesHook(["compilation"]),
			...
			/** @type {SyncHook<[CompilationParams]>} */
			compile: new SyncHook(["params"]),
	

			/** @type {SyncBailHook<[string, string, any[]], true>} */
			infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
			/** @type {SyncHook<[]>} */
			environment: new SyncHook([]),
			/** @type {SyncHook<[]>} */
			afterEnvironment: new SyncHook([]),
			/** @type {SyncHook<[Compiler]>} */
			afterPlugins: new SyncHook(["compiler"]),
			/** @type {SyncHook<[Compiler]>} */
			afterResolvers: new SyncHook(["compiler"]),
			/** @type {SyncBailHook<[string, Entry], boolean>} */
			entryOption: new SyncBailHook(["context", "entry"])
		});

		/** @type {string=} */
		this.name = undefined;
		/** @type {Compilation=} */
		this.parentCompilation = undefined;
		/** @type {Compiler} */
		this.root = this;
                ...
	}
}



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;
};
  1. process.args + webpack.config.js 合并成用户配置

  2. 调用 validateSchema 校验配置

  3. 调用 getNormalizedWebpackOptions + applyWebpackOptionsBaseDefaults 合并出最终配置

  4. 创建 compiler 对象

  5. 遍历用户定义的 plugins 集合,执行插件的 apply 方法

  6. 调用 new WebpackOptionsApply().process 方法,加载各种内置插件

详细看下new WebpackOptionsApply().process(options, compiler)添加内置plugins

主要逻辑集中在 WebpackOptionsApply 类,webpack 内置了数百个插件,这些插件并不需要我们手动配置,WebpackOptionsApply 会在初始化阶段根据配置内容动态注入对应的插件,包括:

  • 注入 EntryOptionPlugin 插件,处理 entry 配置

  • 根据 devtool 值判断后续用那个插件处理 sourcemap,可选值:EvalSourceMapDevToolPluginSourceMapDevToolPluginEvalDevToolModulePlugin

  • 注入 RuntimePlugin ,用于根据代码内容动态注入 webpack 运行时

执行createCompiler先创建Compiler ,创建compiler的hooks,初始化plugins ,完成创建compiler后调用,执行完成后接下来调用

ini 复制代码
compiler.run((err, stats) => {
	compiler.close(err2 => {
	   callback(err || err2, stats);
	});
});

调用钩子函数

kotlin 复制代码
this.hooks.beforeRun.callAsync(this, err => {
	this.hooks.run.callAsync(this, err => {
		this.readRecords(err => {
                    this.compile(onCompiled);
		});
	});
});

调用run 钩子函数,在run中执行compile方法

ini 复制代码
compile(callback) {
		const params = this.newCompilationParams();
		this.hooks.beforeCompile.callAsync(params, err => {
			if (err) return callback(err);

			this.hooks.compile.call(params);

			const compilation = this.newCompilation(params); //创建compilation

			const logger = compilation.getLogger("webpack.Compiler");

			logger.time("make hook");
			this.hooks.make.callAsync(compilation, err => {
				logger.timeEnd("make hook");
				if (err) return callback(err);

				logger.time("finish make hook");
				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");
								this.hooks.afterCompile.callAsync(compilation, err => {
									logger.timeEnd("afterCompile hook");
									if (err) return callback(err);

									return callback(null, compilation);
								});
							});
						});
					});
				});
			});
		});
	}

在this.hooks.beforeCompile中创建compilation对象,Compilation:单次编辑过程的管理器,比如 watch = true 时,运行过程中只有一个 compiler 但每次文件变更触发重新编译时,都会创建一个新的 compilation 对象

在 webpack 构建过程(make 阶段)中逐步收集模块间的依赖关系,到 seal 阶段的 chunk 生成

从网上找了张图,可以看到

webpack 的主体框架:

构建阶段

进入make 阶段,可以看到make 阶段和seal阶段,make阶段通过hooks发布事件完成

make阶段第一个执行的plugin是EntryPlugin

  • Entry:编译入口,webpack 编译的起点

构建阶段从 entry 开始递归解析资源与资源的依赖,在 compilation 对象内逐步构建出 module 集合以及 module 之间的依赖关系,核心流程:

进入handleModuleCreation

ini 复制代码
// Compilation 文件
addModuleChain(context, dependency, callback) {

		this.handleModuleCreation(
			{
				factory: moduleFactory,
				dependencies: [dependency],
				originModule: null,
				context
			},
			err => {
				if (err && this.bail) {
					callback(err);
					this.buildQueue.stop();
					this.rebuildQueue.stop();
					this.processDependenciesQueue.stop();
					this.factorizeQueue.stop();
				} else {
					callback();
				}
			}
		);
	}

handleModuleCreation(
		{ factory, dependencies, originModule, context, recursive = true },
		callback
	) {
		const moduleGraph = this.moduleGraph;

		this.factorizeModule(
			{ currentProfile, factory, dependencies, originModule, context },
			(err, newModule) => {

				this.addModule(newModule, (err, module) => {
					if (err) {
						if (!err.module) {
							err.module = module;
						}
						this.errors.push(err);

						return callback(err);
					}

					for (let i = 0; i < dependencies.length; i++) {
						const dependency = dependencies[i];
						moduleGraph.setResolvedModule(originModule, dependency, module);
					}

		
					if (module !== newModule) {
						if (currentProfile !== undefined) {
							const otherProfile = moduleGraph.getProfile(module);
							if (otherProfile !== undefined) {
								currentProfile.mergeInto(otherProfile);
							} else {
								moduleGraph.setProfile(module, currentProfile);
							}
						}
					}


					this.buildModule(module, err => {
						this.processModuleDependencies(module, err => {
							if (err) {
								return callback(err);
							}
							callback(null, module);
						});
					});
				});
			}
		);
	}

this.buildModule构建模块,执行NormalModule 的build函数

调用 loader-runner 仓库的 runLoaders 转译 module 内容,通常是从各类资源类型转译为 JavaScript 文本

接着调用JavascriptParser模块中的parse方法解析模块,根据解析的ast语法树分析依赖

遍历 AST,触发各种钩子

  1. HarmonyExportDependencyParserPlugin 插件监听 exportImportSpecifier 钩子,解读 JS 文本对应的资源依赖

  2. 调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中

执行this.blockPreWalkStatements 中处理依赖

调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中

执行完成解析后,回到handleParseResult(result);

在handleParseResult 调用handleBuildDone,完成build函数的回调callback;

回到buildModule的回调

processModuleDependencies 处理模块的依赖,触发handleModuleCreation整个循环就跑起来了

总结:

  1. 调用 handleModuleCreate ,根据文件类型构建 module 子类

  2. 调用 loader-runner 仓库的 runLoaders 转译 module 内容,通常是从各类资源类型转译为 JavaScript 文本

  3. 调用 acorn 将 JS 文本解析为AST

  4. 遍历 AST,触发各种钩子

  5. HarmonyExportDependencyParserPlugin 插件监听 exportImportSpecifier 钩子,解读 JS 文本对应的资源依赖

  6. 调用 module 对象的 addDependency 将依赖对象加入到 module 依赖列表中

  7. AST 遍历完毕后,调用 module.handleParseResult 处理模块依赖

  8. 对于 module 新增的依赖,调用 handleModuleCreate ,控制流回到第一步

  9. 所有依赖都解析完毕后,构建阶段结束

生成阶段

构建阶段围绕 module 展开,生成阶段则围绕 chunks

make 完成后进入seal阶段

seal 方法比较复杂,大体流程如下:

  1. 构建本次编译的 ChunkGraph 对象;

  2. 遍历 compilation.modules 集合,将 moduleentry/动态引入 的规则分配给不同的 Chunk 对象;

  3. compilation.modules 集合遍历完毕后,得到完整的 chunks 集合对象

  4. createXxxAssets 遍历 module/chunk ,调用 compilation.emitAssets 方法将 assets 信息记录到 compilation.assets 对象中

  5. 触发 seal 回调,控制流回到 compiler 对象

写入文件系统

seal 结束之后,执行回调函数,回到onCompiled方法

紧接着调用 compiler.emitAssets 函数,函数内部调用 compiler.outputFileSystem.writeFile 方法将 assets 集合写入文件系统

最后总结:

webpack核心完成了 内容转换 + 资源合并 两种功能,实现上包含三个阶段:

  1. 初始化阶段:

  2. 初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数

  3. 创建编译器对象 :用上一步得到的参数创建 Compiler 对象

  4. 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等

  5. 开始编译 :执行 compiler 对象的 run 方法

  6. 确定入口 :根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象

  7. 构建阶段:

  8. 编译模块(make) :根据 entry 对应的 dependence 创建 module 对象,调用 loader 将模块转译为标准 JS 内容,调用 JS 解释器将内容转换为 AST 对象,从中找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理

  9. 完成模块编译 :上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的 依赖关系图

  10. 生成阶段:

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

  12. 写入文件系统(emitAssets):在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

参考文档

相关推荐
醉方休19 小时前
webpack-dev-server使用
webpack
陈陈CHENCHEN19 小时前
使用 Webpack 快速创建 React 项目 - SuperMap iClient JavaScript / Leaflet
react.js·webpack
小白640219 小时前
前端梳理体系从常问问题去完善-工程篇(webpack,vite)
前端·webpack·node.js
guslegend19 小时前
Webpack5 第一节
webpack
光影少年10 天前
webpack打包优化
webpack·掘金·金石计划·前端工程化
百思可瑞教育12 天前
Vue.config.js中的Webpack配置、优化及多页面应用开发
前端·javascript·vue.js·webpack·uni-app·北京百思教育
歪歪10012 天前
webpack 配置文件中 mode 有哪些模式?
开发语言·前端·javascript·webpack·前端框架·node.js
歪歪10012 天前
如何配置Webpack以实现按需加载模块?
开发语言·前端·webpack·node.js
谢尔登12 天前
【Webpack】模块联邦
前端·webpack·node.js