Погрузитесь в мир js, упакованного webpack

Webpack

предисловие

В разработке современных популярных интерфейсных проектов почти всегда можно найти тень веб-пакета, и, похоже, он стал неотъемлемой частью сегодняшней фронтенд-разработки.

Следующая картинка — домашняя страница официального веб-сайта webpack, наглядно демонстрирующая основную функцию webpack: упаковка множества модулей со сложными зависимостями в аккуратные и упорядоченные статические ресурсы.

webpack

Появление WebPack и поддержка готовых лесов позволяют нам сосредоточиться на разработке проекта, не уделяя слишком много внимания к процессу упаковки и результаты.

Однако, если вам интересно, как упаковывается код JS после WebPack?

Напомним, что в нашем проекте с использованием webpack любой ресурс можно рассматривать как модуль (лишь бы есть соответствующий загрузчик, поддерживающий парсинг), а носителем модуля в это время является файл. Но после упаковки проекта носителем модуля становится функция, также известная как функция модуля, а файл становится носителем чанка. Так называемый чанк — это понятие в webpack, представляющее собой набор из нескольких модулей, а чанк часто соответствует файлу.

На самом деле, чтобы ответить на поставленные выше вопросы, необходимо разобраться в отношениях между модулями и модулями, чанками и чанками, а также модулями и чанками после упаковки. Итак, давайте начнем эту статью с двух измерений модуля и чанка.

модуль

Поскольку модуль — это наименьшая единица загрузки ресурсов, мы начнем с самой простой загрузки модуля.

Ниже приведен базовый файл конфигурации webpack4.

// webpack.config.js
const path = require('path')

module.exports = {
  mode: 'production',
  entry: {
    main: './src/main.js'
  },
  output: {
    filename: '[name].js',
    chunkFilename: '[name].[contenthash:8].js',
    path: path.resolve(__dirname, 'dist')
  },
  optimization: {
    // 为了方便阅读理解打包后的代码,关闭代码压缩和模块合并
    minimize: false,
    concatenateModules: false
  }
}

при выполнении команды сборкиnpm run buildПосле этого в проекте будет сгенерирована папка dist, в которой находится файл main.js, а далее — содержимое файла. (Все примеры кодов в этой статье игнорируют некоторые временно нерелевантные коды, чтобы сосредоточиться на ключевых моментах, чтобы не увеличивать нагрузку на читателя)

(function(modules) { // webpackBootstrap
    // 用于缓存已加载模块的地方
    var installedModules = {};

    // 用于加载模块的 require 函数
    function __webpack_require__(moduleId) { ... }

    // 加载入口模块(假设入口模块 id 为0)
    return __webpack_require__(0)
})([...])

Суть приведенного выше кода — выражение немедленно вызываемой функции (IIFE), функциональная часть которого называется webpackBootstrap, а входящий аргумент — массив или объект, содержащий функции модуля.

webpackBootstrap

Ниже приводится английское значение bootstrap:

A technique of loading a program into a computer by means of a few initial instructions which enable the introduction of the rest of the program from an input device.

Перевод — это метод загрузки программы в компьютер с некоторыми начальными инструкциями, которые позволяют вводить остальную часть программы с устройства ввода.

Если вы все еще не понимаете этого, вы можете просто понять его как центр управления, который отвечает за запуск, планирование и выполнение всего.

В WebPackBootstrap объект кэша модуля определен для хранения загруженного модуля и функции загрузки модуля (__webpack_require__) используется для получения модуля, соответствующего идентификатору, и, наконец, модуль ввода загружается для запуска всей программы.

Теперь сосредоточимся на загрузке модулей.

// 加载模块
function __webpack_require__(moduleId) {
  // 检查缓存中是否有该模块,若有,则直接返回
  if (installedModules[moduleId]) {
    return installedModules[moduleId].exports
  }

  // 初始化一个新模块,并且保存到缓存中
  var module = installedModules[moduleId] = {
    i: moduleId, // 模块名
    l: false, // 布尔值,表示该模块是否加载完毕
    exports: {} // 模块的输出对象,包含了模块输出的各个接口
  }

  // 执行模块函数,并传入三个实参:模块本身、模块的输出对象、加载函数,同时定义 this 值为模块的输出对象
  modules[moduleId].call(
    module.exports,
    module,
    module.exports,
    __webpack_require__
  )

  // 标记模块为已加载状态
  module.l = true

  // 返回模块的输出对象
  return module.exports
}

