[Расширенный Webpack] Какой код упакован с помощью Webpack?

внешний интерфейс Webpack
[Расширенный Webpack] Какой код упакован с помощью Webpack?

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

ТакwebpackКак выглядит упакованный код? как каждыйbundleсвязаны вместе? Как обрабатываются отношения между модулями и модулями? динамичныйimport()А когда?

Эта статья позволит нам шаг за шагом раскрытьwebpackТайна упакованного кода

Готов к работе

Создайте файл и инициализируйте

mkdir learn-webpack-output
cd learn-webpack-output
npm init -y 
yarn add webpack webpack-cli -D

Создайте новый файл в корневом каталогеwebpack.config.js,Этоwebpackфайл конфигурации по умолчанию

const path = require('path');

module.exports = {
  mode: 'development', // 可以设置为 production
  // 执行的入口文件
  entry: './src/index.js',
  output: {
    // 输出的文件名
    filename: 'bundle.js',
    // 输出文件都放在 dist 
    path: path.resolve(__dirname, './dist')
  },
  // 为了更加方便查看输出
  devtool: 'cheap-source-map'
}

затем мы возвращаемся кpackage.jsonфайл, вnpm scriptдобавить автозапускwebpackнастроить команду

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "build": "webpack"
}

создать новыйsrcпапка, новаяindex.jsдокументы иsayHelloдокумент

// src/index.js
import sayHello from './sayHello';

console.log(sayHello, sayHello('Gopal'));
// src/sayHello.js
function sayHello(name) {
  return `Hello ${name}`;
}

export default sayHello;

Все готово, выполняйтеyarn build

Основной процесс анализа

Посмотрите на выходной файл, здесь нет конкретного кода, занимает немного места, можно нажатьПосмотреть здесь

По сути, это ИИФЭ.

Не паникуйте, давайте немного разберем его, на самом деле, общий файл - это одинIIFE- Выполнить функцию немедленно.

(function(modules) { // webpackBootstrap
	// The module cache
	var installedModules = {};
	function __webpack_require__(moduleId) {
    // ...省略细节
	}
	// 入口文件
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({

 "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}),
  "./src/sayHello.js": (function(module, __webpack_exports__, __webpack_require__) {})
});

ввод функцииmodulesэто объект, объектkeyкаждыйjsотносительный путь к модулю,valueесть функция (назовем еефункция модуля).IIFEбудет первымrequireвходной модуль. то есть выше./src/index.js:

// 入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");

Тогда входной модуль будет выполнен, когдаrequireдругие модули, такие как./src/sayHello.js" Ниже приведен упрощенный код для непрерывной загрузки зависимых модулей для формирования дерева зависимостей, например следующегофункция модуляссылки на другие документыsayHello.js

{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) { 
    __webpack_require__.r(__webpack_exports__);
	  var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
    console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
    Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
  })
}

Важные механизмы реализации——__webpack_require__

иди сюдаrequireФункции других модулей в основном__webpack_require__. Далее мы в основном представимся__webpack_require__эта функция

  // 缓存模块使用
  var installedModules = {};
  // The require function
  // 模拟模块的加载,webpack 实现的 require
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    // 检查模块是否在缓存中,有则直接从缓存中获取
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    // 没有则创建并放入缓存中,其中 key 值就是模块 Id,也就是上面所说的文件路径
    var module = installedModules[moduleId] = {
      i: moduleId, // Module ID
      l: false, // 是否已经执行
      exports: {}
    };

    // Execute the module function
    // 执行模块函数,挂载到 module.exports 上。this 指向 module.exports
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    // 标记这个 module 已经被加载
    module.l = true;

    // Return the exports of the module
    // module.exports通过在执行module的时候,作为参数存进去,然后会保存module中暴露给外界的接口,如函数、变量等
    return module.exports;
  }

первый шаг,webpackЗдесь выполняется слой оптимизации через объектinstalledModulesКеш, проверяем есть ли модуль в кеше, если есть, получаем прямо из кеша, если нет то создаем и кладем в кеш, гдеkeyзначение - модульId, который является путем к файлу, упомянутому выше

Второй шаг, затем выполнитефункция модуля,Будуmodule, module.exports, __webpack_require__Передайте его как параметр и укажите объект вызова функции модуля наmodule.exports, чтобы убедиться, чтоthisУказатель всегда указывает на текущий модуль.

