webpack系列之六chunk圖生成

做者:肖磊javascript

GitHub:CommanderXLcss

這篇文章主要是經過源碼去探索下 webpack 是如何經過在編譯環節建立的 module graph 來生成對應的 chunk graph。java

首先來了解一些概念及其相互之間的關係:webpack

  1. chunkGroup,由 chunk 組成,一個 chunkGroup 能夠包含多個 chunk,在生成/優化 chunk graph 時會用到;
  2. chunk,由 module 組成,一個 chunk 能夠包含多個 module,它是 webpack 編譯打包後輸出的最終文件;
  3. module,就是不一樣的資源文件,包含了你的代碼中提供的例如:js/css/圖片 等文件,在編譯環節,webpack 會根據不一樣 module 之間的依賴關係去組合生成 chunk

咱們都知道 webpack 打包構建時會根據你的具體業務代碼和 webpack 相關配置來決定輸出的最終文件,具體的文件的名和文件數量也與此相關。而這些文件就被稱爲 chunk。例如在你的業務當中使用了異步分包的 API:git

import('./foo.js').then(bar => bar())
複製代碼

在最終輸出的文件當中,foo.js會被單獨輸出一個 chunk 文件。github

又或者在你的 webpack 配置當中,進行了有關 optimization 優化 chunk 生成的配置:web

module.exports = {
  optimization: {
    runtimeChunk: {
      name: 'runtime-chunk'
    }
  }
}
複製代碼

最終 webpack 會將 webpack runtime chunk 單獨抽離成一個 chunk 後再輸出成一個名爲runtime-chunk.js的文件。數組

而這些生成的 chunk 文件當中便是由相關的 module 模塊所構成的。緩存

接下來咱們就看下 webpack 在工做流當中是如何生成 chunk 的,首先咱們先來看下示例:app

// a.js (webpack config 入口文件)
import add from './b.js'

add(1, 2)

import('./c').then(del => del(1, 2))

-----

// b.js
import mod from './d.js'

export default function add(n1, n2) {
  return n1 + n2
}

mod(100, 11)

-----

// c.js
import mod from './d.js'

mod(100, 11)

import('./b.js').then(add => add(1, 2))

export default function del(n1, n2) {
  return n1 - n2
}

-----

// d.js
export default function mod(n1, n2) {
  return n1 % n2
}
複製代碼

webpack 相關的配置:

// webpack.config.js
module.exports = {
  entry: {
    app: 'a.js'
  },
  output: {
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].bundle.[chunkhash:8].js',
    publicPath: '/'
  },
  optimization: {
    runtimeChunk: {
      name: 'bundle'
    }
  },
}
複製代碼

其中 a.js 爲 webpack config 當中配置的 entry 入口文件,a.js 依賴 b.js/c.js,而 b.js 依賴 d.js,c.js 依賴 d.js/b.js。最終經過 webpack 編譯後,將會生成3個 chunk 文件,其中:

  • bundle.js - 包含了 webpack runtime module 代碼
  • app.bundle.js - 包含了 a.js/b.js/d.js 的代碼
  • 2.bundle.js - 包含了 c.js 的代碼

接下來咱們就經過源碼來看下 webpack 內部是經過什麼樣的策略去完成 chunk 的生成的。

在 webpack 的工做流程當中,當全部的 module 都被編譯完成後,進入到 seal 階段會開始生成 chunk 的相關的工做:

// compilation.js

