媽媽不再用擔憂個人優化|Webpack系列(二):SplitChunksPlugin源碼講解

本文 1500 字,讀完須要 10 分鐘,代碼加註解 500 行,瀏覽須要 15 分鐘,若有錯誤請指正。前端

上期經過項目優化分享和實例解析:在淘寶優化了一個大型項目,分享一些乾貨(代碼實例,圖文結合),把 webpack 打包優化利器 SplitChunksPlugin 系統講解了一遍。本期進一步深刻,分析 SplitChunksPlugin 源碼,搞清楚 webpack 是怎麼和 SplitChunksPlugin 配合,完成打包優化的。剛好 webpack5 的 beta 版本正在迭代,因此我將直接解析 webpack5.0.0-beta.17 的源碼,理解原理的同時,順便從源碼級別領略一些 Webpack5 新特性。兩期講解都建議細品。webpack

SplitChunksPlugin 默認配置從何而來

實際開發會發現,有時 webpack 的默認打包結果並不像官網描述的那樣,到底問題出在哪裏?去源碼找答案。webpack 在 default.js 文件統一進行了默認配置,其中 SplitChunksPlugin 的默認配置源碼以下:git

// D和F都是給對象賦值的方法,區別就在於F傳的是方法,能根據邏輯判斷分配不一樣的值
const D = (obj, prop, value) => {
  if (obj[prop] === undefined) {
    obj[prop] = value;
  }
};

const F = (obj, prop, factory) => {
  if (obj[prop] === undefined) {
    obj[prop] = factory();
  }
};

const applyOptimizationDefaults = (
  optimization,
  { production, development, records }
) => {
  // 省略其餘配置
  const { splitChunks } = optimization;
  if (splitChunks) {
    D(splitChunks, "hidePathInfo", production);
    D(splitChunks, "chunks", "async");
    D(splitChunks, "minChunks", 1);
    //這些屬性默認值在production和development模式下有不一樣取值
    F(splitChunks, "minSize", () => (production ? 30000 : 10000));
    F(splitChunks, "minRemainingSize", () => (development ? 0 : undefined));
    //開發模式下maxAsyncRequests爲無窮大
    F(splitChunks, "maxAsyncRequests", () => (production ? 6 : Infinity));
    F(splitChunks, "maxInitialRequests", () => (production ? 4 : Infinity));
    //官網上仍是默認以"~"爲分割符,源碼中變爲"-"
    D(splitChunks, "automaticNameDelimiter", "-");
    const { cacheGroups } = splitChunks;
    F(cacheGroups, "default", () => ({
      idHint: "",
      reuseExistingChunk: true,
      minChunks: 2,
      priority: -20,
    }));
    F(cacheGroups, "defaultVendors", () => ({
      idHint: "vendors",
      reuseExistingChunk: true,
      test: NODE_MODULES_REGEXP,
      priority: -10,
    }));
  }
};
複製代碼

看了源碼才知道,plitChunksPlugin 的默認配置和官方文檔中並不徹底相同,幾個取值會隨模式切換而變化,但官網對外屏蔽了這些細節,估計由於 webpack 默認狀況下是開發模式,因此官網並無展現開發模式下的默認值,而咱們開發的時候經常切換爲開發模式,因此須要注意這些區別。github

以上期項目爲例,咱們看看新版本下的打包結果: web

Webpack5打包結果
包名不只採用「-」分隔,並且變得更簡短了,這是由於 Webpack5 用模塊自己的名字和類型替代了本來的引用包名稱組合的形式。這種方式更一目瞭然,直接幫你定位到特定文件,若是想知道模塊被哪些包引用,獲得更多細節信息,可使用命令:webpack --display-chunks。

SplitChunksPlugin 的三步走戰略

Webpack 插件統一以 apply 方法爲入口,而後註冊優化事件,插件邏輯都在 SplitChunksPlugin.js 文件中:面試

