众所周知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;
/*
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文件。
此时已生成这两个文件:
接下来我们要配置使用该文件:
...
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首先我们设置了一个依赖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模块的内容。
我们来理清楚一下逻辑,上面的代码可能有点乱。