class Compilation {
  ...
  seal () {
    ...
    this.hooks.beforeChunks.call();
		// 根據 addEntry 方法中收集到入口文件組成的 _preparedEntrypoints 數組
		for (const preparedEntrypoint of this._preparedEntrypoints) {
			const module = preparedEntrypoint.module;
			const name = preparedEntrypoint.name;
			const chunk = this.addChunk(name); // 入口 chunk 且爲 runtimeChunk
			const entrypoint = new Entrypoint(name); // 每個 entryPoint 就是一個 chunkGroup
			entrypoint.setRuntimeChunk(chunk); // 設置 runtime chunk
			entrypoint.addOrigin(null, name, preparedEntrypoint.request);
			this.namedChunkGroups.set(name, entrypoint); // 設置 chunkGroups 的內容
			this.entrypoints.set(name, entrypoint);
			this.chunkGroups.push(entrypoint);

			// 創建起 chunkGroup 和 chunk 之間的關係
			GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
			// 創建起 chunk 和 module 之間的關係
			GraphHelpers.connectChunkAndModule(chunk, module);

			chunk.entryModule = module;
			chunk.name = name;

			this.assignDepth(module);
		}
		this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice());
		// 對 module 進行排序
		this.sortModules(this.modules);
		// 建立完 chunk 以後的 hook
		this.hooks.afterChunks.call(this.chunks);
		//
		this.hooks.optimize.call();

		while (
			this.hooks.optimizeModulesBasic.call(this.modules) ||
			this.hooks.optimizeModules.call(this.modules) ||
			this.hooks.optimizeModulesAdvanced.call(this.modules)
		) {
			/* empty */
		}
		// 優化 module 以後的 hook
		this.hooks.afterOptimizeModules.call(this.modules);
		while (
			this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
			this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
			// 主要涉及到 webpack config 當中的有關 optimization 配置的相關內容
			this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
		) {
			/* empty */
		}
		// 優化 chunk 以後的 hook
		this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
    ...
  }
  ...
}
複製代碼

在這個過程中首先遍歷 webpack config 當中配置的入口 module,每一個入口 module 都會經過addChunk方法去建立一個 chunk,而這個新建的 chunk 爲一個空的 chunk,即不包含任何與之相關聯的 module。以後實例化一個 entryPoint,而這個 entryPoint 爲一個 chunkGroup,每一個 chunkGroup 能夠包含多的 chunk,同時內部會有個比較特殊的 runtimeChunk(當 webpack 最終編譯完成後包含的 webpack runtime 代碼最終會注入到 runtimeChunk 當中)。到此僅僅是分別建立了 chunk 以及 chunkGroup,接下來便調用GraphHelpers模塊提供的connectChunkGroupAndChunkconnectChunkAndModule方法來創建起 chunkGroup 和 chunk 之間的聯繫,以及 chunk 和 入口 module 之間(這裏還未涉及到依賴 module)的聯繫:

// GraphHelpers.js

/** * @param {ChunkGroup} chunkGroup the ChunkGroup to connect * @param {Chunk} chunk chunk to tie to ChunkGroup * @returns {void} */
GraphHelpers.connectChunkGroupAndChunk = (chunkGroup, chunk) => {
	if (chunkGroup.pushChunk(chunk)) {
		chunk.addGroup(chunkGroup);
	}
};

/** * @param {Chunk} chunk Chunk to connect to Module * @param {Module} module Module to connect to Chunk * @returns {void} */
GraphHelpers.connectChunkAndModule = (chunk, module) => {
	if (module.addChunk(chunk)) {
		chunk.addModule(module);
	}
};
複製代碼

例如在示例當中,入口 module 只配置了一個,那麼在處理 entryPoints 階段時會生成一個 chunkGroup 以及一個 chunk,這個 chunk 目前僅僅只包含了入口 module。咱們都知道 webpack 輸出的 chunk 當中都會包含與之相關的 module,在編譯環節進行到上面這一步僅僅創建起了 chunk 和入口 module 之間的聯繫,那麼 chunk 是如何與其餘的 module 也創建起聯繫呢?接下來咱們就看下 webpack 在生成 chunk 的過程中是如何與其依賴的 module 進行關聯的。