apply(compiler) {
	// compiler是webpack編譯器實例,全局惟一,包含webpack環境的全部配置信息
	compiler.hooks.thisCompilation.tap("SplitChunksPlugin", compilation => {
		// 省略次要代碼
		// compilation是每次編譯的資源實例,經過它能獲得當前編譯的全部模塊和資源信息
		// compilation擁有事件流機制,能夠監聽事件並觸發回調(就是觀察者模式),這裏就是在優化事件發生時,執行代碼分割邏輯
		compilation.hooks.optimizeChunks.tap(
			{
				name: "SplitChunksPlugin",
				stage: STAGE_ADVANCED
			},
			chunks => {
				// 三步走,完成代碼分割優化
			}
		)
	}
}
複製代碼

在整個編譯週期中,compilation 會在生成 chunkGraph(包含代碼塊依賴關係的圖結構)以後,觸發 optimizeChunks 事件並傳入 chunks,開始代碼分割優化過程,全部優化都在 optimizeChunks 事件的回調函數中完成。算法

準備階段

進行優化的預處理,定義優化過程當中一些必要的方法和數據結構,在後續階段會用到:後端

const chunkSetsInGraph = new Map();
/** * 優化的核心就是提取公共的module,因此要爲每一個module和包含該module的chunks生成一個key值, * 每一個module對應一個key,也對應全部包含該module的chunks集合(chunksSet), * 這樣咱們就知道每一個module在哪些chunk中重複了,這對優化起了關鍵做用。 * 這裏將該key值和這些chunks創建映射關係,存在chunkSetsInGraph中,便於以後經過key值取出這些chunksSet,進行優化。 */
for (const module of compilation.modules) {
  const chunksKey = getKey(chunkGraph.getModuleChunksIterable(module));
  if (!chunkSetsInGraph.has(chunksKey)) {
    chunkSetsInGraph.set(
      chunksKey,
      new Set(chunkGraph.getModuleChunksIterable(module))
    );
  }
}

const chunkSetsByCount = new Map();
/** * 上面咱們知道了每一個module在哪些chunks中重複,如今要根據重複次數將這些信息整理歸類,存在chunkSetsByCount中。 * 這麼作是爲了匹配minChunks屬性,能夠根據minChunks(module的最小重複次數)直接找到對應的chunksSet的集合, * 不符合minChunks的chunksSet會被天然排除在外。 * 注意,一個module對應一個chunksSet,一個count對應多個chunksSet,也就對應多個module */
for (const chunksSet of chunkSetsInGraph.values()) {
  // 遍歷chunkSetsInGraph,統計每一個chunks集合的chunk數量,即每一個module的重複次數,創建數量和chunks集合的映射
  const count = chunksSet.size;
  let array = chunkSetsByCount.get(count);
  if (array === undefined) {
    array = [];
    chunkSetsByCount.set(count, array);
  }
  array.push(chunksSet);
}

const combinationsCache = new Map();

// 得到可能知足minChunks條件chunks集合,用於後續和minChunks條件比對
const getCombinations = (key) => {
  // 根據key值取出該module對應的chunks集合(chunksSet)
  const chunksSet = chunkSetsInGraph.get(key);
  var array = [chunksSet];
  if (chunksSet.size > 1) {
    for (const [count, setArray] of chunkSetsByCount) {
      if (count < chunksSet.size) {
        // 每一個module對應一個set,這裏是找出setArray的子集,防止遺漏
        for (const set of setArray) {
          if (isSubset(chunksSet, set)) {
            array.push(set);
          }
        }
      }
    }
  }
  return array;
};

// 關鍵的Map結構,每一項對應一個分割出來的緩存組,鍵名爲根據name屬性生成的key值,鍵值爲該key值對應的modules、chunks和cacheGroup信息對象
const chunksInfoMap = new Map();

const addModuleToChunksInfoMap = (
  cacheGroup,
  selectedChunks,
  selectedChunksKey,
  module
) => {
  const name = cacheGroup.getName(module, selectedChunks, cacheGroup.key);
  // 檢查名稱是否和已有的chunk有衝突,此外,webpack5之後不容許cacheGroup名稱覆蓋入口名稱,會報錯
  if (!alreadyValidatedNames.has(name)) {
    alreadyValidatedNames.add(name);
    if (compilation.namedChunks.has(name)) {
      // 省略報錯內容
    }
  }
  /** * 若是cachGroup有name,就用cacheGroup的key和name做爲key,若是沒有,就是用從cacheGroup和chunk生成的key值(selectedChunksKey)。 * 若是cachGroup有name,屬於該cachGroup的module在這裏的key值都是同樣的,因此會合併到一個info中,最後打成一個包, * 而若是cachGroup沒有name,每一個module會生成不一樣key,最後每一個module都會單獨打成一個包, * 這裏建議和上一期的「寶藏屬性Name」一塊兒理解 */
  const key =
    cacheGroup.key + (name ? ` name:${name}` : ` chunks:${selectedChunksKey}`);
  // Add module to maps
  let info = chunksInfoMap.get(key);
  if (info === undefined) {
    chunksInfoMap.set(
      key,
      (info = {
        modules: new SortableSet(undefined, compareModulesByIdentifier),
        cacheGroup,
        name,
        // 判斷minSize是否爲正值
        validateSize:
          hasNonZeroSizes(cacheGroup.minSize) ||
          hasNonZeroSizes(cacheGroup.minRemainingSize),
        sizes: {},
        chunks: new Set(),
        reuseableChunks: new Set(),
        chunksKeys: new Set(),
      })
    );
  }
  info.modules.add(module);
  // 計算代碼塊的體積
  if (info.validateSize) {
    for (const type of module.getSourceTypes()) {
      info.sizes[type] = (info.sizes[type] || 0) + module.size(type);
    }
  }
  // 將代碼塊加入到chunksInfoMap中,以便最後打包
  if (!info.chunksKeys.has(selectedChunksKey)) {
    info.chunksKeys.add(selectedChunksKey);
    for (const chunk of selectedChunks) {
      info.chunks.add(chunk);
    }
  }
};
複製代碼

準備過程當中,chunksInfoMap 和 addModuleToChunksInfoMap 是最重要的兩個角色,重點提一提:緩存

  • chunksInfoMap 存儲着代碼分割信息,每一項都是一個緩存組,對應於最終要分割出哪些額外代碼塊,會不斷迭代,最終將代碼分割結果加入 chunkGraph 中,而 chunkGraph 最終會生成咱們見到的打包文件。固然,這些緩存組目前還附帶一些額外信息,好比 cacheGroup,就是咱們配置的 cacheGroup 代碼分割規則,用於後續校驗;再好比 sizes,記錄了緩存組中模塊的整體積,用於以後判斷是否符合咱們配置的 minSize 條件。
  • addModuleToChunksInfoMap 就是向 chunksInfoMap 中添加新的代碼分割信息,每次添加都會根據 key 值選擇是建立新的緩存組仍是在已有緩存組中添加模塊,並更新緩存組信息。

模塊分組階段

準備完成後,遍歷全部 module,將符合條件的 module 經過 addModuleToChunksInfoMap 方法存到 chunksInfoMap 中,進行分組,其實就是建立緩存組的過程:數據結構

for (const module of compilation.modules) {
  // 經過getCacheGroups獲得module從屬的cacheGroup,一個module可能符合多個cacheGroup的條件
  // Get cache group
  let cacheGroups = this.options.getCacheGroups(module, context);
  if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
    continue;
  }

  // 包含同一個module的chunk會對應惟一的key值,以便接下來獲取要優化的chunks集合
  const chunksKey = getKey(
    // 得到全部包含該module的chunk
    chunkGraph.getModuleChunksIterable(module)
  );
  let combs = combinationsCache.get(chunksKey);
  if (combs === undefined) {
    // 這是準備階段定義的方法,得到可能知足minChunks條件chunks集合,用於後續和minChunks條件比對
    combs = getCombinations(chunksKey);
    combinationsCache.set(chunksKey, combs);
  }

  for (const cacheGroupSource of cacheGroups) {
    // 將的cacheGroup配置都取出來,若是值不存在,則會從splitChunks全局配置繼承
    const cacheGroup = {
      key: cacheGroupSource.key,
      priority: cacheGroupSource.priority || 0,
      // chunksFilter對應cacheGroup配置中的chunks屬性,只是進行了一些處理,變成了一個方法
      chunksFilter: cacheGroupSource.chunksFilter || this.options.chunksFilter,
      minSize: mergeSizes(
        cacheGroupSource.minSize,
        cacheGroupSource.enforce ? undefined : this.options.minSize
      ),
      minRemainingSize: mergeSizes(
        cacheGroupSource.minRemainingSize,
        cacheGroupSource.enforce ? undefined : this.options.minRemainingSize
      ),
      minSizeForMaxSize: mergeSizes(
        cacheGroupSource.minSize,
        this.options.minSize
      ),
      maxAsyncSize: mergeSizes(
        cacheGroupSource.maxAsyncSize,
        cacheGroupSource.enforce ? undefined : this.options.maxAsyncSize
      ),
      maxInitialSize: mergeSizes(
        cacheGroupSource.maxInitialSize,
        cacheGroupSource.enforce ? undefined : this.options.maxInitialSize
      ),
      minChunks:
        cacheGroupSource.minChunks !== undefined
          ? cacheGroupSource.minChunks
          : cacheGroupSource.enforce
          ? 1
          : this.options.minChunks,
      maxAsyncRequests:
        cacheGroupSource.maxAsyncRequests !== undefined
          ? cacheGroupSource.maxAsyncRequests
          : cacheGroupSource.enforce
          ? Infinity
          : this.options.maxAsyncRequests,
      maxInitialRequests:
        cacheGroupSource.maxInitialRequests !== undefined
          ? cacheGroupSource.maxInitialRequests
          : cacheGroupSource.enforce
          ? Infinity
          : this.options.maxInitialRequests,
      getName:
        cacheGroupSource.getName !== undefined
          ? cacheGroupSource.getName
          : this.options.getName,
      filename:
        cacheGroupSource.filename !== undefined
          ? cacheGroupSource.filename
          : this.options.filename,
      automaticNameDelimiter:
        cacheGroupSource.automaticNameDelimiter !== undefined
          ? cacheGroupSource.automaticNameDelimiter
          : this.options.automaticNameDelimiter,
      idHint:
        cacheGroupSource.idHint !== undefined
          ? cacheGroupSource.idHint
          : cacheGroupSource.key,
      reuseExistingChunk: cacheGroupSource.reuseExistingChunk,
    };
    // 這裏就是根據咱們的cacheGroup配置,篩選出符合minChunks和chunks規則的chunk
    for (const chunkCombination of combs) {
      // 若是不知足minChunks,就直接break,不創建這個緩存組,也就不會分割相應代碼
      if (chunkCombination.size < cacheGroup.minChunks) continue;
      // 解構賦值,得到符合chunksFilter("initial" | "async" | "all",其實就是chunks屬性)條件的chunks
      const {
        chunks: selectedChunks,
        key: selectedChunksKey,
      } = getSelectedChunks(chunkCombination, cacheGroup.chunksFilter);

      // 將目前符合條件的modules、chunks和cacheGroup信息存到chunksInfoMap中
      addModuleToChunksInfoMap(
        cacheGroup,
        selectedChunks,
        selectedChunksKey,
        module
      );
    }
  }
}
複製代碼