Приведенный выше код показывает, что загрузка скомпилированных модулей соответствует спецификации CommonJS. Однако спецификация модуля CommonJS не синхронно загружает модули, разве это не работает на стороне браузера? На самом деле это связано с тем, что скомпилированный код веб-пакета гарантирует, что при загрузке модуля модуль уже загружен с сервера, поэтому проблема блокировки, вызванная синхронными запросами, отсутствует. Что касается того, как эта проблема решается, это будет объяснено в разделе фрагментов ниже.

Ниже представлена ​​блок-схема загрузки модуля.

模块加载流程

Однако здесь стоит отметить несколько моментов:

  • Если один и тот же модуль загружается несколько раз, он будет выполнен только один раз, поэтому загруженные модули необходимо кэшировать.
  • Новый модуль сохраняется в кэше сразу после его инициализации, а не после загрузки модуля. На самом деле это нужно для решения проблемы циклической зависимости между модулями, то есть модуль a зависит от модуля b, а модуль b зависит от модуля a. Таким образом, когда невыполненный модуль загружается снова, объект вывода модуля будет возвращен непосредственно при проверке кеша (объект вывода может не содержать все интерфейсы вывода модуля), чтобы избежать бесконечных циклов.
  • В модулях CommonJS значение this на верхнем уровне равноmodule.exports, поэтому используйте функцию вызова, чтобы определить значение this функции модуляmodule.exports. Но в модулях ES6 верхний уровень this не определен, поэтому он преобразуется в undefined во время компиляции.

функция модуля

Так что же именно он делает, когда выполняется функция модуля? Проще говоря, это добавить интерфейс вывода загруженного модуля к объекту вывода.

Давайте рассмотрим функцию модуля на простом примере (поскольку веб-пакет официально рекомендует использовать синтаксис модуля ES6, поэтому в примере используется импорт/экспорт в ES6).

// src/lib.js
export let counter = 0

export function plusOne() {
  counter++
}

// src/main.js(入口模块)
import { counter, plusOne } from './lib'

console.log(counter)

plusOne()
console.log(counter)

Ниже приведена функция модуля, упакованная и скомпилированная webpack.

(function(modules) {
  ...
  // 步骤1:加载入口模块
  return __webpack_require__(1)
})(
  [
    /* moduleId: 0 */
    function(module, __webpack_exports__, __webpack_require__) {
      // ES6模块默认采用严格模式
      'use strict'

      // 步骤1.1.1:在输出对象上定义输出的各个接口
      __webpack_require__.d(__webpack_exports__, 'a', function() {
        return counter
      })
      __webpack_require__.d(__webpack_exports__, 'b', function() {
        return plusOne
      })

      // 步骤1.1.2:声明定义输出接口的值
      let counter = 0

      function plusOne() {
        counter++
      }
    },
    /* moduleId: 1 */
    function(module, __webpack_exports__, __webpack_require__) {
      'use strict'
      // 步骤1.1:加载lib.js模块,并返回其输出对象
      var _lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0)
      // _lib__WEBPACK_IMPORTED_MODULE_0__ = {
      //   get a() { return couter },
      //   get b() { return pluseOne }
      // }

      // 步骤1.2:调用输出对象上的输出接口
      console.log(_lib__WEBPACK_IMPORTED_MODULE_0__[/* counter */ 'a'])

      Object(_lib__WEBPACK_IMPORTED_MODULE_0__[/* plusOne */ 'b'])()
      console.log(_lib__WEBPACK_IMPORTED_MODULE_0__[/* counter */ 'a'])
    }
  ]
)