與此相關的即是 compilation 實例提供的processDependenciesBlocksForChunkGroups方法。這個方法內部細節較爲複雜,它包含了兩個核心的處理流程:

  1. 遍歷 module graph 模塊依賴圖創建起 basic chunk graph 依賴圖;
  2. 遍歷第一步建立的 chunk graph 依賴圖,依據以前的 module graph 來優化 chunk graph(因爲 chunk graph 是 webpack 最終輸出 chunk 的依據,在這一步的處理流程當中會剔除到一些 chunk graph 重複被建立的 chunk)

咱們先經過一個總體的流程圖來大體瞭解下這個方法內部的處理過程:

chunk process

依據 module graph 創建 chunk graph

在第一個步驟中,首先對此次 compliation 收集到的 modules 進行一次遍歷,在遍歷 module 的過程當中,會對這個 module 的 dependencies 依賴進行處理,獲取這個 module 的依賴模塊,同時還會處理這個 module 的 blocks(即在你的代碼經過異步 API 加載的模塊),每一個異步 block 都會被加入到遍歷的過程中,被當作一個 module 來處理。所以在此次遍歷的過程結束後會創建起基本的 module graph,包含普通的 module 及異步 module(block),最終存儲到一個 map 表(blockInfoMap)當中:

const iteratorBlockPrepare = b => {
  blockInfoBlocks.push(b);
  // 將 block 加入到 blockQueue 當中,從而進入到下一次的遍歷過程中
  blockQueue.push(b);
};

// 此次 compilation 包含的全部的 module
for (const modules of this.modules) {
  blockQueue = [module];
  currentModule = module;
  while (blockQueue.length > 0) {
    block = blockQueue.pop(); // 當前正在被遍歷的 module
    blockInfoModules = new Set(); // module 依賴的同步的 module
    blockInfoBlocks = []; // module 依賴的異步 module(block)

    if (block.variables) {
      iterationBlockVariable(block.variables, iteratorDependency);
    }

    // 在 blockInfoModules 數據集(set)當中添加 dependencies 中的普通 module
    if (block.dependencies) {
      iterationOfArrayCallback(block.dependencies, iteratorDependency);
    }

    // 在 blockInfoBlocks 和 blockQueue 數組當中添加異步 module(block),這樣這些被加入到 blockQueue當中的
    // module 也會進入到遍歷的環節,去獲取異步 module(block)的依賴
    if (block.blocks) {
      iterationOfArrayCallback(block.blocks, iteratorBlockPrepare);
    }

    const blockInfo = {
      modules: Array.from(blockInfoModules),
      blocks: blockInfoBlocks
    };
    // blockInfoMap 上保存了每一個 module 依賴的同步 module 及 異步 blocks
    blockInfoMap.set(block, blockInfo);
  }
}
複製代碼

在咱們的實例當中生成的 module graph 即爲:

chunk-module-graph

當基礎的 module graph (即blockInfoMap)生成後,接下來開始根據 module graph 去生成 basic chunk graph。剛開始仍然是數據的處理,將傳入的 entryPoint(chunkGroup) 轉化爲一個新的 queue,queue 數組當中每一項包含了:

  • action (須要被處理的模塊類型,不一樣的處理類型的模塊會通過不一樣的流程處理,初始爲 ENTER_MODULE(1))
  • block (入口 module)
  • module (入口 module)
  • chunk (seal 階段一開始爲每一個入口 module 建立的空 chunk)
  • chunkGroup (entryPoint 即 chunkGroup 類型)

在咱們提供的示例當中,由於是單入口的,所以這裏 queue 初始化後只有一項。

{
  action: ENTER_MODULE,
  block: a.js,
  module: a.js,
  chunk,
  chunkGroup: entryPoint
}
複製代碼

接下來進入到 queue 的遍歷環節

