Горячая замена модуля (сокращенно HMR) — одна из самых захватывающих функций, представленных webpack на данный момент. Когда вы изменяете и сохраняете код, webpack переупаковывает код и отправляет новый модуль в браузер. Браузер заменяет старый модуль. с новым модулем, чтобы приложение можно было обновить без перезагрузки браузера. Например, в процессе разработки веб-страницы, когда вы нажимаете кнопку и появляется всплывающее окно, вы обнаруживаете, что заголовок всплывающего окна не выровнен.В это время вы изменяете стиль CSS и сохраните его Стиль заголовка происходит без обновления браузера изменено. Это похоже на изменение стилей элементов непосредственно в инструментах разработчика Chrome.
Эта статья не для того, чтобы рассказать вам, как использовать HMR.Если вы еще не знакомы с HMR, рекомендуется сначала прочитать ее.Официальный сайт руководства HMR, с простейшим вариантом использования для HMR, я буду ждать, когда вы вернетесь.
Зачем нужен ХМР
Перед функцией WebPack HMR у нас есть много живых инструментов для перезарядки или библиотек, таких какlive-server, эти библиотеки отслеживают изменения файлов, а затем уведомляют браузер о необходимости обновить страницу, так зачем же нам нужен HMR? Ответ на самом деле упомянут выше.
- Инструмент перезагрузки в реальном времени не может сохранить состояние приложения. При обновлении страницы предыдущее состояние приложения теряется. В приведенном выше примере при нажатии кнопки появляется всплывающее окно. При обновлении браузера всплывающее окно сразу исчезает.Чтобы вернуться в предыдущее состояние, нужно нажать кнопку еще раз. С другой стороны, webapck HMR не обновляет браузер, а выполняет горячую замену модулей во время выполнения, что гарантирует, что состояние приложения не будет потеряно, и повышает эффективность разработки.
- В древнем процессе разработки нам может потребоваться вручную запускать команды для упаковки кода и вручную обновлять страницу браузера после упаковки, и эта серия повторяющихся задач может быть автоматизирована с помощью рабочего процесса HMR, что позволяет больше энергии. трата времени на повторяющуюся работу.
- HMR совместим с большинством интерфейсных фреймворков или библиотек на рынке, таких какReact Hot Loader,Vue-loader, способный слушать React Или изменения в компонентах Vue, обновляйте последние компоненты в браузере в режиме реального времени.Elm Hot LoaderОн поддерживает транспиляцию и упаковку кода языка Elm через веб-пакет и, конечно, также реализует функцию HMR.
Иллюстрация того, как работает HMR
Когда я впервые встретил HMR, я подумал, что это потрясающе, и у меня всегда возникали вопросы.
- Webpack может упаковывать различные модули в файлы пакетов или несколько файлов блоков, но когда я разрабатываю через webpack HMR, я не нахожу файлы, упакованные с помощью webpack, в моем каталоге dist Куда они идут?
- просмотревwebpack-dev-serverфайл package.json, который, как мы знаем, зависит отwebpack-dev-middlewareбиблиотеку, так какую роль играет промежуточное ПО webpack-dev в процессе HMR?
- В процессе использования HMR через инструменты разработчика Chrome я знаю, что браузер взаимодействует с webpack-dev-server через websocket, но в сообщении websocket не найден новый код модуля. Как упакованный новый модуль отправляется в браузер? Почему новый модуль не отправляется в браузер с сообщением через websocket?
- Когда браузер получает последний код модуля, как HMR заменяет старый модуль новым и как он обрабатывает зависимости между модулями в процессе замены?
- Когда модуль заменяется в горячем режиме, если модуль не может быть заменен, существует ли резервный механизм?
С приведенными выше вопросами я решил углубиться в исходный код веб-пакета, чтобы найти основную загадку HMR.
Рисунок 1: Схема рабочего процесса HMR
На приведенном выше рисунке представлена блок-схема горячего обновления модуля для веб-пакета и веб-сервера для разработки приложений.
- Красное поле в нижней части рисунка выше — это сторона сервера, а оранжевое поле вверху — сторона браузера.
- Зеленое поле — это область, контролируемая кодом веб-пакета. Синее поле — это область, контролируемая кодом webpack-dev-server, пурпурное поле — это файловая система, в которой происходят изменения файлов, а голубое поле — это само приложение.
На рисунке показан наш модифицированный код для обновления модуля. Завершение цикла горячего, темно-зеленого цвета арабских цифр имеет весь процесс идентификации HMR.
- Первый шаг, в режиме просмотра веб-пакета, когда файл в файловой системе изменяется, веб-пакет отслеживает изменение файла, перекомпилирует и упаковывает модуль в соответствии с файлом конфигурации и сохраняет упакованный код в памяти через простой объект JavaScript. .
- Вторым шагом является интерфейсное взаимодействие между webpack-dev-server и webpack, и на этом этапе, в основном взаимодействие между промежуточным ПО dev-server webpack-dev-middleware и webpack, webpack-dev-middleware вызывает webpack для раскрытия кода. изменяется и указывает webpack упаковать код в память.
- Третий шаг — это мониторинг изменений файлов с помощью webpack-dev-server, этот шаг отличается от первого тем, что он не отслеживает изменения кода и не перепаковывает их. Когда мы настраиваем в файле конфигурацииdevServer.watchContentBaseКогда правда, Сервер Он будет отслеживать изменения статических файлов в этих папках конфигурации и уведомлять браузер о необходимости выполнить перезагрузку приложения в режиме реального времени после изменения. Обратите внимание, что здесь обновление браузера и HMR — это два разных понятия.
- Четвертый шаг — это также работа кода webpack-dev-server, которая в основном выполняетсяsockjs(зависимость от webpack-dev-server) Установить длинное соединение по вебсокету между браузером и сервером, сообщить браузеру информацию о статусе каждого этапа компиляции и упаковки вебпака, а также включить мониторинг статических файлов сервером на третьем шаге. изменение информации. Сторона браузера в соответствии с этими сообщения сокетов выполняют различные операции. Разумеется, самой важной информацией, передаваемой сервером, является хеш-значение нового модуля, и последующие шаги будут выполнять горячую замену модуля на основе этого хэш-значения.
- Сторона webpack-dev-server/client не может ни запрашивать обновленный код, ни выполнять операции горячего модуля и передавать эти задачи webpack.Работа webpack/hot/dev-server основана на webpack-dev. -server/client и конфигурация dev-server определяют, следует ли обновлять браузер или выполнять горячее обновление модуля. Конечно, если вы просто обновите браузер, дальнейших шагов не будет.
- HotModuleReplacement.runtime является хабом HMR клиента, получает хеш-значение нового модуля, переданного ему на предыдущем шаге, отправляет Ajax-запрос на сервер через JsonpMainTemplate.runtime, и сервер возвращает json, содержащий все обновления для обновления.После получения обновленного списка хэш-значения модуля модуль снова запрашивает последний код модуля через jsonp. Это шаги 7, 8 и 9 на изображении выше.
- 10-й шаг является ключевым шагом для определения успеха HMR.На этом этапе HotModulePlugin сравнит старый и новый модули, чтобы решить, следует ли обновлять модуль.Приняв решение об обновлении модуля, проверьте зависимости между модулями и обновите module Также обновите зависимости между модулями.
- На последнем шаге, когда HMR дает сбой, вернитесь к операции перезагрузки в реальном времени, которая заключается в обновлении браузера для получения последнего упакованного кода.
Простой пример с использованием HMR
В предыдущей части с помощью блок-схемы HMR был кратко объяснен процесс горячего обновления модуля HMR. Конечно, вы все еще можете чувствовать себя запутанным, и вы можете быть незнакомы с некоторыми английскими терминами, которые появляются выше (вышеуказанные английские термины представляют собой склады кода или файловые модули на складе), это не имеет значения, в этой части я пропущу аСамый простой и чистый пример, проанализировав wepack и webpack-dev-server В исходном коде подробно описаны конкретные обязанности каждой библиотеки в процессе HMR.
Прежде чем приступить к этому примеру, ниже приводится краткое описание файла репозитория, который содержит следующие файлы:
--hello.js
--index.js
--index.html
--package.json
--webpack.config.js
Проект содержит два файла js. Файл входа в проект — это файл index.js, а файл hello.js — зависимость от файла index.js. Добавьте элемент div, содержащий «hello world».
Конфигурация webpack.config.js выглядит следующим образом:
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: './index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, '/')
},
devServer: {
hot: true
}
}
Стоит отметить, что HotModuleReplacementPlugin не настраивается в приведенной выше конфигурации, потому что, когда мы устанавливаем devServer.hot в true и добавляем следующий сценарий сценария в файл package.json:
"start": "webpack-dev-server --hot --open"
После добавления элемента конфигурации --hot devServer скажет веб-пакету автоматически импортировать плагин HotModuleReplacementPlugin вместо того, чтобы импортировать его вручную.
Перейдите в каталог хранилища, после того, как npm install установит зависимости, запустите npm start, чтобы запустить службу devServer, доступhttp://127.0.0.1:8080Вы можете посмотреть нашу страницу.
Следующее введет ключевую ссылку.В простом примере я изменю код в файле hello.js, чтобы проанализировать конкретный процесс работы HMR на уровне исходного кода.Конечно, я буду анализировать его в соответствии с приведенной выше схемой. Измененный код выглядит следующим образом: (Первая строка всех следующих блоков кода — это путь к файлу)
// hello.js
- const hello = () => 'hello world' // 将 hello world 字符串修改为 hello eleme
+ const hello = () => 'hello eleme'
После этого текст hello world на странице становится hello eleme.
Первый шаг: webpack следит за файловой системой и упаковывает ее в память
webpack-dev-middleware вызывает API веб-пакета для наблюдения за файловой системой.Когда файл hello.js изменяется, веб-пакет перекомпилирует и упаковывает файл, а затем сохраняет его в памяти.
// webpack-dev-middleware/lib/Shared.js
if(!options.lazy) {
var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);
context.watching = watching;
}
Вам может быть интересно, почему webpack не упаковывает файлы непосредственно в каталог output.path? Куда пропали файлы? Получается, что webpack упаковывает файл bundle.js в память, причина, по которой файл не генерируется, в том, что доступ к коду в памяти быстрее, чем доступ к файлу в файловой системе, а также снижает накладные расходы на запись файла код в файл, все благодаряmemory-fs, память-fs Это зависимая библиотека webpack-dev-middleware.webpack-dev-middleware заменяет исходную outputFileSystem из webpack экземпляром MemoryFileSystem, так что код будет выводиться в память. Исходный код этой части в webpack-dev-middleware выглядит следующим образом:
// webpack-dev-middleware/lib/Shared.js
var isMemoryFs = !compiler.compilers && compiler.outputFileSystem instanceof MemoryFileSystem;
if(isMemoryFs) {
fs = compiler.outputFileSystem;
} else {
fs = compiler.outputFileSystem = new MemoryFileSystem();
}
Сначала определите, является ли текущая файловая система уже экземпляром MemoryFileSystem, если нет, замените outputFileSystem перед компилятором экземпляром MemoryFileSystem. Таким образом, код файла bundle.js сохраняется в памяти как простой объект javascript.Когда браузер запрашивает файл bundle.js, devServer напрямую находит сохраненный выше объект javascript в памяти и возвращает его браузеру.
Шаг 2: devServer уведомляет браузер об изменении файла
На данном этапе,sockjsЭто мост между сервером и браузером.При запуске devServer sockjs устанавливает длинное соединение через веб-сокет между сервером и браузером, чтобы информировать браузер о статусе каждого этапа компиляции и упаковки веб-пакета.Наиболее важные Шаг по-прежнему webpack-dev-server для вызова webpack api
слушать компилироватьdone
Событие, когда компиляция завершена, проходит webpack-dev-server_sendStatus
Метод отправляет в браузер скомпилированное и упакованное хеш-значение нового модуля.
// webpack-dev-server/lib/Server.js
compiler.plugin('done', (stats) => {
// stats.hash 是最新打包文件的 hash 值
this._sendStats(this.sockets, stats.toJson(clientStats));
this._stats = stats;
});
...
Server.prototype._sendStats = function (sockets, stats, force) {
if (!force && stats &&
(!stats.errors || stats.errors.length === 0) && stats.assets &&
stats.assets.every(asset => !asset.emitted)
) { return this.sockWrite(sockets, 'still-ok'); }
// 调用 sockWrite 方法将 hash 值通过 websocket 发送到浏览器端
this.sockWrite(sockets, 'hash', stats.hash);
if (stats.errors.length > 0) { this.sockWrite(sockets, 'errors', stats.errors); }
else if (stats.warnings.length > 0) { this.sockWrite(sockets, 'warnings', stats.warnings); } else { this.sockWrite(sockets, 'ok'); }
};
Шаг 3: webpack-dev-server/client получает сообщение сервера и отвечает
У вас снова могут возникнуть вопросы Я не добавлял код для получения сообщений вебсокета в бизнес-коде, а также не добавлял новый файл записи в атрибут записи в webpack.config.js, затем код для получения сообщений вебсокета в пакете. js Откуда это взялось? Оказалось, что webpack-dev-server модифицировал атрибут entry в конфигурации webpack и добавил код webpack-dev-client, так что код для получения websocket-сообщений будет в итоговом файле bundle.js.
webpack-dev-server/client временно сохранит хэш-значение после получения сообщения с типом хэша и выполнит операцию перезагрузки в приложении после получения сообщения с типом ok, как показано на рисунке ниже, хэш-сообщение перед сообщением ок.
Рисунок 2: websocket получает список сообщений, отправленных dev-сервером в браузер через sockjs
В операции перезагрузки webpack-dev-server/client решит, следует ли обновить браузер или выполнить горячее обновление (HMR) кода в соответствии с горячей конфигурацией. код показывает, как показано ниже:
// webpack-dev-server/client/index.js
hash: function msgHash(hash) {
currentHash = hash;
},
ok: function msgOk() {
// ...
reloadApp();
},
// ...
function reloadApp() {
// ...
if (hot) {
log.info('[WDS] App hot update...');
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
// ...
} else {
log.info('[WDS] App updated. Reloading...');
self.location.reload();
}
}
Как показано в приведенном выше коде, сначала временно сохраните хэш-значение в переменной currentHash и перезагрузите приложение после получения сообщения ok. Если настроено горячее обновление модуля, вызовите webpack/hot/emitter, чтобы отправить последнее хеш-значение в webpack, а затем передайте управление клиентскому коду webpack. Если горячее обновление модуля не настроено, вызовите метод location.reload напрямую, чтобы обновить страницу.
Шаг 4: webpack получает последнюю проверку хеш-значения и запрашивает код модуля.
На этом этапе это фактически результат взаимодействия между тремя модулями (три файла, за которыми следует английское имя, соответствующее пути к файлу) в webpack.Первый — это webpack/hot/dev-server (далее именуемый dev -server) Мониторинг веб-пакета третьего шага, отправленного dev-сервером/клиентомwebpackHotUpdate
сообщение, вызовите метод проверки в webpack/lib/HotModuleReplacement.runtime (называемый средой выполнения HMR), чтобы определить, есть ли новое обновление, проверяемое
В процессе будут использоваться два метода в webpack/lib/JsonpMainTemplate.runtime (называемые средой выполнения jsonp).hotDownloadUpdateChunk
а такжеhotDownloadManifest
, второй способ — вызвать AJAX для запроса обновленных файлов у сервера, если есть список обновленных файлов, которые нужно отправить обратно в браузер, а первый способ — запросить последний код модуля через jsonp, а затем вернуть код для среды выполнения HMR, среда выполнения HMR
Дальнейшая обработка будет выполняться в соответствии с возвращенным кодом нового модуля, что может быть связано с обновлением страницы или горячим обновлением модуля.
Рисунок 3: Метод hotDownloadManifest получает список обновленных файлов
Рисунок 4: hotDownloadUpdateChunk получает обновленный код нового модуля
Как показано на двух рисунках выше, стоит отметить, что два запроса представляют собой имена файлов запроса, объединенные с последним хеш-значением, метод hotDownloadManifest возвращает последнее хеш-значение, а метод hotDownloadUpdateChunk возвращает код, соответствующий последнему хэш-значению. Кусок. Затем верните новый блок кода в среду выполнения HMR для горячего обновления модуля.
все еще помнюИллюстрация того, как работает HMRв вопросе 3? Почему код модуля обновления не отправляется напрямую в браузер через websocket на третьем шаге, а получается через jsonp? Насколько я понимаю, разделение функциональных блоков, каждый модуль выполняет свои обязанности, dev-сервер/клиент отвечает только за передачу сообщений, а не за приобретение новых модулей, и эти задачи должна выполнять среда выполнения HMR. , а среда выполнения HMR должна быть местом для получения нового кода. Затем, из-за того, что вы не используете webpack-dev-server, используйтеwebpack-hot-middlewareВ связке с webpack также может выполняться процесс горячего обновления модуля.Есть интересная особенность использования webpack-hot-middleware.Он не использует websocket, а использует long polling. Подводя итог, в рабочем процессе HMR новый код модуля не должен помещаться в сообщения веб-сокета.
Шаг 5: HotModuleReplacement.runtime для горячего обновления модуля
Этот шаг является ключевым во всем горячем обновлении модуля (HMR), а горячее обновление модуля происходит в методе hotApply среды выполнения HMR.Я не планирую публиковать здесь весь исходный код метода hotApply, потому что это содержит более 300 строк кода, я буду извлекать только ключевые фрагменты кода.
// webpack/lib/HotModuleReplacement.runtime
function hotApply() {
// ...
var idx;
var queue = outdatedModules.slice();
while(queue.length > 0) {
moduleId = queue.pop();
module = installedModules[moduleId];
// ...
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
// remove "parents" references from all children
for(j = 0; j < module.children.length; j++) {
var child = installedModules[module.children[j]];
if(!child) continue;
idx = child.parents.indexOf(moduleId);
if(idx >= 0) {
child.parents.splice(idx, 1);
}
}
}
// ...
// insert new code
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
// ...
}
Как видно из вышеописанного метода hotApply, горячая замена модулей в основном делится на три этапа.Первый этап - поиск устаревших модулей и устаревших зависимостей.Эту часть кода я здесь не выкладывал.Если интересно,можете почитать исходный код самостоятельно. На втором этапе из кеша удаляются устаревшие модули и зависимости следующим образом:
delete installedModules[moduleId];
delete outdatedDependencies[moduleId];
Третий этап — добавление новых модулей в модули, при следующем вызове метода __webpack_require__ (require method required by webpack) получается новый код модуля.
Обработка ошибок при горячем обновлении модуля. Если при горячем обновлении возникает ошибка, горячее обновление откатывается к обновлению браузера. Эта часть кода находится в коде dev-сервера. Краткий код выглядит следующим образом:
module.hot.check(true).then(function(updatedModules) {
if(!updatedModules) {
return window.location.reload();
}
// ...
}).catch(function(err) {
var status = module.hot.status();
if(["abort", "fail"].indexOf(status) >= 0) {
window.location.reload();
}
});
dev-сервер, чтобы проверить, есть ли обновления, нет ли обновлений кода, а затем перезагрузите браузер. Если вы прерываете или терпите неудачу, возникает ошибка во время перегрузки браузера hotApply.
Шаг шестой: бизнес-код должен что-то делать?
После замены старого модуля кодом нового модуля наш бизнес-код не знает, что код изменился, то есть при изменении файла hello.js нам нужно вызвать метод accept HMR в файле index.js. file., добавьте функцию обработки после обновления модуля и вовремя вставьте возвращаемое значение метода hello на страницу. код показывает, как показано ниже:
// index.js
if(module.hot) {
module.hot.accept('./hello.js', function() {
div.innerHTML = hello()
})
}
Это весь рабочий процесс HMR.
напиши в конце
Цель этой статьи не в том, чтобы предоставить подробный анализ webpack HMR, и многие детали не обсуждаются слишком подробно, она просто хочет сыграть роль руководства, чтобы показать вам обзор рабочего процесса HMR.Если вам интересно в webpack, вы хотите узнать об этом больше.Для более подробной информации о webpack HMR, я считаю, что чтение исходного кода webpack будет хорошим выбором, и я надеюсь, что эта статья поможет вам прочитать исходный код, который моя настоящая цель письма.