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
Оценка результата загрузки