На третьем шаге загруженный модуль, наконец, возвращается, и вызывающая сторона может напрямую вызвать его.

такэто__webpack_require__это загрузить модуль и вернуть модуль в концеmodule.exportsПеременная

Как webpack поддерживает ESM

Как вы, возможно, заметили, то, что я написал выше,ESMСпособ написания, для понимания некоторых модульных решений можете посмотреть другую мою статью[Интервью] Что такое CJS, AMD, UMD и ESM в Javascript?

мы оглядываемся назадфункция модуля

{
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) { 
    __webpack_require__.r(__webpack_exports__);
	  var _sayHello__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sayHello.js");
    console.log(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"],
    Object(_sayHello__WEBPACK_IMPORTED_MODULE_0__["default"])('Gopal'));
  })
}

Давайте посмотрим__webpack_require__.rфункция

__webpack_require__.r = function(exports) {
 object.defineProperty(exports, '__esModule', { value: true });
};

Просто для__webpack_exports__добавить свойство__esModule, значениеtrue

увидеть другой__webpack_require__.nреализация

// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
  var getter = module && module.__esModule ?
    function getDefault() { return module['default']; } :
    function getModuleExports() { return module; };
  __webpack_require__.d(getter, 'a', getter);
  return getter;
};

__webpack_require__.nБудет судить, является ли модуль модулем es, когда__esModuleПри значении true модуль идентифицируется как модуль es и возвращается по умолчанию.module.default, иначе возвратmodule.

последний взгляд__webpack_require__.d, основная работа заключается в преобразовании вышеуказанногоgetterФункция привязана к свойству a в экспортеgetterначальство

// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
	if(!__webpack_require__.o(exports, name)) {
		Object.defineProperty(exports, name, {
			configurable: false,
			enumerable: true,
			get: getter
		});
	}
};

увидимся наконецsayHello.jsупакованныйфункция модуля, вы можете видеть, что экспорт здесь __webpack_exports__["default"], что на самом деле__webpack_require__.nЭто реализуется слоем упаковки.В самом деле видно, что на самом деле,webpackда может поддержатьCommonJSа такжеES Moduleсмешанные вместе

 "./src/sayHello.js":
  /*! exports provided: default */
 (function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  function sayHello(name) {
    return `Hello ${name}`;
  }
  /* harmony default export */ __webpack_exports__["default"] = (sayHello);
 })

Пока что мы знаем примерноwebpackКак работает упакованный файл Далее разберем специальный сценарий разделения кода — динамический импорт

динамический импорт

Разделение кодаwebpackОдна из самых ярких особенностей в . Эта функция позволяет разделить код на разныеbundle, который затем может быть загружен по запросу или параллельно. Разделение кода может быть использовано для получения меньшихbundle, и управлять приоритетом загрузки ресурсов, при разумном использовании это сильно повлияет на время загрузки.

Общие методы разделения кода следующие:

  • входная точка:использоватьentryКонфигурация вручную разделяет код.
  • предотвратить повторение:использоватьEntry dependenciesилиSplitChunksPluginДедупликация и разделение чанков.
  • динамический импорт: Разделение кода путем встраивания вызовов функций модулей.

В этой статье мы в основном рассматриваем динамический импорт, мы вsrcСоздайте новый файл нижеanother.js

function Another() {
  return 'Hi, I am Another Module';
}

export { Another };

Исправлятьindex.js

import sayHello from './sayHello';

console.log(sayHello, sayHello('Gopal'));

// 单纯为了演示,就是有条件的时候才去动态加载
if (true) {
  import('./Another.js').then(res => console.log(res))
}

Смотрим содержимое пакета наружу, не обращая внимания на .map файл, можно увидеть еще один0.bundle.jsфайл, это мы называем динамически загружаемымchunk,bundle.jsмы зовем лордаchunk

Если выходной код, основнойchunkСмотретьздесь, динамически загружаетсяchunkСмотретьздесь, ниже приводится анализ двух кодов

Анализ основного фрагмента

Посмотрим на Господаchunk

Контента намного больше, давайте рассмотрим его поближе:

Прежде всего, мы заметили, что место, куда мы динамически импортировали, стало после компиляции следующим, что выглядит как асинхронно загружаемая функция

if (true) {
  __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./Another.js */ "./src/Another.js")).then(res => console.log(res))
}

