процесс сборки вебпака
Webpack — самый популярный интерфейсный инструмент для упаковки и сборки. По сути, это упаковщик модулей. Он ищет зависимости между модулями посредством рекурсивного анализа из входного файла и, наконец, выводит один или несколько файлов пакетов.
Создание вебпака — это последовательный процесс, от начала до конца последовательно выполняются следующие процессы:
-
Начальная конфигурация
Прочитайте параметры из файла конфигурации и командной строки и объедините параметры, сгенерируйте окончательный элемент конфигурации и выполните оператор создания экземпляра плагина в файле конфигурации, сгенерируйте метод применения, переданный компилятором в плагин, и перехватите пользовательский хук для поток событий веб-пакета;
-
начать компиляцию
Сгенерируйте пример компилятора, выполнитеcompile.run, чтобы начать компиляцию;
-
Определяем входной файл
Чтение всех входных файлов из элементов конфигурации;
-
модуль компиляции
Начать компиляцию из входного файла, использовать соответствующий загрузчик для компиляции модуля и рекурсивно скомпилировать модули, от которых зависит текущий модуль.После компиляции всех модулей получается окончательное содержимое всех модулей и зависимости между модулями, и, наконец, все модули скомпилированы.
require
заменить заявление на__webpack_require__
имитировать модульную работу; -
выход ресурса
В соответствии с зависимостями между записью и модулем собрать в чанки, содержащие несколько модулей, а затем преобразовать чанк в отдельный файл и добавить его в выходной список;
-
makefile
Создайте файл на основе конфигурации и выведите сгенерированное содержимое в указанное место.
Основным объектом webpack является Compile, который отвечает за мониторинг файлов и запуск компиляции.Он наследуется от Tapable[GitHub.com/Веб-пакет/Taping…], чтобы экземпляр Compile имел функцию регистрации и вызова плагинов.
Когда веб-пакет выполняет процесс построения, веб-пакет будет транслировать соответствующее событие в определенное время.После того, как плагин прослушает событие, он выполнит определенную логику для изменения содержимого модуля.
С помощью следующей блок-схемы мы можем получить более интуитивное представление о процессе создания веб-пакета:
блок-схема работы webpackАнализ выходных файлов webpack
Далее мы проанализируем, как файл пакета запускается в браузере, анализируя вывод файла пакета webpack.
анализ одного файла
Сначала создайтеsrc/index.js
, выполните простейший оператор js:
console.log('hello world')
Создайтеwebpack.config.js
, настроенный следующим образом:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist')
}
}
Версия веб-пакета, используемая в этом примере, — 4.35.3. Чтобы лучше анализировать выходной файл пакета, режим установлен на «нет». В настоящее время веб-пакет не будет включать какие-либо плагины по умолчанию.
mode имеет три необязательных значения, а именно «нет», «производство», «разработка», значение по умолчанию — «производство», и по умолчанию включены следующие плагины:
-
FlagDependencyUsagePlugin: помечать зависимости во время компиляции;
-
FlagIncludedChunksPlugin: пометить подчанки, чтобы предотвратить множественную загрузку зависимостей;
-
ModuleConcatenationPlugin: хостинг области, функция прекомпиляции, обновление или прекомпиляция всех модулей в замыкание, повышение скорости выполнения кода в браузере;
-
NoEmitOnErrorsPlugin: на этапе вывода пропускается при обнаружении ошибок компиляции;
-
OccurrenceOrderPlugin: более короткие значения для часто используемых идентификаторов;
-
SideEffectsFlagPlugin: определите флаг sideEffects в package.json или module.rules (чистые модули ES2015) и безопасно удалите неиспользуемые экспорты;
-
TerserPlugin: Сжать код
Когда значение режима равно «разработка», следующие плагины включены по умолчанию:
-
NamedChunksPlugin: закрепляет chunkId по имени;
-
NamedModulesPlugin: закрепляет идентификатор модуля по имени
Выполните команду сборки веб-пакета:
$ webpack
вывод в папку distmain.js
Содержимое файла следующее:
(function(modules) { // webpackBootstrap
// 模块缓存
var installedModules = {};
// 模块加载函数
function __webpack_require__(moduleId) {
// 如果加载过该模块,则直接从缓存中读取
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块并将其缓存起来
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行模块函数,设置module.exports
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 将module标记为已加载
module.l = true;
// 返回设置好的module.exports
return module.exports;
}
// 指向modules
__webpack_require__.m = modules;
// 指向缓存
__webpack_require__.c = installedModules;
// 定义exports的get方式
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 设置es6模块标记
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// 兼容commonjs和es6模块
__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;
};
// Object.prototype.hasOwnProperty的封装
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// webpack配置的publicpath
__webpack_require__.p = "";
// 加载模块并返回
return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
([
/* 0 */
/***/ (function(module, exports) {
console.log('hello world')
/***/ })
]);
Вы можете видеть, что выходной код представляет собой IIFE (немедленно выполняемую функцию), которую можно упростить следующим образом:
(function(modules) {
var installedModules = {};
// webpack require语句
// 加载模块
function __webpack_require__(moduleId) {}
return __webpack_require__(0)
})([
function(module, exports) {
console.log('hello world')
}
])
в упрощенном коде__webpack_require__
Функция играет функцию загрузки модуля.Параметр, полученный функцией IIFE, представляет собой массив, а 0-й элементsrc/index.js
операторы кода в__webpack_require__
Функция загружает и выполняет модуль и, наконец, выводит его в консоль браузера.hello world
.
Далее давайте проанализируем код__webpack_reuqire__
Как работают функции внутри
function __weboack_require__(moduleId) {
// 如果已经加载过该模块,则从缓存中直接读取
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 如果没有加载过该模块,则创建一个新的module存入缓存中
var module = installedModules[moduleId] = {
i: moduleId, // module id
l: false, // 是否已加载 false
exports: {} // 模块导出
};
// 执行该module
// call方法第一个参数为modules.exports,是为了module内部的this指向该模块
// 然后传入三个参数,分别为module, module.exports, __webpack_require__模块加载函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 设置module为已加载
module.l = true;
// 最终返回module.exports
return module.exports;
}
}
можно увидеть__webpack_require__
Функция получает идентификатор модуля, выполняет модуль и, наконец, возвращает экспорт модуля и кэширует модуль в памяти. Если модуль загружается снова, он считывается непосредственно из кеша.modules[modulesId]
Содержимое - это 0-й элемент параметра IIFE, то есть:
function(module, exports) {
console.log('hello world')
}
В экспортируемом IIFE, кроме__webpack_require__
функция, еще__webpack_require__
Загружено много свойств.
-
__webpack_require__.m
: смонтировать все модули; -
__webpack_require__.c
: Монтировать кешированные модули; -
__webpack_require__.d
: Определение экспорта Getter; -
__webpack_require__.r
: установить модуль на модуль es6; -
__webpack_require__.t
: вернуть соответствующий обработанный модуль или значение в соответствии с различными сценариями; -
__webpack_require__.n
: вернуть геттер, чтобы определить, является ли он внутренним модулем es6; -
__webpack_require__.o
: инкапсуляция функции Object.prototype.hasOwnProperty; -
__webpack_require__.p
: свойство publicPath в выходном элементе конфигурации;
Анализ цитирования нескольких файлов
В предыдущем примере пакет, упакованный webpack, содержит только очень простой входной файл, и между модулями нет ссылок.
Ниже мы модифицируемsrc/index.js
код в , ссылающийся на модуль ES6src/math.js
Заходи:
// math.js
const add = function (a, b) {
return a + b
}
export default add
// index.js
import add from './math'
console.log(add(1, 2))
Повторно выполните команду упаковки веб-пакета, вы увидите, что параметры в выходном IIFE стали двумя элементами:
([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
console.log(Object(_math__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
const add = function (a, b) {
return a + b
}
/* harmony default export */ __webpack_exports__["default"] = (add);
/***/ })
]);
Элемент 1 массива определенmath.js
модуль и, выполнив__webpack_require__.r(__webpack_exports__)
Заставьте webpack распознать, что модуль является модулем ES6, и, наконец,__webpack_exports__
изdefault
значение свойства установлено для функцииadd
.
Элемент 0 массиваindex.js
Выход модуля после упаковки, заявлениеvar _math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1)
Функция состоит в том, чтобы преобразовать модульmath.js
Экспортadd
функция импортируется,__webpack_require__(1)
вернутьmodule.exports
,в1
это chunkId, созданный webpack при упаковке и, наконец, переданныйconsole.log(Object(_math__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))
воплощать в жизньindex.js
заявление в .
Webpack загружает исходные независимые модули, сохраняя их в параметрах IIFE, так что все модули могут быть выполнены только с одним сетевым запросом, избегая проблемы длительного времени загрузки, вызванной многократной сетевой загрузкой каждого модуля. А внутри функции IIFE веб-пакет еще больше оптимизирует загрузку модулей.Кэшируя загруженные модули и сохраняя их в памяти, те же самые модули напрямую извлекаются из памяти, когда один и тот же модуль загружается во второй раз.
Анализ асинхронной нагрузки
Приведенные выше два примера как загружают модули синхронно, так и выполняют их, но в реальных проектах, чтобы улучшить скорость загрузки страницы, модули, которые временно не используются во время инициализации первого экрана, часто загружаются асинхронно, например модуль маршрутизации после прыжка с домашней страницы и т.д. Далее мы загрузим его асинхронноmath.js
модуль и выполнить его экспортированныйadd
функция.
import('./math').then((add) => {
console.log(add(1, 2))
})
После перепаковки выводmain.js
а также1.js
,1.js
это файл, который необходимо загрузить асинхронно.
Сначала проанализируйте входной файлmain.js
, вы можете видеть, что по сравнению с выводом кода метода синхронной загрузки в файле больше__webpack_require__.e
а такжеwebpackJsonpCallback
функция, в IIFE есть только один параметр:
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then((add) => {
console.log(add(1, 2))
})
/***/ })
Этот модуль проходит__webpack_require__.e(1)
Загрузите файл модуля 1 способом загрузки, а затем выполните__webpack_require__.bind(null, 1)
Вернитесь к модулю 1, а затем выполнитеadd
функция.
__webpack_require__.e
Функция заключается в загрузке модулей, которые должны быть загружены асинхронно.Содержание функции следующее:
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) { // 如果为0则代表已经加载过该模块
// installedChunkData 不为空且不为0表示该 Chunk 正在网络加载中
// 直接返回promise对象
if (installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// 该chunk从未被加载过,返回数组包含三项,分别是resolve,reject和创建的promise对象
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// 创建script标签,加载模块
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
// jsonpScriptSrc的作用是返回根据配置的publicPath和chunkId生成的文件路径
script.src = jsonpScriptSrc(chunkId);
// 创建一个Error实例,用于在加载错误时catch
var error = new Error();
onScriptComplete = function (event) {
// 防止内存泄漏
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if (chunk !== 0) {
if (chunk) {
// 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;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
// 异步加载最长等待时间120s
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
// 将创建的script标签插入dom中
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
Внутри функции сначала определите, был ли загружен модуль, если нет, создайтеscript
Этикетка,script
Путь лежит через внутреннююjsonpScriptSrc
Функция генерирует окончательный путь src в соответствии с конфигурацией webpack и возвращает его. В итоге функция возвращаетPromise
Объекты, когда файл JS не загружается, будет выполнятьсяreject
скиньте ошибку.
math.js
выходной комплект1.js
Содержание очень простое, код выглядит следующим образом:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],[
/* 0 */,
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
const add = function (a, b) {
return a + b
}
/* harmony default export */ __webpack_exports__["default"] = (add);
/***/ })
]]);
Вы можете видеть, что роль пакета заключается в отправкеwindow['webpackJsonp']
Новый массив помещается в массив, первый элемент которого[1]
это chunkId, созданный веб-пакетом, а второй элементmath.js
Конкретное содержимое преобразованного модуля.
В то же время вmain.js
Последняя часть IIFE для глобального крепленияwindow['webpackJsonp']
Метод push массива был переписан, чтобы указывать на ранее определенныйwebpackJsonpCallback
функция:
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// 将data第1项模块添加到modules中,
// 然后将对应的chunkId标记为已加载
var moduleId, chunkId, i = 0, resolves = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
// 将传进来的moreModules数组中的每一个模块依次添加到IIFE中缓存的modules中
for(moduleId in moreModules) {
if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// parentJsonpFunction为window['webpackJsonp']中原声的数组push方法
// 执行parentJsonpFunction将data真正的添加到window['webpackJsonp']数组中去
if(parentJsonpFunction) parentJsonpFunction(data);
// 将前面创建的promise执行resolve
while(resolves.length) {
resolves.shift()();
}
};
после анализаwebpackJsonpCallback
Содержимое функции, вы можете видеть, что основная функция этой функции - пометить входящий чанкид как загруженный, и повесить входящий модуль в модуле кешаmodules
объект, окончательное исполнение__webpack_require__.e
Метод разрешения объекта обещания, возвращаемый функцией, указывает, что асинхронно загруженный модуль был загружен.__webpack_require__.e(1).then()
Вы можете загрузить модуль, синхронно загрузив модуль.
Реорганизуйте общий процесс загрузки асинхронных модулей в основной файл записи:
- воплощать в жизнь
__webpack_require__.e
загружать асинхронные модули; - Создайте тег скрипта, соответствующий чанку, чтобы загрузить скрипт и вернуть обещание;
- Если нагрузка не удается, отклоните обещание; если нагрузка успешна, асинхронный кусок выполнен сразу
window[webpackJsonp]
Метод push отмечает модуль как загруженный и разрешает соответствующее обещание; - После успеха вы можете
__webpack_require__.e().then
загружать модули синхронно.
сводка выходного файла
В файле, выводимом webpack, передать все модули в качестве параметров в виде IIFE, использовать__webpack_require__
Смоделируйте оператор import или require, а затем рекурсивно выполните загруженные модули из модуля ввода.Модули, которые необходимо загрузить асинхронно, загружаются путем вставки нового тега script в dom. И оптимизация внутренней обработки кеша выполняется для загрузки модуля.
В реальном проекте содержимое выходного пакета намного сложнее, чем в демонстрации в этой статье, и будет оптимизация настроек ChunkID, извлечение общедоступных блоков, сжатие кода, но вы можете быть знакомы с этой самой простой демонстрацией, знакомой с WebPack. Файлы в рабочих процессах среды выполнения для удобного анализа при отладке.
Написать простой загрузчик
Прежде чем писать загрузчик, давайте кратко представим роль загрузчика webpack. В веб-пакете загрузчик можно понимать как конвертер, обрабатывающий ввод файла, возвращающий новый результат и, наконец, передающий его веб-пакету для дальнейшей обработки.
Загрузчик — это модуль nodejs, и его основная структура выглядит следующим образом:
// 可以通过loader-utils这个包获取该loader的配置项options
const loaderUtils = require('loader-utils')
// 导出一个函数,source为webpack传递给loader的文件源内容
module.exports = function(source) {
// 获取该loader的配置项
const options = loaderUtils.getOptions(this)
// 一些转换处理,最终返回处理后的结果。
return source
}
При настройке загрузчика webpack мы всегда используем загрузчик установленный через npm.Чтобы загрузить локальный загрузчик обычно есть два способа.Первый это связать загрузчик с node_modules проекта через ссылку npm, и есть еще один Способ заключается в том, чтобы сообщить webpack, в какой форме искать загрузчик, настроив элемент конфигурации resolveLoader.modules в wepack. Первый способ требует настройки, связанной сpackage.json
, в этом примере используйте вторую конфигурацию.
module.exports = {
resolveLoader: {
// 假设本地编写的loader在loaders文件夹下
modules: ['node_modules', './loaders/']
}
}
Ниже пишем загрузчик для удаления комментариев в коде. Назовите его remove-comment-loader:
module.exports = function(source) {
// 匹配js中的注释内容
const reg = new RegExp(/(\/\/.*)|(\/\*[\s\S]*?\*\/)/g)
// 删除注释
return source.replace(reg, '')
}
Затем измените webpack.config.js:
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/index.js',
module: {
rules: [
{
test: /\.js$/,
loader: 'remove-comment-loader' // 当匹配到js文件时,使用我们编写的remove-comment-loader
}
]
},
output: {
path: path.resolve(__dirname, 'dist')
},
resolveLoader: {
modules: ['node_modules', './loaders/'] // 配置加载本地loader
}
}
Затем добавьте несколько комментариев к коду входного файла, переупакуйте и просмотрите выходной файл, и вы увидите, что комментарии в коде были удалены.
См. демонстрационный код в этой статье;GitHub.com/Ду Венбин031…
Кстати, я хотел бы порекомендовать apollo-build, интерфейсный инструмент упаковки и построения в мобильной платформе финансовой разработки Firefly компании Minsheng Technology. apollo-build включает в себя отладку разработки, упаковку, тестирование,
И функция упаковки dll, и предоставляет очень полезную интерфейсную функцию Mock интерфейса, опыт работы с командной строкой согласуется с create-реагировать-приложение. Мы инкапсулировали большинство общих функций в webpack и сделали множество внутренних оптимизаций, а также извлекли наиболее часто используемые элементы конфигурации.Даже если вы не знакомы с конфигурацией webpack, вы можете быстро начать работу, а также поддерживатьwebpack.config.js
Чтобы внести дополнительные изменения, посетите официальный сайт Minsheng Technology.
Ссылаться на
«Углубленный веб-пакет» - Ву Хаолинь
Демистификация Webpack — единственный способ создать высококачественный интерфейс
об авторе
Ду Вэньбинь Миншэн Технолоджи Ко., Лтд. Технологический отдел пользовательского опыта Firefly Mobile Financial Development Platform Инженер по фронтенд-разработке