В приведенном выше коде файл модуля ES6 компилируется в функцию модуля спецификации CommonJS. Чтобы сохранить характеристики синтаксиса модуля ES6, скомпилированный код становится несколько неясным. Некоторые из них сбивают с толку:

  1. __webpack_require__.dДля чего используются функции?

    Эта функция используется для определения различных интерфейсов вывода на объекте вывода. Но разве простое присвоение свойств объекта не может решить эту задачу? Это потому чтоМодули ES6 выводят ссылки только для чтения на значения.

    Ниже__webpack_require__.dреализация.

    __webpack_require__.d = function(exports, name, getter) {
      // __webpack_require__.o 是用于判断输出对象上是否已存在同名的输出接口
      if (!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, { enumerable: true, get: getter })
      }
    }
    

    Приведенный выше код показывает, что при экспорте интерфейса модуля ES6 он будет использоватьObject.definePropertyдля определения свойств выходного объекта и определения только получателя (функции получения) свойства, чтобы выходной интерфейс был доступен только для чтения. Затем геттер свойства взаимодействует с замыканием, чтобы реализовать ссылку на выходной интерфейс как значение.

  2. Почему унифицированный определяет выходной интерфейс в верхней части функции модуля (кромеexport defaultЗа исключением некоторых специальных сцен, таких какexport default 1В этом случае имя выходного интерфейса явно не указано)?

    Это потому чтоМодули ES6 представляют собой выходные интерфейсы времени компиляции., модули CommonJS, напротив, загружаются во время выполнения. Разница между ними отражена в проблеме круговой загрузки между модулями. Следовательно, чтобы имитировать эту функцию модулей ES6, необходимо определить имя выходного интерфейса до того, как модуль загрузит зависимые модули или выполнит другие операции.

  3. Почему необходимо извлекать из выходного объекта каждый раз при использовании выходного интерфейса модуля lib (например, при использовании значения couter на шаге 1.2), вместо того, чтобы выходной интерфейс был независимым в исходном коде (например, переменная couter в исходном коде)?

    Это связано с тем, что атрибут на выходном объекте на самом деле является функцией-получателем.Если значение атрибута вынесено, а переменная объявлена ​​отдельно, эффект закрытия будет потерян, и изменение значения выходного интерфейса в загруженном модуле невозможно отследить, и вывод будет потерян.Интерфейсы — это ссылки на значения, особенность модулей ES6. Взяв приведенный выше пример кода в качестве примера, обычно консоль выводит 0 и 1 по очереди, но если скомпилированное значение выходного интерфейса присваивается новой переменной, консоль выводит 0 дважды.

кусок

Если размер кода проекта становится больше, то упаковка всего js-кода в один файл неизбежно столкнется с узкими местами в производительности, что приведет к увеличению времени загрузки ресурсов. В это время пригодится технология разделения фрагментов webpack. Модули можно разделить на разные файлы фрагментов в соответствии с различными стратегиями оптимизации подпакетов.

Асинхронный чанк (асинхронный чанк)

Для некоторых менее часто используемых маршрутов или менее часто используемых компонентов они могут быть разделены на асинхронные фрагменты с помощью отложенной загрузки.

Доступ к асинхронным фрагментам можно получить, вызвавimport()Метод динамически загружает модуль для получения. Давайте изменим файл main.js, чтобы лениво загружать модуль lib.

// src/main.js
import('./lib').then((lib) => {
  console.log(lib.counter)

  lib.plusOne()
  console.log(lib.counter)
})

Ниже приведен перестроенный и упакованный файл main.js (показан только новый и измененный код).

// dist/main.js
(function(modules) {
  // chunk 下载完毕后执行的函数
  function webpackJsonpCallback(data) { ... }

  // 用于标记各个 chunk 加载状态的对象
  // undefined:chunk 未加载
  // null:chunk preloaded/prefetched
  // Promise:chunk 正在加载
  // 0:chunk 已加载
  var installedChunks = {
    0: 0
  }

  // 获取 chunk 的请求地址(url),包含了 chunk 名及 chunk 哈希
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"1":"3215c03a"}[chunkId] + ".js"
  }

  // 获取 chunk
  __webpack_require__.e = function requireEnsure(chunkId) { ... }

  // chunk 的公共路径(public path),即 webpack 配置中的 output.publicPath
  __webpack_require__.p = "";

  // 围绕 webpackJsonp 的一系列操作,会在下面做详细介绍
  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  var parentJsonpFunction = oldJsonpFunction;

  return __webpack_require__(0);
})([
  /* 0 */
  (function(module, exports, __webpack_require__) {
    __webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then((lib) => {
      console.log(lib.counter)

      lib.plusOne()
      console.log(lib.counter)
    })
  })
])

