// module包含了此次compilation中所有的module文件
// 在本例中,是index.js, another-module.js以及lodash.js module
for (const module of compilation.modules) {
// 通过module拿到所有匹配的cacheGroups
// 对于index和another-module,是一个包含唯一的default cache group的单元素数组
// 对于lodash,则为包含两个元素的数组,数组元素分别对应default cache group以及defaultVendors cache group
let cacheGroups = this.options.getCacheGroups(module, context);
if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
continue;
}
let cacheGroupIndex = 0;
// 对于lodash的两次遍历,分别是default和defaultVendors cache group
// 两次不同的是defaultVendors默认的minChunks为1
// 但是并不影响什么
// 因为selectedChunks和selectedChunksKey都分别是空数组和0n
for (const cacheGroupSource of cacheGroups) {
// 对于index,another-module经历一次遍历
// 对应cacheGroup的经过处理的default cache group
const cacheGroup = this._getCacheGroup(cacheGroupSource);
// 对于index和another-module,这里是一个chunk数组,且只包含一个元素
// 对于lodash,是一个chunk和set数组,大致是[set, chunk, chunk]
// 其中set包含的元素也是这两个chunk
// 至于原因则是createGetCombinations中的逻辑
const combs = cacheGroup.usedExports ?
getCombsByUsedExports() :
getCombs();
// For all combination of chunk selection
for (const chunkCombination of combs) {
// Break if minimum number of chunks is not reached
// 对于lodash,第一次遍历的chunkCombination是包含两个chunk的set
// 第二和第三次分别是index chunk和another chunk
// 同理,count为1直接跳过
const count =
chunkCombination instanceof Chunk ? 1 : chunkCombination.size;
// 默认的minChunks为2,对于index和another-module,这里不做处理,直接进入下一个module的处理
if (count < cacheGroup.minChunks) continue;
// Select chunks by configuration
// 对于lodash进入这里的逻辑,因为lodash包含在两个chunk中
const {
chunks: selectedChunks,
key: selectedChunksKey
} =
getSelectedChunks(chunkCombination, cacheGroup.chunksFilter);
// 第一次为空数组,直接返回
addModuleToChunksInfoMap(
cacheGroup,
cacheGroupIndex,
selectedChunks,
selectedChunksKey,
module
);
}
cacheGroupIndex++;
}
}
getCombs
我们示例的usedExports没有设置,默认为false
// Prepare some values (usedExports = false)
const getCombs = memoize(() => {
// 获取到包含这个module的所有chunk, 是一个集合
const chunks = chunkGraph.getModuleChunksIterable(module);
const chunksKey = getKey(chunks);
return getCombinations(chunksKey);
});
getModuleChunksIterable
chunkGraph包含_modules map,其中key为module,值为chunkGraphModule
class ChunkGraphModule {
constructor() {
/** @type {SortableSet<Chunk>} */
this.chunks = new SortableSet();
/** @type {Set<Chunk> | undefined} */
this.entryInChunks = undefined;
/** @type {Set<Chunk> | undefined} */
this.runtimeInChunks = undefined;
/** @type {RuntimeSpecMap<ModuleHashInfo>} */
this.hashes = undefined;
/** @type {string | number} */
this.id = null;
/** @type {RuntimeSpecMap<Set<string>> | undefined} */
this.runtimeRequirements = undefined;
/** @type {RuntimeSpecMap<string>} */
this.graphHashes = undefined;
/** @type {RuntimeSpecMap<string>} */
this.graphHashesWithConnections = undefined;
}
}
_getChunkGraphModule(module) {
let cgm = this._modules.get(module);
if (cgm === undefined) {
cgm = new ChunkGraphModule();
this._modules.set(module, cgm);
}
return cgm;
}
getModuleChunksIterable(module) {
const cgm = this._getChunkGraphModule(module);
return cgm.chunks;
}
getKey
// 如果只有单个chunk,返回唯一的chunk
// 否则返回一个bigInt
const getKey = chunks => {
const iterator = chunks[Symbol.iterator]();
let result = iterator.next();
if (result.done) return ZERO;
const first = result.value;
result = iterator.next();
if (result.done) return first;
let key =
chunkIndexMap.get(first) | chunkIndexMap.get(result.value);
while (!(result = iterator.next()).done) {
const raw = chunkIndexMap.get(result.value);
key = key ^ raw;
}
return key;
};
getCombinations
const groupChunkSetsByCount = chunkSets => {
/** @type {Map<number, Array<Set<Chunk>>>} */
const chunkSetsByCount = new Map();
// for of mapIterator只遍历map的values
// 也就是chunk set
for (const chunksSet of chunkSets) {
// 对于index和another-module,这个count都为1
// 因为分别只有一个chunk包含module
const count = chunksSet.size;
let array = chunkSetsByCount.get(count);
if (array === undefined) {
array = [];
chunkSetsByCount.set(count, array);
}
array.push(chunksSet);
}
// 返回的map键为被chunk引用的次数,也就是这个module包含在多少chunk中
// 值则为chunk set array,array中的元素set中的元素个数应等同于map的键
return chunkSetsByCount;
};
const getChunkSetsByCount = memoize(() =>
groupChunkSetsByCount(
// 返回一个mapItearator对象
getChunkSetsInGraph().chunkSetsInGraph.values()
)
);
const getChunkSetsInGraph = memoize(() => {
/** @type {Map<bigint, Set<Chunk>>} */
const chunkSetsInGraph = new Map();
/** @type {Set<Chunk>} */
const singleChunkSets = new Set();
for (const module of compilation.modules) {
// 是一个chunk集合
const chunks = chunkGraph.getModuleChunksIterable(module);
const chunksKey = getKey(chunks);
if (typeof chunksKey === "bigint") {
// 对于lodash,这里在chunkSetsInGraph中设置值,值为chunks集合
if (!chunkSetsInGraph.has(chunksKey)) {
chunkSetsInGraph.set(chunksKey, new Set(chunks));
}
} else {
// 只包含单个chunk
// 对应于index和another-module
singleChunkSets.add(chunksKey);
}
}
return {
chunkSetsInGraph,
singleChunkSets
};
});
const createGetCombinations = (
chunkSets,
singleChunkSets,
chunkSetsByCount
) => {
/** @type {Map<bigint | Chunk, (Set<Chunk> | Chunk)[]>} */
const combinationsCache = new Map();
return key => {
const cacheEntry = combinationsCache.get(key);
if (cacheEntry !== undefined) return cacheEntry;
// 对于index和another-module,直接从这里返回,结果是一个包含chunk的数组
if (key instanceof Chunk) {
const result = [key];
combinationsCache.set(key, result);
return result;
}
// 得到index和another chunk set
const chunksSet = chunkSets.get(key);
/** @type {(Set<Chunk> | Chunk)[]} */
// 对于lodash进入这里的逻辑
// 初始情况下数组就包含一个set
const array = [chunksSet];
for (const [count, setArray] of chunkSetsByCount) {
// "equal" is not needed because they would have been merge in the first step
if (count < chunksSet.size) {
for (const set of setArray) {
if (isSubset(chunksSet, set)) {
array.push(set);
}
}
}
}
// 后续加入index和another chunk
for (const chunk of singleChunkSets) {
if (chunksSet.has(chunk)) {
array.push(chunk);
}
}
combinationsCache.set(key, array);
return array;
};
};
const getCombinationsFactory = memoize(() => {
const {
chunkSetsInGraph,
singleChunkSets
} = getChunkSetsInGraph();
return createGetCombinations(
chunkSetsInGraph,
singleChunkSets,
getChunkSetsByCount()
);
});
const getCombinations = key => getCombinationsFactory()(key);
getSelectedChunks
const getSelectedChunks = (chunks, chunkFilter) => {
let entry = selectedChunksCacheByChunksSet.get(chunks);
if (entry === undefined) {
entry = new WeakMap();
selectedChunksCacheByChunksSet.set(chunks, entry);
}
/** @type {SelectedChunksResult} */
let entry2 = entry.get(chunkFilter);
if (entry2 === undefined) {
/** @type {Chunk[]} */
const selectedChunks = [];
if (chunks instanceof Chunk) {
if (chunkFilter(chunks)) selectedChunks.push(chunks);
} else {
// lodash第一次遍历时chunks为set,进入这里的逻辑
// chunk分别是index chunk和another chunk
// chunkFilter是chunk => !chunk.canBeInitial()
// 而index chunk是initial chunk,所以不进入selectedChunks
// 对于another chunk同理,因为它也是initial chunk
for (const chunk of chunks) {
if (chunkFilter(chunk)) selectedChunks.push(chunk);
}
}
// selectedChunks为空数组
// key为0n
entry2 = {
chunks: selectedChunks,
key: getKey(selectedChunks)
};
entry.set(chunkFilter, entry2);
}
return entry2;
};
addModuleToChunksInfoMap
const addModuleToChunksInfoMap = (
cacheGroup,
cacheGroupIndex,
selectedChunks,
selectedChunksKey,
module
) => {
// Break if minimum number of chunks is not reached
// 第一次为空数组,直接返回
if (selectedChunks.length < cacheGroup.minChunks) return;
// Determine name for split chunk
const name = cacheGroup.getName(
module,
selectedChunks,
cacheGroup.key
);
// Check if the name is ok
const existingChunk = compilation.namedChunks.get(name);
if (existingChunk) {
const parentValidationKey = `${name}|${
typeof selectedChunksKey === "bigint"
? selectedChunksKey
: selectedChunksKey.debugId}`;
const valid = alreadyValidatedParents.get(parentValidationKey);
if (valid === false) return;
if (valid === undefined) {
// Module can only be moved into the existing chunk if the existing chunk
// is a parent of all selected chunks
let isInAllParents = true;
/** @type {Set<ChunkGroup>} */
const queue = new Set();
for (const chunk of selectedChunks) {
for (const group of chunk.groupsIterable) {
queue.add(group);
}
}
for (const group of queue) {
if (existingChunk.isInGroup(group)) continue;
let hasParent = false;
for (const parent of group.parentsIterable) {
hasParent = true;
queue.add(parent);
}
if (!hasParent) {
isInAllParents = false;
}
}
const valid = isInAllParents;
alreadyValidatedParents.set(parentValidationKey, valid);
if (!valid) {
if (!alreadyReportedErrors.has(name)) {
alreadyReportedErrors.add(name);
compilation.errors.push(
new WebpackError(
"SplitChunksPlugin\n" +
`Cache group "${cacheGroup.key}" conflicts with existing chunk.\n` +
`Both have the same name "${name}" and existing chunk is not a parent of the selected modules.\n` +
"Use a different name for the cache group or make sure that the existing chunk is a parent (e. g. via dependOn).\n" +
'HINT: You can omit "name" to automatically create a name.\n' +
"BREAKING CHANGE: webpack < 5 used to allow to use an entrypoint as splitChunk. " +
"This is no longer allowed when the entrypoint is not a parent of the selected modules.\n" +
"Remove this entrypoint and add modules to cache group's 'test' instead. " +
"If you need modules to be evaluated on startup, add them to the existing entrypoints (make them arrays). " +
"See migration guide of more info."
)
);
}
return;
}
}
}
// Create key for maps
// When it has a name we use the name as key
// Otherwise we create the key from chunks and cache group key
// This automatically merges equal names
const key =
cacheGroup.key +
(name ?
` name:${name}` :
` chunks:${keyToString(selectedChunksKey)}`);
// Add module to maps
let info = chunksInfoMap.get(key);
if (info === undefined) {
chunksInfoMap.set(
key,
(info = {
modules: new SortableSet(
undefined,
compareModulesByIdentifier
),
cacheGroup,
cacheGroupIndex,
name,
sizes: {},
chunks: new Set(),
reuseableChunks: new Set(),
chunksKeys: new Set()
})
);
}
const oldSize = info.modules.size;
info.modules.add(module);
if (info.modules.size !== oldSize) {
for (const type of module.getSourceTypes()) {
info.sizes[type] = (info.sizes[type] || 0) + module.size(type);
}
}
const oldChunksKeysSize = info.chunksKeys.size;
info.chunksKeys.add(selectedChunksKey);
if (oldChunksKeysSize !== info.chunksKeys.size) {
for (const chunk of selectedChunks) {
info.chunks.add(chunk);
}
}
};
当添加
情况有所不同,因为默认情况下,webpack只会对async chunk进行split处理,一个chunk实例有一个canBeInitial方法
canBeInitial() {
for (const chunkGroup of this._groups) {
if (chunkGroup.isInitial()) return true;
}
return false;
}
class Entrypoint extends ChunkGroup
constructor(entryOptions, initial = true)
isInitial() {
return this._initial;
}
而非entrypoint实例的chunkgroup实例则直接为false,entrypoint chunkgroup根据initial属性判断是不是initial chunk。
我们在splitChunks中设置chunks: all会影响chunkFilter,从而影响selectedChunks的值,进而会把lodash分离
Webpack will automatically split chunks based on these conditions:
- New chunk can be shared OR modules are from the
node_modulesfolder- New chunk would be bigger than 20kb (before min+gz)
- Maximum number of parallel requests when loading chunks on demand would be lower or equal to 30
- Maximum number of parallel requests at initial page load would be lower or equal to 30
因为lodash位于node_modules且被两个module共享,minify和gzip之前size大于20k,且满足初始加载请求数量小于30(此时为3),需要注意的是,对于node_Modules中的module,默认情况下minChunks为1,只要被某个module引用就会被分离为一个单独的chunk。