// 建立異步的 block
// For each async Block in graph
/** * @param {AsyncDependenciesBlock} b iterating over each Async DepBlock * @returns {void} */
const iteratorBlock = b => {
  // 1. We create a chunk for this Block
  // but only once (blockChunkGroups map)
  let c = blockChunkGroups.get(b);
  if (c === undefined) {
    c = this.namedChunkGroups.get(b.chunkName);
    if (c && c.isInitial()) {
      this.errors.push(
        new AsyncDependencyToInitialChunkError(b.chunkName, module, b.loc)
      );
      c = chunkGroup;
    } else {
      // 經過 addChunkInGroup 方法建立新的 chunkGroup 及 chunk,並返回這個 chunkGroup
      c = this.addChunkInGroup(
        b.groupOptions || b.chunkName,
        module, // 這個 block 所屬的 module
        b.loc,
        b.request
      );
      chunkGroupCounters.set(c, { index: 0, index2: 0 });
      blockChunkGroups.set(b, c);
      allCreatedChunkGroups.add(c);
    }
  } else {
    // TODO webpack 5 remove addOptions check
    if (c.addOptions) c.addOptions(b.groupOptions);
    c.addOrigin(module, b.loc, b.request);
  }

  // 2. We store the Block+Chunk mapping as dependency for the chunk
  let deps = chunkDependencies.get(chunkGroup);
  if (!deps) chunkDependencies.set(chunkGroup, (deps = []));
  // 當前 chunkGroup 所依賴的 block 及 chunkGroup
  deps.push({
    block: b,
    chunkGroup: c,
    couldBeFiltered: true
  });
  // 異步的 block 使用建立的新的 chunkGroup
  // 3. We enqueue the DependenciesBlock for traversal
  queueDelayed.push({
    action: PROCESS_BLOCK,
    block: b,
    module: module,
    chunk: c.chunks[0], // 獲取新建立的 chunkGroup 當中的第一個 chunk,即 block 須要被加入的 chunk
    chunkGroup: c // 異步 block 使用新建立的 chunkGroup
  });
};
...
const ADD_AND_ENTER_MODULE = 0;
const ENTER_MODULE = 1;
const PROCESS_BLOCK = 2;
const LEAVE_MODULE = 3;
...
const chunkGroupToQueueItem = chunkGroup => ({
  action: ENTER_MODULE,
  block: chunkGroup.chunks[0].entryModule,
  module: chunkGroup.chunks[0].entryModule,
  chunk: chunkGroup.chunks[0],
  chunkGroup
});

let queue = inputChunkGroups.map(chunkGroupToQueueItem).reverse()

while (queue.length) { // 外層 queue 遍歷
  while (queue.length) { // 內層 queue 遍歷
    const queueItem = queue.pop();
    module = queueItem.module;
    block = queueItem.block;
    chunk = queueItem.chunk;
    chunkGroup = queueItem.chunkGroup;

    switch (queueItem.action) {
      case ADD_AND_ENTER_MODULE: {
        // 添加 module 至 chunk 當中
        // We connect Module and Chunk when not already done
        if (chunk.addModule(module)) {
          module.addChunk(chunk);
        } else {
          // already connected, skip it
          break;
        }
      }
      // fallthrough
      case ENTER_MODULE: {
        ...
        queue.push({
          action: LEAVE_MODULE,
          block,
          module,
          chunk,
          chunkGroup
        });
      }
      // fallthrough
      case PROCESS_BLOCK: {
        // get prepared block info
        const blockInfo = blockInfoMap.get(block);
        // Traverse all referenced modules
        for (let i = blockInfo.modules.length - 1; i >= 0; i--) {
          const refModule = blockInfo.modules[i];
          if (chunk.containsModule(refModule)) {
            // skip early if already connected
            continue;
          }
          // enqueue the add and enter to enter in the correct order
          // this is relevant with circular dependencies
          queue.push({
            action: ADD_AND_ENTER_MODULE,
            block: refModule, // 依賴 module
            module: refModule, // 依賴 module
            chunk, // module 所屬的 chunk
            chunkGroup // module 所屬的 chunkGroup
          });
        }

        // 開始建立異步的 chunk
        // Traverse all Blocks
        iterationOfArrayCallback(blockInfo.blocks, iteratorBlock);

        if (blockInfo.blocks.length > 0 && module !== block) {
          blocksWithNestedBlocks.add(block);
        }
        break;
      }
      case LEAVE_MODULE: {
        ...
        break;
      }
    }
  }
  const tempQueue = queue;
  queue = queueDelayed.reverse();
  queueDelayed = tempQueue;
}
複製代碼