__webpack_require__.e

В приведенном выше коде в модуле вводаimport('./lib')компилируется как__webpack_require__.e(1).then(__webpack_require__.bind(null, 1)), который фактически эквивалентен следующему коду.

__webpack_require__.e(1)
  .then(function() {
    return __webpack_require__(1)
  })

Приведенный выше код состоит из двух частей, первой половины__webpack_require__.e(1)Он используется для асинхронной загрузки фрагментов, а функция обратного вызова, переданная в метод then во второй половине, используется для синхронной загрузки модуля lib.

Это рассматривается в спецификации CommonJS.Загружать модули синхронноЭто вызовет проблемы с блокировкой выполнения на стороне браузера.

// 获取 chunk
__webpack_require__.e = function requireEnsure(chunkId) {
  var promises = [];

  // 利用 JSONP 下载 js chunk
  var installedChunkData = installedChunks[chunkId];
  // 0代表该 chunk 已加载完毕
  if(installedChunkData !== 0) {

    if(installedChunkData) { // chunk 正在加载中
      promises.push(installedChunkData[2]);
    } else {
      // 在 chunk 缓存中更新 chunk 状态为正在加载中,并缓存 resolve、reject、promise
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      promises.push(installedChunkData[2] = promise);

      // 开始准备下载 chunk
      var script = document.createElement('script');
      var onScriptComplete;

      script.charset = 'utf-8';
      script.timeout = 120;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      script.src = jsonpScriptSrc(chunkId);

      // 在堆栈展开之前创建 error 以便稍后获得有用的堆栈跟踪
      var error = new Error();
      // chunk 下载完成后(成功或异常)的回调函数
      onScriptComplete = function (event) {
        // 防止 IE 浏览器下内存泄漏
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if(chunk !== 0) {
          if(chunk) {
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
            error.name = 'ChunkLoadError';
            error.type = errorType;
            error.request = realSrc;
            // reject(error)
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined;
        }
      };
      // 处理请求超时
      var timeout = setTimeout(function(){
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      // 处理请求成功及异常
      script.onerror = script.onload = onScriptComplete;
      // 发起请求
      document.head.appendChild(script);
    }
  }
  return Promise.all(promises);
};

Ниже__webpack_require__.eПроцесс выполнения:

  1. Инициализируйте переменную обещания для обработки процесса асинхронного загрузки каждого хлопама (хотя только один кусок JS должен быть обработан в приведенном выше примере, некоторые модули JS зависят от файлов CSS, поэтому при нагрузке CSS js также будет зависеть Загрузите, нужно обрабатывать асинхронную загрузку нескольких чанков, поэтому эта переменная является массивом).
  2. Определите, был ли загружен фрагмент js, если фрагмент был загружен, перейдите сразу к шагу 6, в противном случае продолжите выполнение.
  3. Определить, загружается ли чанк, если да, добавить экземпляр промиса в массив, сохраненный чанком в кэше чанков (installedChunks), в массив обещаний, а затем перейти к шагу 6, в противном случае продолжить выполнение.
  4. Инициализирует экземпляр промиса для обработки процесса асинхронной загрузки чанка и будет состоять из массива функций разрешения и отклонения промиса и самого экземпляра промиса (т.[resolve, reject, promise]) в кеш чанков, затем добавьте экземпляр промиса в массив промисов.
  5. Начните подготовку к загрузке фрагментов, включая создание тегов скрипта, настройку атрибутов скрипта, таких как src, тайм-аут и т. д., обработку событий успеха, сбоя и тайм-аута запроса скрипта и, наконец, добавление скрипта в документ для завершения отправки запроса.
  6. выполнить и вернутьPromise.all(promises), и после того, как все асинхронные фрагменты будут успешно загружены, вызовите функцию обратного вызова в методе then (то есть загрузите модули, содержащиеся в фрагменте).

Некоторых может смутить, почему не в onScriptCompletechunk === 0Когда выполняется функция разрешения? Как следующее:

onScriptComplete = function (event) {
  ...
  var chunk = installedChunks[chunkId];
  if(chunk !== 0) {
    if(chunk) {
      ...
      // reject(error)
      chunk[1](error);
    }
    installedChunks[chunkId] = undefined;
  } else { // chunk === 0
    // resolve()
    chunk[0]()
  }
};

Суть этого вопроса в том, когда заканчивается процесс асинхронной загрузки чанков? Заканчивается ли он, когда загрузка чанка завершена? На самом деле загрузка чанка js состоит из двух частей: скачивание файла чанка и выполнение кода чанка, и чанк не загружается до тех пор, пока оба не будут завершены. Таким образом, разрешение сохраняется в кэше фрагментов, а функция разрешения выполняется после выполнения кода фрагмента, завершая процесс асинхронной загрузки. Хотя событие загрузки скрипта запускается после его загрузки и выполнения, событие загрузки заботится только о самой загрузке.Даже если скрипт выдает исключение во время выполнения, событие загрузки все равно будет запущено.

webpackJsonpCallback

Когда фрагмент js будет успешно загружен, он начнет выполнять код.Ниже приведен фрагмент, упакованный модулем lib.js.

// dist/1.3215c03a.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
  /* 0 */,
  /* 1 */
  (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    __webpack_require__.d(__webpack_exports__, "counter", function() { return counter; });
    __webpack_require__.d(__webpack_exports__, "plusOne", function() { return plusOne; });
    let counter = 0

    function plusOne() {
      counter++
    }

  })
]]);

