关于webpack(v5.74.0)的dllPlugin插件的原理

众所周知webpack的dll的作用是提前编译好模块,并生成一个json文件。然后用的时候就从该json文件中查找,如果有就从打包好的模块直接引入。下面我们看看具体实现。

构建

// webpack.dll.js

const DllPlugin = require('./plugins/testDllPlguin')
const path = require('path')
module.exports = {
  mode: 'development',
  entry: ['lodash', 'jquery'],
  output: {
    filename: '[name].dll.js',
    path: path.join(__dirname,'dll'),
    library: '_dll_[name]',
  },
  plugins: [
    new DllPlugin({
      name: '_dll_[name]',
      path: path.join(__dirname, 'dll/mainfest.json'),
    }),
  ],
}

将lodash和jquery打包的逻辑发生在DllPlugin插件中:

compiler.hooks.entryOption.tap("DllPlugin", (context, entry) => {
	if (typeof entry !== "function") {
		for (const name of Object.keys(entry)) {
			const options = {
				name,
				filename: entry.filename
			};
			new DllEntryPlugin(context, entry[name].import, options).apply(
				compiler
			);
		}
	} else {
		throw new Error(
			"DllPlugin doesn't support dynamic entry (function) yet"
		);
	}
	return true;
});

首先遍历entry调用DllEntryPlugin插件:

/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const DllModuleFactory = require("webpack/lib/DllModuleFactory");
const DllEntryDependency = require("webpack/lib/dependencies/DllEntryDependency");
const EntryDependency = require("webpack/lib/dependencies/EntryDependency");

class DllEntryPlugin {
	constructor(context, entries, options) {
		this.context = context;
		this.entries = entries;
		this.options = options;
	}

	apply(compiler) {
		// compiltion创建之后执行去拿到compiltion,设置当前依赖的模块创建工厂
		compiler.hooks.compilation.tap(
			"DllEntryPlugin",
			(compilation, { normalModuleFactory }) => {
				const dllModuleFactory = new DllModuleFactory();
        		// 主模块的内容
				compilation.dependencyFactories.set(
					DllEntryDependency,
					dllModuleFactory
				);
       			 // 依赖的内容是通过普通模块生成
				compilation.dependencyFactories.set(
         			 // ntryDependency创建依赖,jquery变依赖对象,方便后面筛选
					EntryDependency,
         			 // 根据依赖对象生成模块内容
					normalModuleFactory
				);
			}
		);
   		 // compiltion结束前执行
		compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
			compilation.addEntry(
				this.context,
        		// module.exports = __webpack_require__的依赖
				new DllEntryDependency(
					this.entries.map((e, idx) => {
            			// 添加依赖,jquery,lodash
						const dep = new EntryDependency(e);
						dep.loc = {
							name: this.options.name,
							index: idx
						};
						return dep;
					}),
					this.options.name
				),
				this.options,
				callback
			);
		});
	}
}

module.exports = DllEntryPlugin;

  1. 给主模块设置DllEntryDependency依赖和创建依赖模块的类dllModuleFactory:
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const DllModule = require("./DllModule");
const ModuleFactory = require("./ModuleFactory");

/** @typedef {import("./ModuleFactory").ModuleFactoryCreateData} ModuleFactoryCreateData */
/** @typedef {import("./ModuleFactory").ModuleFactoryResult} ModuleFactoryResult */
/** @typedef {import("./dependencies/DllEntryDependency")} DllEntryDependency */

class DllModuleFactory extends ModuleFactory {
	constructor() {
		super();
		this.hooks = Object.freeze({});
	}
	/**
	 * @param {ModuleFactoryCreateData} data data object
	 * @param {function(Error=, ModuleFactoryResult=): void} callback callback
	 * @returns {void}
	 */
	create(data, callback) {
		const dependency = /** @type {DllEntryDependency} */ (data.dependencies[0]);
		callback(null, {
			module: new DllModule(
				data.context,
				dependency.dependencies,
				dependency.name
			)
		});
	}
}

module.exports = DllModuleFactory;

主模块内容的生成是有DllModule负责的:

...
codeGeneration(context) {
	const sources = new Map();
	sources.set(
		"javascript",
		new RawSource("module.exports = __webpack_require__;")
	);
	return {
		sources,
		runtimeRequirements: RUNTIME_REQUIREMENTS
	};
}
	...