經過源碼咱們發現對於 queue 的處理進行了2次遍歷的操做(內層和外層),具體爲何會須要進行2次遍歷操做後文會說明。首先咱們來看下內層的遍歷操做,首先根據 action 的類型進入到對應的處理流程當中:

首先進入到 ENTRY_MODULE 的階段,會在 queue 中新增一個 action 爲 LEAVE_MODULE 的項會在後面遍歷的流程當中使用,當 ENTRY_MODULE 的階段進行完後,當即進入到了 PROCESS_BLOCK 階段:

在這個階段當中根據 module graph 依賴圖保存的模塊映射 blockInfoMap 獲取這個 module(稱爲A) 的同步依賴 modules 及異步依賴 blocks。

接下來遍歷 modules 當中的包含的 module(稱爲B),判斷當前這個 module(A) 所屬的 chunk 當中是否包含了其依賴 modules 當中的 module(B),若是不包含的話,那麼會在 queue 當中加入新的項,新加入的項目的 action 爲 ADD_AND_ENTER_MODULE,即這個新增項在下次遍歷的時候,首先會進入到 ADD_AND_ENTER_MODULE 階段。

當新項被 push 至 queue 當中後,即這個 module 依賴的還未被處理的 module(A) 被加入到 queue 當中後,接下來開始調用iteratorBlock方法來處理這個 module(A) 依賴的全部的異步 blocks,在這個方法內部主要完成的工做是:

  1. 調用addChunkInGroup爲這個異步的 block 新建一個 chunk 以及 chunkGroup,同時調用 GraphHelpers 模塊提供的 connectChunkGroupAndChunk 創建起這個新建的 chunk 和 chunkGroup 之間的聯繫。這裏新建的 chunk 也就是在你的代碼當中使用異步API 加載模塊時,webpack 最終會單獨給這個模塊輸出一個 chunk,可是此時這個 chunk 爲一個空的 chunk,沒有加入任何依賴的 module;

  2. 創建起當前 module 所屬的 chunkGroup 和 block 以及這個 block 所屬的 chunkGroup 之間的依賴關係,並存儲至 chunkDependencies Map 表中,這個 Map 表主要用於後面優化 chunk graph;

  3. 向 queueDelayed 中添加一個 action 類型爲 PROCESS_BLOCK,module 爲當前所屬的 module,block 爲當前 module 依賴的異步模塊,chunk(chunkGroup 當中的第一個 chunk) 及 chunkGroup 都是處理異步模塊生成的新項,而這裏向 queueDelayed 數據集當中添加的新項主要就是用於 queue 的外層遍歷。

在 ENTRY_MODULE 階段即完成了將 entry module 的依賴 module 加入到 queue 當中,這個階段結束後即進入到了 queue 內層第二輪的遍歷的環節:

在對 queue 的內層遍歷過程中,咱們主要關注 queue 當中每項 action 類型爲 ADD_AND_ENTER_MODULE 的項,在進行實際的處理時,進入到 ADD_AND_ENTER_MODULE 階段,這個階段完成的主要工做就是判斷 chunk 所依賴的 module 是否已經添加到 chunk 內部(chunk.addModule方法),若是沒有的話,那麼便會將 module 加入到 chunk,並進入到 ENTRY_MODULE 階段,進入到後面的流程(見上文),若是已經添加過了,那麼則會跳過此次遍歷。

當對 queue 這一輪的內層的遍歷完成後(每一輪的內層遍歷都對應於同一個 chunkGroup,即每一輪內層的遍歷都是對這個 chunkGroup 當中所包含的全部的 module 進行處理),開始進入到外層的遍歷當中,即對 queueDelayed 數據集進行處理。

