splitChunk算法源码分析

2,349 阅读4分钟

最近在学习webpack代码分割时,发现网上很多资料讲的都是splitChunk是如何使用的。本文章主要从splitChunk源码分析,学习splitChunk设计思想。

首先,让我们看一个实例:

1、实例

打包后的结果

那么这里就有一个问题:为什么不是vendors~demo~index~search、vendors~demo~index、vendors~index~search?

答案:lib-flexible<30k

那么我们把minSize = 0

  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 0, // 默认是30kb,minSize设置为0之后
    }
  },

此时打包后的结果

1、哎,此时的结果为什么是vendors-search.js,而不是vendors~index~search.js?

2、vendors? 这个是从那里来的?

答案:

1、默认情况下maxInitialRequests=3,index页面已经有(vendors~demo~index~search.js、vendors~demo~index.js、index.js)三个请求,所以针对index页面来说不能再分割了。

2、webpack会设置默认cacheGroup,这个vendors来自于这个默认cacheGroup中【下面阅读源码时,会详细讲这部分】

现在进入今天的主题:源码的分析

2、源码

先看下整体的流程图:

第一步

给每个chunk添加一个id

	for (const chunk of chunks) {
		indexMap.set(chunk, index++);
	}

第二步

循环module,建立chunk集和:chunkSetsInGraph

for (const module of compilation.modules) {
    const chunksKey = getKey(module.chunksIterable);
  /*	*********************建立chunkSetsInGraph,比如****************
          demo:vue、axios
          index:vue、axios、lib-flexible
          search:vue、lib-flexible
          打包顺序demo->index->search,对应的indexMap的id是1、2、3
          chunkSetsInGraph:
          1->demo
          1、2->axios
          1、2、3->vue
          2->index
          2、3->lib-flexible
          3->search
      */
    if (!chunkSetsInGraph.has(chunksKey)) {
        chunkSetsInGraph.set(chunksKey, new Set(module.chunksIterable));
    }
}

第三步

循环module,创建可能被分割的chunk集和chunksInfoMap。

这个的chunk集和非常重要,是代码分割的开端。

1、先看下上述实例中,生成的chunk集合和chunksInfoMap是什么样子的?

问题:

1、模块集合中default和vendors是怎么来的?

2、chunk集合怎么来的?

3、模块集合chunksInfoMap是怎么生成的?

答案:

1、如果我们在webpack.config.js中不设置cacheGroup,webpack会默认为我们设置cacheGroup。其中像index、demo、search默认设置只有一个default这个cacheGroup。而axios、vue、lib-flexible这种被多个页面引用的模块,会多一个vendors这样的cacheGroup。

2、chunk集合生成

循环模块,根据每个模块所依赖的chunk,得到chunksKey,然后根据getCombinations()这个方法获取combs。

for (const module of compilation.modules) {
	const chunksKey = getKey(module.chunksIterable);
	let combs = combinationsCache.get(chunksKey);
	if (combs === undefined) {
	// *****************创建chunk子集合****************
		combs = getCombinations(chunksKey);
		combinationsCache.set(chunksKey, combs);
	}
}

分析:getCombinations

const getCombinations = key => {
      const chunksSet = chunkSetsInGraph.get(key);
      var array = [chunksSet];
      if (chunksSet.size > 1) {
          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);
                      }
                  }
              }
          }
      }
      return array;
 };

这段代码做了什么?让我们用例子分析:

我们的chunkSetsInGraph是下面子集的map:

那么针对vue【1,2,3】来说,在chunkSetsInGraph中包含的子集就是

那么针对axios【1,2】来说,在chunkSetsInGraph中包含的子集就是

那么针对lib-flexible【2,3】来说,在chunkSetsInGraph中包含的子集就是

同样的道理: 我们的模块对应的chunk子集是:

3、chunksInfoMap生成?