Приведенный выше код выглядит очень просто, всего два шага, один — инициализация.window["webpackJsonp"]является массивом (если он не был инициализирован ранее), другой — добавить массив вwindow["webpackJsonp"]В массиве (выражение не является строгим, подробности см. ниже). Массив, используемый в качестве фактического параметра, состоит из двух массивов, первый массив представляет собой набор chunkId (при нормальных обстоятельствах массив содержит только текущий chunkId. Однако в случае неправильной стратегии субподряда массив может содержать несколько chunkId), второй массив представляет собой набор функций модуля.

Однако собственная операция PUSH может только просто добавлять данные из блока CHUNK в массив. Что такое обработка Webpack? Как это делается?

Если вы все еще впечатлены, выше есть абзац, посвященный webpackBootstrap.window["webpackJsonp"]Операции над массивами.

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 将 window["webpackJsonp"].push 方法替换为 webpackJsonpCallback 函数
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
// 对之前已加载的全部初始 chunk 中的数据调用 webpackJsonpCallback
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// 将 window["webpackJsonp"] 数组原生的 push 方法赋给 parentJsonpFunction 变量
var parentJsonpFunction = oldJsonpFunction;

Как видно из вышеприведенного кода, все данные в чанках (кроме чанка, где находится webpackBootstrap) загружаются и обрабатываются в виде JSONP (то есть вызовом webpackJsonpCallback), но до чанка, где находится webpackBootstrap, загружен. , webpackJsonpCallback не объявлен и не определен, поэтому данные временно сохраняются вwindow["webpackJsonp"]В массиве, после его загрузки, сначала ставитсяwindow["webpackJsonp"]Метод push массива заменяется функцией webpackJsonpCallback (таким образом загруженные позже чанки вызывают метод push, но на самом деле вызывают непосредственно функцию webpackJsonpCallback для обработки данных), а затем сохраняют предыдущие чанки вwindow["webpackJsonp"]Данные в массиве по очереди вызывают webpackJsonpCallback.

function webpackJsonpCallback(data) {
  var chunkIds = data[0];
  var moreModules = data[1];

  var moduleId, chunkId, i = 0, resolves = [];
  // 取出各个异步 chunk 所对应的 promise 的 resolve 函数,并在 chunk 缓存中标记 chunk 状态为已加载
  for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0;
  }
  // 将 chunk 中包含的模块都添加到 webpackBootstrap 的 modules 对象中
  for(moduleId in moreModules) {
    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  // 利用 window["webpackJsonp"] 数组原生的 push 方法将 chunk 中的数据添加到 window["webpackJsonp"] 中
  if(parentJsonpFunction) parentJsonpFunction(data);

  // 异步 chunks 加载成功,执行 resolve 函数来 fulfill 各个 chunk 对应的 promise,触发 then 中的回调函数
  while(resolves.length) {
    resolves.shift()();
  }
};