以上是在processDependenciesBlocksForChunkGroups方法內部對於 module graph 和 chunk graph 的初步處理,最終的結果就是根據 module graph 創建起了 chunk graph,將本來空的 chunk 裏面加入其對應的 module 依賴。

entryPoint 包含了 a, b, d 3個 module,而 a 的異步依賴模塊 c 以及 c 的同步依賴模塊 d 同屬於新建立的 chunkGroup2,chunkGroup2 中只有一個 chunk,而 c 的異步模塊 b 屬於新建立的 chunkGroup3。

chunk-graph

優化 chunk graph

接下來進入到第二個步驟,遍歷 chunk graph,經過和依賴的 module 之間的使用關係來創建起不一樣 chunkGroup 之間的父子關係,同時剔除一些沒有創建起聯繫的 chunk。

/** * Helper function to check if all modules of a chunk are available * * @param {ChunkGroup} chunkGroup the chunkGroup to scan * @param {Set<Module>} availableModules the comparitor set * @returns {boolean} return true if all modules of a chunk are available */
// 判斷chunkGroup當中是否已經包含了全部的 availableModules
const areModulesAvailable = (chunkGroup, availableModules) => {
  for (const chunk of chunkGroup.chunks) {
    for (const module of chunk.modulesIterable) {
      // 若是在 availableModules 存在沒有的 module,那麼返回 false
      if (!availableModules.has(module)) return false;
    }
  }
  return true;
};

// For each edge in the basic chunk graph
/** * @param {TODO} dep the dependency used for filtering * @returns {boolean} used to filter "edges" (aka Dependencies) that were pointing * to modules that are already available. Also filters circular dependencies in the chunks graph */
const filterFn = dep => {
  const depChunkGroup = dep.chunkGroup;
  if (!dep.couldBeFiltered) return true;
  if (blocksWithNestedBlocks.has(dep.block)) return true;
  if (areModulesAvailable(depChunkGroup, newAvailableModules)) {
    return false; // break, all modules are already available
  }
  dep.couldBeFiltered = false;
  return true;
};

/** @type {Map<ChunkGroup, ChunkGroupInfo>} */
const chunkGroupInfoMap = new Map();

/** @type {Queue<ChunkGroup>} */
const queue2 = new Queue(inputChunkGroups);
for (const chunkGroup of inputChunkGroups) {
  chunkGroupInfoMap.set(chunkGroup, {
    minAvailableModules: undefined,
    availableModulesToBeMerged: [new Set()]
  });
}

...

while (queue2.length) {
  chunkGroup = queue2.dequeue();
  const info = chunkGroupInfoMap.get(chunkGroup);
  const availableModulesToBeMerged = info.availableModulesToBeMerged;
  let minAvailableModules = info.minAvailableModules;
  ...
}

...
複製代碼

首先仍是完成一些數據的初始化工做,chunkGroupInfoMap 存放了不一樣 chunkGroup 相關信息:

  • minAvailableModules (chunkGroup 可追蹤的最小 module 數據集)
  • availableModulesToBeMerged (遍歷環節所使用的 module 集合)
/** @type {Map<ChunkGroup, ChunkGroupInfo>} */
const chunkGroupInfoMap = new Map();

/** @type {Queue<ChunkGroup>} */
const queue2 = new Queue(inputChunkGroups);
for (const chunkGroup of inputChunkGroups) {
  chunkGroupInfoMap.set(chunkGroup, {
    minAvailableModules: undefined,
    availableModulesToBeMerged: [new Set()]
  });
}
複製代碼

完成以後,遍歷 queue2,其中的每一項都是 chunkGroup,初始爲 entry 對應的 chunkGroup,在咱們的示例中因爲存在動態加載的模塊c,因此也會加入到queue2隊列中當作一個「獨立」的 entry 處理,可是是存在父子關係的,它依託於入口 entry 所對應的 chunkGroup。