在分組階段,會將 cacheGroup 的配置所有取出,順便檢查配置中的 minChunks 和 chunks 規則,只有符合條件的分組纔會建立。本階段只檢查和數量有關的配置,其餘配置在下個階段進行校驗。

排隊檢查階段

上一階段生成了緩存組信息 chunksInfoMap,本階段按照用戶的 cacheGroup 配置,一項一項檢查 chunksInfoMap 中各個緩存組是否符合規則,去除不符合的,留下符合的加入 compilation 的 chunkGraph 中,直至把所有代碼分割結果都更新到 chunkGraph 中。代碼比較長,但都是循序漸進,先進行規則校驗,而後將符合條件的緩存組中的模塊打包成新的 chunk:

// 將體積小於minSize的緩存組(這裏對應chunsInfoItem)從chunksInfoMap中刪除
for (const pair of chunksInfoMap) {
  const info = pair[1];
  if (info.validateSize && !checkMinSize(info.sizes, info.cacheGroup.minSize)) {
    chunksInfoMap.delete(pair[0]);
  }
}

while (chunksInfoMap.size > 0) {
  // 尋找最匹配的cacheGroup分組信息,優先進行分割,優先產生打包結果
  let bestEntryKey;
  let bestEntry;
  for (const pair of chunksInfoMap) {
    const key = pair[0];
    const info = pair[1];
    if (bestEntry === undefined || compareEntries(bestEntry, info) < 0) {
      bestEntry = info;
      bestEntryKey = key;
    }
  }

  const item = bestEntry;
  chunksInfoMap.delete(bestEntryKey);

  let chunkName = item.name;
  // 由緩存組生成的新chunk
  let newChunk;
  let isExistingChunk = false;
  let isReusedWithAllModules = false;
  // 真正的代碼分割從這開始,前面其實都是準備工做
  if (chunkName) {
    const chunkByName = compilation.namedChunks.get(chunkName);
    // 若是在本來的chunks中找到了這樣名字的chunk,就將它提取出來,最終會將全部同名chunk合併在一塊兒
    if (chunkByName !== undefined) {
      newChunk = chunkByName;
      item.chunks.delete(newChunk);
      isExistingChunk = true;
    }
  } else if (item.cacheGroup.reuseExistingChunk) {
    // 若是沒有設定name,則尋找是否能複用已有的chunk
    outer: for (const chunk of item.chunks) {
      if (chunkGraph.getNumberOfChunkModules(chunk) !== item.modules.size) {
        continue;
      }
      if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
        continue;
      }
      for (const module of item.modules) {
        if (!chunkGraph.isModuleInChunk(module, chunk)) {
          continue outer;
        }
      }
      if (!newChunk || !newChunk.name) {
        newChunk = chunk;
      } else if (chunk.name && chunk.name.length < newChunk.name.length) {
        newChunk = chunk;
      } else if (
        chunk.name &&
        chunk.name.length === newChunk.name.length &&
        chunk.name < newChunk.name
      ) {
        newChunk = chunk;
      }
    }
    if (newChunk) {
      item.chunks.delete(newChunk);
      chunkName = undefined;
      isExistingChunk = true;
      isReusedWithAllModules = true;
    }
  }

  // 該緩存組內沒有chunk,則跳過本次循環,又由於以前chunksInfoMap.delete(bestEntryKey)刪除了該緩存組,因此至關於從代碼分割的結果集中去除了沒有chunk的緩存組
  if (item.chunks.size === 0 && !isExistingChunk) continue;

  const usedChunks = Array.from(item.chunks);
  let validChunks = usedChunks;
  // 檢測緩存組中的代碼塊是否知足maxInitialRequests和maxAsyncRequests條件,若是它們都是無窮大,就不必檢測了
  if (
    Number.isFinite(item.cacheGroup.maxInitialRequests) ||
    Number.isFinite(item.cacheGroup.maxAsyncRequests)
  ) {
    validChunks = validChunks.filter((chunk) => {
      // 若是chunk是初始代碼塊,只需判斷maxInitialRequests條件是否知足;
      // 若是chunk不是初始代碼塊,只需判斷maxAsyncRequests條件是否知足;
      // 若是chunk能夠做爲初始代碼塊,就取二者最小值;不過目前這個分支條件是走不到的,由於目前版本代碼塊只有初始(做爲入口)或者非初始(懶加載)
      const maxRequests = chunk.isOnlyInitial()
        ? item.cacheGroup.maxInitialRequests
        : chunk.canBeInitial()
        ? Math.min(
            item.cacheGroup.maxInitialRequests,
            item.cacheGroup.maxAsyncRequests
          )
        : item.cacheGroup.maxAsyncRequests;
      // 若是不知足最大請求數的條件,則從validChunks中去除
      return !isFinite(maxRequests) || getRequests(chunk) < maxRequests;
    });
  }

  // 將那些再也不包含緩存組中模塊的代碼塊刪除
  validChunks = validChunks.filter((chunk) => {
    for (const module of item.modules) {
      if (chunkGraph.isModuleInChunk(module, chunk)) return true;
    }
    return false;
  });

  // 將去除不符合條件的chunk以後的新緩存組加入chunksInfoMap,不斷迭代,更新代碼分割結果
  if (validChunks.length < usedChunks.length) {
    if (isExistingChunk) validChunks.push(newChunk);
    if (validChunks.length >= item.cacheGroup.minChunks) {
      for (const module of item.modules) {
        addModuleToChunksInfoMap(
          item.cacheGroup,
          validChunks,
          getKey(validChunks),
          module
        );
      }
    }
    continue;
  }

  // Webpack5新特性minRemainingSize,保證chunk被分割後的剩餘體積不小於該值,防止出現特別小的單個代碼塊
  if (
    validChunks.length === 1 &&
    hasNonZeroSizes(item.cacheGroup.minRemainingSize)
  ) {
    const chunk = validChunks[0];
    const chunkSizes = { ...chunkGraph.getChunkModulesSizes(chunk) };
    for (const key of Object.keys(item.sizes)) {
      chunkSizes[key] -= item.sizes[key];
    }
    if (!checkMinSize(chunkSizes, item.cacheGroup.minRemainingSize)) {
      continue;
    }
  }

  // 建立新的代碼塊,加入咱們編譯器的chunkGraph,這個新的代碼塊就是分割出來的公共代碼
  if (!isExistingChunk) {
    newChunk = compilation.addChunk(chunkName);
  }
  // 建立了新代碼塊還不夠,還須要創建chunk和chunkGroup之間的關係
  for (const chunk of usedChunks) {
    // Add graph connections for splitted chunk
    chunk.split(newChunk);
  }

  // 提供輸出信息:分割出來的新chunk是不是複用的
  newChunk.chunkReason =
    (newChunk.chunkReason ? newChunk.chunkReason + ", " : "") +
    (isReusedWithAllModules ? "reused as split chunk" : "split chunk");
  // 提供輸出信息:分割出來的新chunk中會備註所屬cacheGroup的信息,最終打包輸出時會附加這些信息,便於咱們debug
  if (item.cacheGroup.key) {
    newChunk.chunkReason += ` (cache group: ${item.cacheGroup.key})`;
  }
  if (!isReusedWithAllModules) {
    // 將緩存組中的全部模塊都加入新生成的chunk中,就是把緩存組打包成新的代碼塊
    for (const module of item.modules) {
      // chunkCondition方法現版本永遠返回true
      if (!module.chunkCondition(newChunk, compilation)) continue;
      chunkGraph.connectChunkAndModule(newChunk, module);
      // 從緩存組的chunks中刪除那些已經被提取出來的模塊,達到優化體積的目的
      for (const chunk of usedChunks) {
        chunkGraph.disconnectChunkAndModule(chunk, module);
      }
    }
  } else {
    // 若是緩存組中全部module都被複用了,則從usedChunks中將這些module所有刪除,避免冗餘
    for (const module of item.modules) {
      for (const chunk of usedChunks) {
        chunkGraph.disconnectChunkAndModule(chunk, module);
      }
    }
  }

  // 從其餘緩存組中刪除已經被提取出來的模塊,避免產生重複代碼
  for (const [key, info] of chunksInfoMap) {
    if (isOverlap(info.chunks, item.chunks)) {
      if (info.validateSize) {
        let updated = false;
        for (const module of item.modules) {
          if (info.modules.has(module)) {
            // remove module
            // 刪除模塊
            info.modules.delete(module);
            // 更新緩存組體積
            for (const key of module.getSourceTypes()) {
              info.sizes[key] -= module.size(key);
            }
            updated = true;
          }
        }
        // 刪除重複模塊後,要從新判斷緩存組體積,若是小於minSize則刪除該緩存組
        if (updated) {
          if (info.modules.size === 0) {
            chunksInfoMap.delete(key);
            continue;
          }
          if (!checkMinSize(info.sizes, info.cacheGroup.minSize)) {
            chunksInfoMap.delete(key);
          }
        }
      } else {
        for (const module of item.modules) {
          info.modules.delete(module);
        }
        if (info.modules.size === 0) {
          chunksInfoMap.delete(key);
        }
      }
    }
  }
}