Функция webpackJsonpCallback в основном выполняет две обработки данных в чанке: кэширование и завершение процесса асинхронной загрузки.

Кэш состоит из двух уровней: один — это кеш состояния загрузки чанка, чтобы избежать отправки нескольких запросов для одного и того же чанка, а другой — кеш функции модуля для последующей загрузки модуля.

Завершить процесс асинхронной загрузки чанка на самом деле означает выполнить функцию разрешения в кеше чанков.

Начальный чанк (начальный чанк)

Для модулей, которые необходимо загрузить на начальном этапе веб-сайта, их можно разделить на несколько начальных блоков, таких как основная библиотека базовых классов, библиотека компонентов пользовательского интерфейса и бизнес-код, в зависимости от размера, скорости обмена и частоты обновления. модуля.

Чтобы получить несколько начальных кусков, настроить файл main.js и конфигурацию webpack.config.js.

// src/main.js
import * as _ from 'lodash'

const arr = [1, 2]
console.log(_.concat(arr, 3, [4]))

// webpack.config.js(基于上面的 webpack 配置)
module.exports = {
  ...
  optimization: {
    ...
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
      }
    }
  }
}

В приведенном выше коде модуль Main.js зависит от библиотеки Lodash и разбивает библиотеку Lodash в отдельный кусок, поэтому конфигурация Spreithunks добавляется в объект оптимизации конфигурации WebPack, чтобы разделить библиотеку Lodash на поставщиков по именованным поставщикам кусок.

Ниже приведен код, добавленный в webpackBootstrap после упаковки.

(function(modules) { // webpackBootstrap
  function webpackJsonpCallback(data) {
    ...
    var executeModules = data[2];
    // 如果加载的 chunk 中有入口模块,则将其添加到 deferredModules 数组
    deferredModules.push.apply(deferredModules, executeModules || []);

    return checkDeferredModules();
  }

  // 检查入口模块所依赖的 chunk 是否加载完成,如果是,则加载入口模块,否则不执行任何操作
  function checkDeferredModules() {
    var result;
    // 遍历所有入口模块
    for(var i = 0; i < deferredModules.length; i++) {
      var deferredModule = deferredModules[i];
      var fulfilled = true;
      // 检查入口模块所依赖的全部 chunk 是否加载完成
      for(var j = 1; j < deferredModule.length; j++) {
        var depId = deferredModule[j];
        if(installedChunks[depId] !== 0) fulfilled = false;
      }
      // 如果入口模块依赖的全部 chunk 都加载完成,则加载入口模块
      if(fulfilled) {
        deferredModules.splice(i--, 1);
        result = __webpack_require__(deferredModule[0]);
      }
    }
    return result;
  }

  var deferredModules = [];

  // 将入口模块添加到 deferredModules 数组
  // 数组中第一个元素为入口模块 id,后面的元素都是入口模块依赖的初始 chunk 的 id
  deferredModules.push([1,1]);

  return checkDeferredModules();
})(...)

Получается, что при наличии только одного начального чанка в чанке содержатся все модули, необходимые для начального этапа, поэтому при его загрузке модуль входа может быть загружен напрямую. Однако, когда модуль разбит на несколько начальных частей, начальный модуль может быть загружен только после загрузки всех начальных частей и готовности всех модулей, необходимых для начального этапа. Таким образом, единственное отличие состоит в том, что время загрузки входного модуляотсрочить (отсрочить).

Таким образом, в приведенном выше коде и функция webpackBootstrap, и функция webpackJsonpCallback вызывают функцию checkDeferredModules в конце, чтобы гарантировать, что все фрагменты будут проверять, соответствуют ли какие-либо входные модули требованиям после их загрузки (то есть все начальные фрагменты, от которых они зависят). были загружены), если модуль ввода удовлетворен, начните загрузку модуля ввода.

резюме

Эта статья на самом деле отвечает на вопрос: как работает js-код, упакованный webpack? В основе ответа два момента: загрузка модулей и загрузка чанков. Первый является синхронно блокирующим, а второй — асинхронным и неблокирующим. Когда вы знаете, как гармонично сочетать их вместе, вы недалеко от полного ответа.