众所周知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;
- 给主模块设置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模块的内容。
总结
我们来理清楚一下逻辑,上面的代码可能有点乱。
- 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。
- 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函数返回模块内容。