可以看到主模块内容是module.exports = __webpack_require__;。确定了主模块内容接下来就是确定该模块的依赖,我们往回看DllEntryPlugin:

compilation.dependencyFactories.set(
    // ntryDependency创建依赖,jquery变依赖对象,方便后面筛选
	EntryDependency,
    // 根据依赖对象生成模块内容
	normalModuleFactory
);

先确定依赖的模块创建函数normalModuleFactory,然后添加入口依赖DllEntryDependency:

compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
	compilation.addEntry(
		this.context,
	    // module.exports = __webpack_require__的依赖
		new DllEntryDependency(
			this.entries.map((e, idx) => {
	         	// 添加依赖,jquery,lodash
				const dep = new EntryDependency(e);
				dep.loc = {
					name: this.options.name,
					index: idx
				};
				return dep;
			}),
			this.options.name
		),
		this.options,
		callback
	);
});

DllEntryDependency是我们的主模块,遍历入口传入的lodash和jquery去添加依赖模块。此时打包我们得到三个模块:
在这里插入图片描述
在这里插入图片描述
第二个主要内容是new LibManifestPlugin(this.options).apply(compiler)生成manifest.json文件:

/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const asyncLib = require("neo-async");
const EntryDependency = require("./dependencies/EntryDependency");
const { someInIterable } = require("./util/IterableHelpers");
const { compareModulesById } = require("./util/comparators");
const { dirname, mkdirp } = require("./util/fs");

/** @typedef {import("./Compiler")} Compiler */

/**
 * @typedef {Object} ManifestModuleData
 * @property {string | number} id
 * @property {Object} buildMeta
 * @property {boolean | string[]} exports
 */

class LibManifestPlugin {
	constructor(options) {
		this.options = options;
	}

	/**
	 * Apply the plugin
	 * @param {Compiler} compiler the compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		// 输出资源前
		compiler.hooks.emit.tapAsync(
			"LibManifestPlugin",
			(compilation, callback) => {
				const moduleGraph = compilation.moduleGraph;
				asyncLib.forEach(
					Array.from(compilation.chunks),
					(chunk, callback) => {
						if (!chunk.canBeInitial()) {
							callback();
							return;
						}
						const chunkGraph = compilation.chunkGraph;
						const targetPath = compilation.getPath(this.options.path, {
							chunk
						});
						const name =
							this.options.name &&
							compilation.getPath(this.options.name, {
								chunk
							});
						const content = Object.create(null);
						for (const module of chunkGraph.getOrderedChunkModulesIterable(
							chunk,
							compareModulesById(chunkGraph)
						)) {
							if (
								this.options.entryOnly &&
								!someInIterable(
									moduleGraph.getIncomingConnections(module),
									c => c.dependency instanceof EntryDependency
								)
							) {
								continue;
							}
							const ident = module.libIdent({
								context: this.options.context || compiler.options.context,
								associatedObjectForCache: compiler.root
							});
							if (ident) {
								const exportsInfo = moduleGraph.getExportsInfo(module);
								const providedExports = exportsInfo.getProvidedExports();
								/** @type {ManifestModuleData} */
								const data = {
									id: chunkGraph.getModuleId(module),
									buildMeta: module.buildMeta,
									exports: Array.isArray(providedExports)
										? providedExports
										: undefined
								};
								content[ident] = data;
							}
						}
						const manifest = {
							name,
							type: this.options.type,
							content
						};
						// Apply formatting to content if format flag is true;
						const manifestContent = this.options.format
							? JSON.stringify(manifest, null, 2)
							: JSON.stringify(manifest);
						const buffer = Buffer.from(manifestContent, "utf8");
						mkdirp(
							compiler.intermediateFileSystem,
							dirname(compiler.intermediateFileSystem, targetPath),
							err => {
								if (err) return callback(err);
								compiler.intermediateFileSystem.writeFile(
									targetPath,
									buffer,
									callback
								);
							}
						);
					},
					callback
				);
			}
		);
	}
}
module.exports = LibManifestPlugin;

该段的主要逻辑是拿到当前编译的所有模块并判断是不是EntryDependency依赖创建的,我们知道lodash和jquery的依赖是EntryDependency对应的normalModuleFactory生成的。所以会过滤出lodash和jquery并组成content,然后通过mkdirp生成mainfest.json文件。

使用dll

此时已生成这两个文件:在这里插入图片描述
接下来我们要配置使用该文件:


...
  plugins: [
    ...
    new DllReferencePlugin({
      context: __dirname,
      manifest: require('./dll/mainfest.json'),
    })
  ]
};

我们先来看看DllReferencePlugin:

...

class DllReferencePlugin {
	/**
	 * @param {DllReferencePluginOptions} options options object
	 */
	constructor(options) {
		validate(options);
		this.options = options;
		/** @type {WeakMap<Object, {path: string, data: DllReferencePluginOptionsManifest?, error: Error?}>} */
		this._compilationData = new WeakMap();
	}

	apply(compiler) {
		compiler.hooks.compilation.tap(
			"DllReferencePlugin",
			(compilation, { normalModuleFactory }) => {
				// 将dll-reference _dll_main这个依赖设置为normalModuleFactory,然后监听normalModuleFactory的hooks
				// 当解析模块的时候就可以返回我们设置的模块内容
				compilation.dependencyFactories.set(
					// 代理资源的依赖
					DelegatedSourceDependency,
					normalModuleFactory
				);
			}
		);

		compiler.hooks.compile.tap("DllReferencePlugin", params => {
			let name = this.options.name;
			let sourceType = this.options.sourceType;
			let content =
				"content" in this.options ? this.options.content : undefined;
			if ("manifest" in this.options) {
				let manifestParameter = this.options.manifest;
				let manifest;
				if (typeof manifestParameter === "string") {
					const data = this._compilationData.get(params);
					// If there was an error parsing the manifest
					// file, exit now because the error will be added
					// as a compilation error in the "compilation" hook.
					if (data.error) {
						return;
					}
					manifest = data.data;
				} else {
					manifest = manifestParameter;
				}
				if (manifest) {
					if (!name) name = manifest.name;
					if (!sourceType) sourceType = manifest.type;
					if (!content) content = manifest.content;
				}
			}
			/** @type {Externals} */
			const externals = {};
			const source = "dll-reference " + name;
			externals[source] = name;
			const normalModuleFactory = params.normalModuleFactory;
			// 拦截依赖提供dll-reference _dll_main模块
			// 入口:dll-reference _dll_main 内容:module.exports = _dll_main;var表示是一个变量
			new ExternalModuleFactoryPlugin(sourceType || "var", externals).apply(
				normalModuleFactory
			);
			// 从mainfest.json文件中返回新模块
			new DelegatedModuleFactoryPlugin({
				source: source,
				type: this.options.type,
				scope: this.options.scope,
				context: this.options.context || compiler.options.context,
				content,
				extensions: this.options.extensions,
				associatedObjectForCache: compiler.root
			}).apply(normalModuleFactory);
		});
...
		// );
	}
}
...

首先我们设置了一个依赖DelegatedSourceDependency,然后调用了ExternalModuleFactoryPlugin插件:

...
callback(
	null,
	new ExternalModule(
		externalConfig,// _dll_main
		type || globalType, // var
		dependency.request // dll-reference _dll_main
	)
);
...

该插件主要是监听了normalModuleFactory.hooks.factorize方法在解析依赖前判断是不是externals对象里面的模块。如果是则会调用ExternalModule方法返回模块内容。直接看该模块的codeGeneration函数:
在这里插入图片描述
此时也只是完成了查找依赖的过程,还会通过DelegatedModuleFactoryPlugin从mainfest.json文件中查找对应模块,如果有就会从dll-reference _dll_main中导入:

// 创建 NormalModule 实例后调用
normalModuleFactory.hooks.module.tap(
	"DelegatedModuleFactoryPlugin",
	module => {
		const request = module.libIdent(this.options);
		if (request) {// './node_modules/lodash/lodash.js'是否在mainfest.json文件中
			if (request in this.options.content) {
				const resolved = this.options.content[request];
				// 返回新模块
				return new DelegatedModule(
					this.options.source,// 'dll-reference _dll_main'
					resolved,// './node_modules/lodash/lodash.js'
					this.options.type,
					request,// './node_modules/lodash/lodash.js'
					module // './node_modules/lodash/lodash.js'模块
				);
			}
		}
		return module;
	}
);

在DelegatedModule模块的build中我们会添加一个依赖:

/** 每个模块生成的时候添加依赖dll-reference _dll_main
 * @param {WebpackOptions} options webpack options
 * @param {Compilation} compilation the compilation
 * @param {ResolverWithOptions} resolver the resolver
 * @param {InputFileSystem} fs the file system
 * @param {function(WebpackError=): void} callback callback function
 * @returns {void}
 */