while (queue2.length) {
  chunkGroup = queue2.dequeue();
  const info = chunkGroupInfoMap.get(chunkGroup);
  const availableModulesToBeMerged = info.availableModulesToBeMerged;
  let minAvailableModules = info.minAvailableModules;

  // 1. Get minimal available modules
  // It doesn't make sense to traverse a chunk again with more available modules.
  // This step calculates the minimal available modules and skips traversal when
  // the list didn't shrink.
  availableModulesToBeMerged.sort(bySetSize);
  let changed = false;
  for (const availableModules of availableModulesToBeMerged) {
    if (minAvailableModules === undefined) {
      minAvailableModules = new Set(availableModules);
      info.minAvailableModules = minAvailableModules;
      changed = true;
    } else {
      for (const m of minAvailableModules) {
        if (!availableModules.has(m)) {
          minAvailableModules.delete(m);
          changed = true;
        }
      }
    }
  }
  availableModulesToBeMerged.length = 0;
  if (!changed) continue;

  // 獲取這個 chunkGroup 的 deps 數組,包含異步的 block 及 對應的 chunkGroup
  // 2. Get the edges at this point of the graph
  const deps = chunkDependencies.get(chunkGroup);
  if (!deps) continue;
  if (deps.length === 0) continue;

  // 根據以前的 minAvailableModules 建立一個新的 newAvailableModules 數據集
  // 即以前全部遍歷過的 chunk 當中的 module 都會保存到這個數據集當中,不停的累加
  // 3. Create a new Set of available modules at this points
  newAvailableModules = new Set(minAvailableModules);
  for (const chunk of chunkGroup.chunks) {
    for (const m of chunk.modulesIterable) { // 這個 chunk 當中所包含的 module
      newAvailableModules.add(m);
    }
  }

  // 邊界條件,及異步的 block 所在的 chunkGroup
  // 4. Foreach remaining edge
  const nextChunkGroups = new Set();
  // 異步 block 依賴
  for (let i = 0; i < deps.length; i++) {
    const dep = deps[i];

    // Filter inline, rather than creating a new array from `.filter()`
    if (!filterFn(dep)) {
      continue;
    }
    // 這個 block 所屬的 chunkGroup,在 iteratorBlock 方法內部建立的
    const depChunkGroup = dep.chunkGroup;
    const depBlock = dep.block;

    // 開始創建 block 和 chunkGroup 之間的關係
    // 在爲 block 建立新的 chunk 時,僅僅創建起了 chunkGroup 和 chunk 之間的關係,
    // 5. Connect block with chunk
    GraphHelpers.connectDependenciesBlockAndChunkGroup(
      depBlock,
      depChunkGroup
    );

    // 創建起新建立的 chunkGroup 和此前的 chunkGroup 之間的相互聯繫
    // 6. Connect chunk with parent
    GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup);

    nextChunkGroups.add(depChunkGroup);
  }

  // 7. Enqueue further traversal
  for (const nextChunkGroup of nextChunkGroups) {
    ...

    // As queue deduplicates enqueued items this makes sure that a ChunkGroup
    // is not enqueued twice
    queue2.enqueue(nextChunkGroup);
  }
}
複製代碼

獲取在第一階段的 chunkDependencies 當中緩存的 chunkGroup 的 deps 數組依賴,chunkDependencies 中保存了不一樣 chunkGroup 所依賴的異步 block,以及同這個 block 一同建立的 chunkGroup(目前兩者僅僅是存於一個 map 結構當中,還未創建起 chunkGroup 和 block 之間的依賴關係)。