// 最後還有一段對maxSize的校驗,很長,但原理和步驟與以前大同小異,這裏省略,有興趣能夠克隆個人github源碼倉庫細看
複製代碼

通過本階段的篩選,chunksInfoMap 中符合配置規則的緩存組會被所有打包成新代碼塊,而且加入 compilation 的 chunkGraph 中,完成代碼分割的工做,最終生成打包文件。不要懼怕大量if,else分支,其實都只是循序漸進檢查各種配置是否知足,排除一些特殊特殊狀況。

此外,有些方法如 module 的 chunkCondition 現版本永遠返回 true,應該是預留一個可擴展的分支邏輯,之後版本可能會有更多優化狀況。

劃重點

SplitChunksPlugin 的核心在於將每一個模塊(module)按照規則分配到各個緩存組中,造成一個緩存組的 map 結構 chunksInfoMap,每一個緩存組會對應最終分割出來的新代碼塊。咱們對 splitChunks 中的 cacheGroups 進行配置,其實就是控制 chunksInfoMap 中的每一個緩存組。

回顧整個過程,其實沒有複雜的算法邏輯,就是在合適的時候遍歷判斷各個條件是否知足,可是卻能將一個龐大項目的複雜包結構分割成可預測的結果。實用的工具背後的邏輯每每很簡單明確,咱們開發項目也同樣,不須要過分設計,先用最直接的邏輯完成最須要作的事,也許纔是當下最好的解決方案。若是真的須要一些複雜的設計,也應該儘可能把複雜度聚合在數據結構中,採用聲明式而非命令式的方式解決問題。

下一期,一塊兒走進 webpack 的核心數據結構 chunkGraph,看看 webpack 是怎麼把文件一步步整理成包結構輸出的。

源碼

個人 webpack 註解

其餘乾貨

Webpack 系列第一篇:

面經加答案:

CSS 細節:

寫給找不到方向的同窗

相關文章
相關標籤/搜索