Итак, давайте посмотрим__webpack_require__.eРеализация этой функции

__webpack_require__.e- Динамическая загрузка с использованием JSONP

// 已加载的chunk缓存
var installedChunks = {
  "main": 0
};
// ...
__webpack_require__.e = function requireEnsure(chunkId) {
  // promises 队列,等待多个异步 chunk 都加载完成才执行回调
  var promises = [];

  // JSONP chunk loading for javascript
  var installedChunkData = installedChunks[chunkId];
  // 0 代表已经 installed
  if(installedChunkData !== 0) { // 0 means "already installed".

    // a Promise means "currently loading".
    // 目标chunk正在加载,则将 promise push到 promises 数组
    if(installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // setup Promise in chunk cache
      // 利用Promise去异步加载目标chunk
      var promise = new Promise(function(resolve, reject) {
        // 设置 installedChunks[chunkId]
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      // i设置chunk加载的三种状态并缓存在 installedChunks 中,防止chunk重复加载
      // nstalledChunks[chunkId]  = [resolve, reject, promise]
      promises.push(installedChunkData[2] = promise);
      // start chunk loading
      // 使用 JSONP
      var head = document.getElementsByTagName('head')[0];
      var script = document.createElement('script');

      script.charset = 'utf-8';
      script.timeout = 120;

      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      // 获取目标chunk的地址,__webpack_require__.p 表示设置的publicPath,默认为空串
      script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
      // 请求超时的时候直接调用方法结束,时间为 120 s
      var timeout = setTimeout(function(){
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      script.onerror = script.onload = onScriptComplete;
      // 设置加载完成或者错误的回调
      function onScriptComplete(event) {
        // avoid mem leaks in IE.
        // 防止 IE 内存泄露
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        // 如果为 0 则表示已加载,主要逻辑看 webpackJsonpCallback 函数
        if(chunk !== 0) {
          if(chunk) {
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined;
        }
      };
      head.appendChild(script);
    }
  }
  return Promise.all(promises);
};
  • Можно видеть, чтоimport()конвертировать в аналогJSONPдля загрузки динамически загружаетсяchunkдокумент

  • настраиватьchunkТри состояния загружаются и кэшируются вinstalledChunks, чтобы предотвратить повторную загрузку фрагментов. Изменения в этих состояниях будутwebpackJsonpCallbackупоминается в

    // 设置 installedChunks[chunkId]
    installedChunkData = installedChunks[chunkId] = [resolve, reject];
    
    • installedChunks[chunkId]для0, представляющийchunkуже загружен
    • installedChunks[chunkId]дляundefined, представляющийchunkЗагрузка не удалась, время загрузки истекло, никогда не загружалось
    • installedChunks[chunkId]дляPromiseобъект, представляющийchunkзагрузка

прочитай это__webpack_require__.e, что мы знаем, так это то, что мы динамически импортируем через JSONPchunkФайл обрабатывается в соответствии с состоянием результата введения, так как же мы узнаем состояние после введения? Давайте посмотрим на асинхронную загрузкуchunkкак это

Асинхронный блок

// window["webpackJsonp"] 实际上是一个数组,向中添加一个元素。这个元素也是一个数组,其中数组的第一个元素是chunkId,第二个对象,跟传入到 IIFE 中的参数一样
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{

  /***/ "./src/Another.js":
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
  
  "use strict";
  __webpack_require__.r(__webpack_exports__);
  /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Another", function() { return Another; });
  function Another() {
    return 'Hi, I am Another Module';
  }
  /***/ })
  
  }]);
  //# sourceMappingURL=0.bundle.js.map

Главное поместить массивwindow['webpackJsonp']Вставьте элемент в , который также является массивом, где первый элемент массиваchunkId, второй объект, с основнымchunkПараметры, передаваемые в IIFE, аналогичны. Ключ в этомwindow['webpackJsonp']Где он будет использоваться? мы возвращаемся к лордуchunkсередина. существуетreturn __webpack_require__(__webpack_require__.s = "./src/index.js");Перед входом есть еще одна секция

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 保存原始的 Array.prototype.push 方法
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 将 push 方法的实现修改为 webpackJsonpCallback
// 这样我们在异步 chunk 中执行的 window['webpackJsonp'].push 其实是 webpackJsonpCallback 函数。
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
// 对已在数组中的元素依次执行webpackJsonpCallback方法
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

jsonpArrayто естьwindow["webpackJsonp"], сосредоточьтесь на следующем коде при выполненииpushметод, он будет выполняться webpackJsonpCallback, что эквивалентно слою hijacking, то есть эта функция вызывается по завершении операции push.

jsonpArray.push = webpackJsonpCallback;

webpackJsonpCallback — обратный вызов после загрузки динамического чанка

Посмотрим еще раз webpackJsonpCallbackфункция, входные параметры здесь загружаются динамическиchunkизwindow['webpackJsonp']Параметры для ввода.

var installedChunks = {
  "main": 0
};	

function webpackJsonpCallback(data) {
  // window["webpackJsonp"] 中的第一个参数——即[0]
  var chunkIds = data[0];
  // 对应的模块详细信息,详见打包出来的 chunk 模块中的 push 进 window["webpackJsonp"] 中的第二个参数
  var moreModules = data[1];

  // add "moreModules" to the modules object,
  // then flag all "chunkIds" as loaded and fire callback
  var moduleId, chunkId, i = 0, resolves = [];
  for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    // 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]
    // 这个可以看 __webpack_require__.e 中设置的状态
    // 表示正在执行的chunk,加入到 resolves 数组中
    if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    // 标记成已经执行完
    installedChunks[chunkId] = 0;
  }
  // 挨个将异步 chunk 中的 module 加入主 chunk 的 modules 数组中
  for(moduleId in moreModules) {
    if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  // parentJsonpFunction: 原始的数组 push 方法,将 data 加入 window["webpackJsonp"] 数组。
  if(parentJsonpFunction) parentJsonpFunction(data);
  // 等到 while 循环结束后,__webpack_require__.e 的返回值 Promise 得到 resolve
  // 执行 resolove
  while(resolves.length) {
    resolves.shift()();
  }
};

когда мыJSONPзагрузить асинхронноchunkПосле завершения он будет выполненwindow["webpackJsonp"] || []).push, то есть,webpackJsonpCallback. В основном это следующие шаги

  • Перейдите к загружаемым чанкам, найдите незавершенные чанки и добавьте их в разрешения.