若是 deps 數據不存在或者長度爲0,那麼會跳過遍歷 deps 當中的 chunkGroup 流程,不然會爲這個 chunkGroup 建立一個新的 available module 數據集 newAvailableModules,開始遍歷這個 chunkGroup 當中全部的 chunk 所包含的 module,並加入到 newAvailableModules 這一數據集當中。並開始遍歷這個 chunkGroup 的 deps 數組依賴,這個階段主要完成的工做就是:

  1. 判斷 chunkGroup 提供的 newAvailableModules(能夠將 newAvailableModules 理解爲這個 chunkGroup 全部 module 的集合setA)和 deps 依賴中的 chunkGroup (由異步 block 建立的 chunkGroup)所包含的 chunk 當中全部的 module 集合(setB)包含關係:
  • 若是在 setB 當中有 setA 沒有的 module(通常是異步的 block),它們在 chunk graph 被當作了(edge 條件),那說明目前已經遍歷過的 chunk 裏面的 module 組成的 setA 還未包含全部用到的 module,而這些未被包含的 module 就存在於 deps 依賴中的 chunkGroup 當中,所以還須要繼續遍歷 deps 依賴中的 chunkGroup
  • 若是在 setB 當中的全部的 module 都已經存在於了 setA 當中,說明依賴的 chunkGroup 中全部使用的 module 已經包含在了目前已經遍歷過的 chunk 當中了,那麼就不須要進行後面的流程,直接跳過,進行下一個的依賴遍歷;
  1. 經過 GraphHelpers 模塊提供的輔助函數connectDependenciesBlockAndChunkGroup創建起 deps 依賴中的異步 block 和 chunkGroup 的依賴關係;
  2. 經過 GraphHelpers 模塊提供的輔助函數connectChunkGroupParentAndChild創建起 chunkGroup 和 deps 依賴中的 chunkGroup 之間的依賴關係 (這個依賴關係也決定了在 webpack 編譯完成後輸出的文件當中是否會有 deps 依賴中的 chunkGroup 所包含的 chunk)
  3. 將 deps 依賴中的 chunkGroup 加入到 nextChunkGroups 數據集當中,接下來就進入到遍歷新加入的 chunkGroup 環節。
  4. 當以上全部的遍歷過程都結束後,接下來開始遍歷在處理異步 block 建立的 chunkGroup 組成的數據集(allCreatedChunkGroups),開始處理沒有依賴關係的 chunkGroup(chunkGroup 之間的依賴關係是在👆第3步的過程當中創建起來的),若是遇到沒有任何依賴關係的 chunkGroup,那麼就會將這些 chunkGroup 當中所包含的全部 chunk 從 chunk graph 依賴圖當中剔除掉。最終在 webpack 編譯過程結束輸出文件的時候就不會生成這些 chunk。

那麼在咱們給出的示例當中,通過在上面提到的這些步驟,第一階段處理 entryPoint(chunkGroup),以及其包含的全部的 module,在處理過程當中發現這個 entryPoint 依賴異步 block c,它包含在了 blocksWithNestedBlocks 數據集當中,依據對應的過濾規則,是須要繼續遍歷異步 block c 所在的 chunkGroup2。接下來在處理 chunkGroup2 的過程中,它依賴 chunkGroup3,且這個 chunkGroup3 包含異步 block d,由於在第一階段處理 entryPoint 過程當中完成了一輪 module 集的收集,其中就包含了同步的 module d,這裏能夠想象獲得的是同步的 module d 和異步 block d 最終只可能輸出一個,且同步的 module d 要比異步的 block d 的優先級更高。所以最終模塊 d 的代碼會以同步的 module d 的形式被輸出到 entryPoint 所包含的 chunk 當中,這樣包含異步 block d 的 chunkGroup3 也就相應的不會再被輸出,即會被從 chunk graph 當中剔除掉。

最終會生成的 chunk 依賴圖爲:

chunk-graph-2

以上就是經過源碼分析了 webpack 是如何構建 module graph,以及是如何經過 module graph 去生成 chunk graph 的,當你讀完這篇文章後應該就大體瞭解了在你每次構建完成後,你的項目應用中目標輸出文件夾出現的不一樣的 chunk 文件是通過哪些過程而產生的。

相關文章
相關標籤/搜索