build(options, compilation, resolver, fs, callback) {
	this.buildMeta = { ...this.delegateData.buildMeta };
	this.buildInfo = {};
	this.dependencies.length = 0;
	this.delegatedSourceDependency = new DelegatedSourceDependency(
		this.sourceRequest
	);
	// 给当前模块(lodash jquery wsy)添加依赖dll-reference _dll_main,当解析的时候就会根据该内容查找到依赖,再根据依赖生成模块内容
	this.addDependency(this.delegatedSourceDependency);
	this.addDependency(
		new StaticExportsDependency(this.delegateData.exports || true, false)
	);
	callback();
}

/**
 * @param {CodeGenerationContext} context context for code generation
 * @returns {CodeGenerationResult} result
 */ // 
codeGeneration({ runtimeTemplate, moduleGraph, chunkGraph }) {
	// 当前模块依赖于dll-reference _dll_main
	const dep = /** @type {DelegatedSourceDependency} */ (this.dependencies[0]);
	const sourceModule = moduleGraph.getModule(dep);// 当前依赖生成的模块
	let str;

	if (!sourceModule) {
		str = runtimeTemplate.throwMissingModuleErrorBlock({
			request: this.sourceRequest
		});
	} else {
		str = `module.exports = (${runtimeTemplate.moduleExports({
			module: sourceModule,// dll-reference _dll_main模块
			chunkGraph,
			request: dep.request, // dll-reference _dll_main
			runtimeRequirements: new Set()
		})})`;

		switch (this.delegationType) {
			case "require":
				str += `(${JSON.stringify(this.request)})`;
				break;
			case "object":
				str += `[${JSON.stringify(this.request)}]`;
				break;
		}

		str += ";";
	}

	const sources = new Map();
	if (this.useSourceMap || this.useSimpleSourceMap) {
		sources.set("javascript", new OriginalSource(str, this.identifier()));
	} else {
		sources.set("javascript", new RawSource(str));
	}

	return {
		sources,
		runtimeRequirements: RUNTIME_REQUIREMENTS
	};
}

在生成代码时会取出当前依赖对应的模块生成对应的code:
在这里插入图片描述
在这里插入图片描述
当解析到'module.exports = (__webpack_require__(/*! dll-reference _dll_main */ "dll-reference _dll_main"))("./node_modules/lodash/lodash.js")'会引入查找dll-reference _dll_main模块,此时会触发normalModuleFactory.hooks.factorize方法:

callback(
	null,
	new ExternalModule(
		externalConfig,// _dll_main
		type || globalType, // var
		dependency.request // dll-reference _dll_main
	)
);

返回ExternalModule模块的内容。

总结

我们来理清楚一下逻辑,上面的代码可能有点乱。

  1. DllPlugin主要通过DllEntryPlugin监听compiler.hooks.compilation去添加当前依赖DllEntryDependency对应的模块工厂dllModuleFactory,随后通过监听compiler.hooks.make(compilation 结束之前执行)添加入口依赖DllEntryDependency。并遍历entries给当前入口添加依赖EntryDependency(jquery,lodash)。此时DllEntryDependency对应的DllModuleFactory会返回DllModule模块生成的内容(暴露lodash,jquery)。随后执行LibManifestPlugin插件监听compiler.hooks.emit(输出资源前)获取当前chunk的所有的模块,并通过获取incomingConnections(含有父级模块引入的依赖类型)判断该模块是不是来自EntryDependency(除去运行时的模块),去生成mainfest.json。
  2. DllReferencePlugin通过ExternalModuleFactoryPlugin插件监听normalModuleFactory.hooks.factorize(在初始化解析之前判断是不是externals对象里面的模块),当解析该模块的导入入径时候会触发并返回自定义的dll-reference _dll_main模块内容。同时还会通过DelegatedModuleFactoryPlugin插件监听normalModuleFactory.hooks.module(创建 NormalModule 实例后调用)判断当前请求的模块是不是在mainfest.json文件中。如果在则调用DelegatedModule方法。DelegatedModule类在build过程中由于要从我们打包好的文件中返回模块内容,所以依赖于dll-reference _dll_main。会添加DelegatedSourceDependency。该依赖在解析时会触发我们在开头定义的normalModuleFactory.hooks.factorize函数返回模块内容。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Young soul2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值