splitchunksplugin源码分析(webpack 5)

728 阅读4分钟
// 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);
    }
  }
};

image.png

当添加

image.png

情况有所不同,因为默认情况下,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_modules folder
  • 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。