for(;i < chunkIds.length; i++) {
  chunkId = chunkIds[i];
  // 所以此处是找到那些未加载完的chunk,他们的value还是[resolve, reject, promise]
  // 这个可以看 __webpack_require__.e 中设置的状态
  // 表示正在执行的chunk,加入到 resolves 数组中
  if(installedChunks[chunkId]) {
    resolves.push(installedChunks[chunkId][0]);
  }
  // 标记成已经执行完
  installedChunks[chunkId] = 0;
}
  • То, что здесь не выполняется, является состоянием, отличным от 0, и после выполнения оно устанавливается в 0.

  • installedChunks[chunkId][0]На самом деле это решение в конструкторе Promise

    // __webpack_require__.e 
    var promise = new Promise(function(resolve, reject) {
    	installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    
  • один за другим будет асинхроннымchunkсерединаmoduleприсоединиться к основномуchunkизmodulesв массиве

  • исходный массивpushметод, будетdataПрисоединяйсяwindow["webpackJsonp"]множество

  • выполнить каждыйresolvesспособ, расскажи__webpack_require__.eСостояние функции обратного вызова в

Только когда этот метод будет выполнен, мы узнаемJSONPуспех или нет, т.script.onload/onerrorБудет вwebpackJsonpCallbackВыполнить потом. такonload/onerrorНа самом деле он используется для проверкиwebpackJsonpCallbackСтепень завершенности: Были лиinstalledChunksсоответствующийchunkзначение равно 0

Резюме динамического импорта

Общий процесс показан на рисунке ниже

流程图

Суммировать

В этой статье анализируетсяwebpackОсновной процесс упаковки и код вывода в случае динамической загрузки резюмируется следующим образом.

  • Общий файл представляет собойIIFE- выполнить функцию немедленно
  • webpackЗагруженные файлы кэшируются для оптимизации производительности.
  • главным образом через__webpack_require__ имитироватьimportмодуль и вернуть модуль в концеexportПеременные
  • webpackкак поддержатьES Moduleиз
  • динамическая нагрузкаimport()Реализация в основном предназначена для использованияJSONPДинамически загружать модули и передаватьwebpackJsonpCallbackОценка результата загрузки

Ссылаться на