for (const module of compilation.modules) {

	// Prepare some values
	const chunksKey = getKey(module.chunksIterable);
	let combs = combinationsCache.get(chunksKey);
	// chunksKey:1、2
	// console.log('----chunksKey------',chunksKey);
	if (combs === undefined) {
	// *****************创建chunk子集合****************
		combs = getCombinations(chunksKey);
		// console.log('----combs------',combs);
		combinationsCache.set(chunksKey, combs);
	}

	for (const cacheGroupSource of cacheGroups) {
		// ....
		// For all combination of chunk selection
		for (const chunkCombination of combs) {
			// *******************忽略小于minChunks的chunkCombination
			if (chunkCombination.size < cacheGroup.minChunks) continue;
			// Select chunks by configuration
			const {
				chunks: selectedChunks,
				key: selectedChunksKey
			} = getSelectedChunks(chunkCombination, cacheGroup.chunksFilter);
			// *******************模块与chunk之间的映射
			addModuleToChunksInfoMap(cacheGroup, selectedChunks, selectedChunksKey, module);
		}
	}
}

循环每个module的cacheGroups,并在循环中循环上一步骤得到的chunk子集combs。通过addModuleToChunksInfoMap,添加 module。

addModuleToChunksInfoMap代码:

const addModuleToChunksInfoMap = (
  cacheGroup,
  selectedChunks,
  selectedChunksKey,
  module
) => {
  if (selectedChunks.length < cacheGroup.minChunks) return;
  // Determine name for split chunk
  const name = cacheGroup.getName(
      module,
      selectedChunks,
      cacheGroup.key
  );
  const key =
      cacheGroup.key +
      (name ? ` name:${name}` : ` chunks:${selectedChunksKey}`);
  // Add module to maps
  let info = chunksInfoMap.get(key);
  // console.log('---------preview-info-------------',info);
  if (info === undefined) {
      chunksInfoMap.set(
          key,
          (info = {
              modules: new SortableSet(undefined, sortByIdentifier),
              cacheGroup,
              name,
              validateSize: cacheGroup.minSize > 0,
              size: 0,
              chunks: new Set(),
              reuseableChunks: new Set(),
              chunksKeys: new Set()
          })
      );
  }
  // console.log('----------chunksInfoMap------------',chunksInfoMap.get(key));
  info.modules.add(module);
  if (info.validateSize) {
      info.size += module.size();
  }
  if (!info.chunksKeys.has(selectedChunksKey)) {
      info.chunksKeys.add(selectedChunksKey);
      for (const chunk of selectedChunks) {
          info.chunks.add(chunk);
      }
  }
  // console.log('---------last-info------------',info);
};

看看我们的chunksInfoMap长什么样?

2、回到第一张图,合并相同的key

后一个集合与前一个集合的区别?

比如:后一个vendors~search包含lib-flexible+vue 前一个:仅仅只有vue

最终集合:chunksInfoMap

第四步

我们不可能把上面的chunksInfo全都转换成chunk,优化chunksInfoMap,将满足条件的抽离出来,转化成最终生成的chunk文件。

1、怎么优化呢?

循环chunksInfoMap,找出最优bestEntryKey[就是chunksInfoMap的key],直至chunksInfoMap为空。

1、如何找出bestEntryKey?

根据上述条件,找到的第一个bestEntryKey是: vendors name:vendors~demo~index~search

分析: vendors的priority>default的priority,排除default开头的,然后vendors~demo~index~search的chunk个数是3,最大。所以最优。

2、校验bestEntryKey,通过每个chunk的maxRequest:

也就是我们配置的maxInitialRequests

此时我们设定的三个chunk对应的请求文件是:

vendors name:vendors~demo~index~search

那么,请求数是:

demo: 1 index:1 search:1

3、当前的chunksInfoMap是多少?

去掉最优集合bestEntryKey所对应的module之后 循环chunksInfoMap,将其每一项剩下的module的size与minSize对比 把<minSize的项去掉

endors name:vendors~demo~index~search 对应的module是vue

(1)去掉vue

(2)剩下的module.size与minSize比较

此刻 chunksInfomap只剩下四个

接下来进行第二次循环:

此刻的chunksInfoMap:

至此:chunsInfoMap已经为空了,

最后结果

补充:

当bestEntryKey校验失败时