Чжу Хайхуа: Группа поддержки платформы отдела передовых технологий WeDoctor Мой регион подходит, вы можете попробовать еще раз~🤔
Предыстория HMR
В использованииWebpack Dev Server
В будущем мы можем сосредоточиться на кодировании в проекте разработки, поскольку он может контролировать изменения кода для достижения обновлений пакетов и, наконец, синхронизируйте в браузере через автоматическое обновление, чтобы мы могли просматривать эффект во времени. Но DEV Server идет от прослушивания упаковки в уведомление浏览器整体刷新页面
приводит к неприятной проблеме, заключающейся в том, что无法保存应用状态
Итак, для этой проблемы Webpack предлагает новое решение.Hot Module Replacement
Простая концепция HMR
Горячая замена модуля означает, что когда мы изменяем и сохраняем код, Webpack переупаковывает код и отправляет новый модуль в браузер. Браузер заменяет старый модуль новым модулем, чтобы не обновлять страницу. браузера. Наиболее очевидным преимуществом является то, что по сравнению с традиционнымlive reload
Другими словами, HMR не теряет состояние приложения и повышает эффективность разработки. Прежде чем мы начнем подробно разбираться в Webpack HMR, мы можем кратко просмотреть следующую блок-схему.
Обзор процесса HMR
- Компиляция Webpack: наблюдайте за упаковкой локальных файлов и записью в память
- Boundle Server: Запустите локальную службу, которая предоставляет файлы для доступа на стороне браузера.
- Сервер HMR: экспорт оперативно обновляемых файлов в среду выполнения HMR.
- Среда выполнения HMR: сгенерированные файлы, внедренные в память браузера.
- Bundle: сборка выходных файлов
Начало работы с HMR
На самом деле открыть HMR очень просто, потому что сам HMR интегрирован в Webpack, и открыть его можно двумя способами.
- Добавьте напрямую, запустив команду webpack-dev-server
--hot
Параметр Открыть HMR напрямую - Напишите код файла конфигурации следующим образом
// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
// ...
devServer: {
// 开启 HMR 特性 如果不支持 MMR 则会 fallback 到 live reload
hot: true,
},
plugins: [
// ...
// HMR 依赖的插件
new webpack.HotModuleReplacementPlugin()
]
}
Сервер и клиент в HMR
devServer уведомляет браузер об изменении файла
Листая через webpack-dev-serverисходный кодВ этом процессе в зависимости отsockjsПредусмотрен мост между сервером и браузером.При запуске devServer устанавливается длинная ссылка webSocket для уведомления браузера о различных состояниях при компиляции и упаковке webpack, и в то же время отслеживание события done при компиляции. компиляция завершена, хеш-значение перекомпилированного и упакованного нового модуля отправляется в браузер через метод sendStats.
// webpack-dev-server/blob/master/lib/Server.js
sendStats(sockets, stats, force) {
const shouldEmit =
!force &&
stats &&
(!stats.errors || stats.errors.length === 0) &&
(!stats.warnings || stats.warnings.length === 0) &&
stats.assets &&
stats.assets.every((asset) => !asset.emitted);
if (shouldEmit) {
this.sockWrite(sockets, 'still-ok');
return;
}
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');
}
}
Клиент получает сообщение сервера и отвечает
Когда webpack-dev-server/client получает сообщение типа hash, он временно кэширует хеш-значение, а когда получает сообщение типа ok, он выполняет операцию перезагрузки в браузере.
выбор стратегии перезагрузки
function reloadApp(
{ hotReload, hot, liveReload },
{ isUnloading, currentHash }
) {
if (isUnloading || !hotReload) {
return;
}
if (hot) {
log.info('App hot update...');
const hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
if (typeof self !== 'undefined' && self.window) {
// broadcast update to window
self.postMessage(`webpackHotUpdate${currentHash}`, '*');
}
}
// allow refreshing the page only if liveReload isn't disabled
else if (liveReload) {
let rootWindow = self;
// use parent window for reload (in case we're in an iframe with no valid src)
const intervalId = self.setInterval(() => {
if (rootWindow.location.protocol !== 'about:') {
// reload immediately if protocol is valid
applyReload(rootWindow, intervalId);
} else {
rootWindow = rootWindow.parent;
if (rootWindow.parent === rootWindow) {
// if parent equals current window we've reached the root which would continue forever, so trigger a reload anyways
applyReload(rootWindow, intervalId);
}
}
});
}
function applyReload(rootWindow, intervalId) {
clearInterval(intervalId);
log.info('App updated. Reloading...');
rootWindow.location.reload();
}
Перелистывая webpack-dev-server/clientисходный код, мы видим, что сначала он решит, какую стратегию обновления использовать в соответствии с горячей конфигурацией, обновите браузер или код для горячего обновления (HMR), если HMR настроен, вызовитеwebpack/hot/emitter
Отправьте последнее хэш-значение в веб-пакет и вызовите его напрямую, если не настроено горячее обновление модуля.applyReload
внизlocation.reload
метод обновления страницы.
webpack запрашивает последний код модуля на основе хеша
На этом этапе это фактически результат взаимодействия между тремя модулями (три файла, за которыми следует английское имя, соответствующее пути к файлу) в webpack.Первый — это webpack/hot/dev-server (далее именуемый dev -server) Мониторинг веб-пакета третьего шага — сообщение webpackHotUpdate, отправленное dev-сервером/клиентом, вызывает метод проверки в webpack/lib/HotModuleReplacement.runtime (называемый средой выполнения HMR), чтобы определить, есть ли новое обновление. process, webpack/lib/JsonpMainTemplate.runtime ( Для краткости в среде выполнения jsonp есть два метода: hotDownloadUpdateChunk и hotDownloadManifest. Второй метод — вызвать AJAX для запроса обновленных файлов на сервере. Если есть список обновленных файлов для отправки обратно в браузер, первый способ — передать jsonp запрос последнего кода модуля, а затем вернуть код в среду выполнения HMR.Среда выполнения HMR выполнит дальнейшую обработку на основе возвращенного нового кода модуля, которая может заключаться в обновлении страницы. или выполнить горячее обновление модуля.
В этом процессе это фактически результат, полученный после одновременного выполнения трех модулей веб-пакета.
-
webpack/hot/dev-server
Прослушивание сообщений, отправленных клиентомwebpackHotUpdate
Информация
// ....
var hotEmitter = require("./emitter");
hotEmitter.on("webpackHotUpdate", function (currentHash) {
lastHash = currentHash;
if (!upToDate() && module.hot.status() === "idle") {
log("info", "[HMR] Checking for updates on the server...");
check();
}
});
log("info", "[HMR] Waiting for update signal from WDS...");
} else {
throw new Error("[HMR] Hot Module Replacement is disabled.");
-
[HMR runtime/check()](https://github.com/webpack/webpack/blob/v4.41.5/lib/HotModuleReplacement.runtime.js)
Определить, есть ли новое обновление, которое будет использоваться в процессе проверкиwebpack/lib/web/JsonpMainTemplate.runtime.jsсерединаhotDownloadUpdateChunk
(запросите новый код модуля через jsonp и вернитесь в среду выполнения HMR) иhotDownloadManifest
(Отправьте запрос AJAx на сервер, чтобы узнать, есть ли обновленный файл, и если да, он вернет новый файл в браузер)
Получить список файлов обновленийПолучить последний код после обновления модуля
Горячее обновление модуля HMR Runtime
Вот самый важный шаг во всем HMR, и самый важный из них — не что иное, какhotApplyЭтот метод, потому что количество кода слишком много, здесь мы непосредственно вводим анализ процесса (ключевой код), заинтересованные студенты могут прочитать исходный код.
- выяснить
outdatedModules
а такжеoutdatedDependencies
- Удалить устаревшие модули и соответствующие зависимости
// remove module from cache
delete installedModules[moduleId];
// when disposing there is no need to call dispose handler
delete outdatedDependencies[moduleId];
- Новые модули добавляются в модули
for(moduleId in appliedUpdate) {
if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
modules[moduleId] = appliedUpdate[moduleId];
}
}
На этом весь процесс замены модуля завершен, и можно получить самый последний код модуля, теперь очередь бизнес-кода узнать, как изменился модуль~
горячий член в HMR
HotModuleReplaceMentPlugin
Поскольку код JavaScript, который мы пишем, представляет собой модуль без каких-либо правил, то, что может быть экспортировано, является модулем, функцией или даже просто строкой.Для этих модулей без правил Webpack не может предоставить общий модуль. хотите испытать весь процесс разработки HMR, нам нужно вручную решить, как заменить обновленный модуль JS на страницу после обновления модуля JS.Поэтому HotModuleReplacementPlugin предоставляет нам серию API-интерфейсов для HMR, и наиболее важной частью являетсяhot.accept
.
Далее мы попытаемся вручную обработать обновление модуля JS и уведомить браузер о необходимости выполнения соответствующего частичного обновления.
:::Информация Текущие основные среды разработки Vue и React предоставляют унифицированную функцию замены модулей, поэтому проекты Vue и React не требуют ручной обработки кода для HMR, а файлы css также единообразно обрабатываются загрузчиком стилей, поэтому дополнительная обработка не требуется. следующая логика обработки кода полностью основана на чистой нативной разработке. :::
Вернуться к коду, предположим, что текущий файл main.js выглядит следующим образом
// ./src/main.js
import createChild from './child'
const child = createChild()
document.body.appendChild(child)
main.js — это входной файл, упакованный Webpack.Дочерний модуль вводится в файл.Поэтому, когда бизнес-код в дочернем модуле изменяется, webpack неизбежно переупаковывает и повторно использует эти обновленные модули.Поэтому нам нужно добавить main.js Реализовать логику горячей замены для обработки обновлений модулей, от которых она зависит.
С включенным HMR мы можем получить доступ к глобальномуmodule
под объектомhot 成员
это обеспечиваетaccept 方法
, этот метод используется для регистрации того, что делать при обновлении модуля, он принимает два параметра: один — это путь (относительный путь) отслеживаемого модуля, а второй параметр — как действовать при обновлении модуля. По сути, это callback-функция.
// main.js
// 监听 child 模块变化
module.hot.accept('./child', () => {
console.log('老板好,child 模块更新啦~')
})
Когда это будет сделано, повторно запустите npm run serve и измените дочерний модуль. Вы обнаружите, что консоль будет выводить вышеуказанное содержимое консоли. При этом браузер не будет обновляться автоматически. Следовательно, мы можем сделать вывод. при обработке вручную После обновления модуля механизм автоматического обновления не будет активирован.Дальше рассмотрим принцип и как реализовать логику замены JS модуля в HMR.
принцип module.hot.accept
Почему мы просто звонимmoudule.hot.accept
Только тогда можно реализовать горячее обновление, полистайтеисходный кодНа самом деле можно обнаружить, что реализация выглядит следующим образом
// 部分源码
accept: function (dep, callback, errorHandler) {
if (dep === undefined) hot._selfAccepted = true;
else if (typeof dep === "function") hot._selfAccepted = dep;
else if (typeof dep === "object" && dep !== null) {
for (var i = 0; i < dep.length; i++) {
hot._acceptedDependencies[dep[i]] = callback || function () {};
hot._acceptedErrorHandlers[dep[i]] = errorHandler;
}
} else {
hot._acceptedDependencies[dep] = callback || function () {};
hot._acceptedErrorHandlers[dep] = errorHandler;
}
},
// module.hot.accept 其实等价于 module.hot._acceptedDependencies('./child) = render
// 业务逻辑实现
module.hot.accept('./child', () => {
console.log('老板好,child 模块更新啦~')
})
принятьhot._acceptedDependencies
Этот объект хранит обратный вызов частичного обновления.При изменении модуля собираются изменения, которые необходимо внести в модуль._acceptedDependencies
В то же время при изменении содержимого отслеживаемого модуля родительский модуль может пройти_acceptedDependencies
Знайте, что изменилось.
Реализовать замену модуля JS
Когда мы понимаем метод accept, на самом деле нам нужно рассмотреть очень простое, то есть как реализовать бизнес-логику в cb, Фактически, когда метод accept выполняется, последняя модифицированная версия может быть получена в его callback.Функциональное содержимое модуля
// ./src/main.js
import createChild from './child'
console.log(createChild) // 未更新前的函数内容
module.hot.accept('./child', ()=> {
console.log(createChild) // 此时已经可以获取更新以后的函数内容
})
Поскольку можно получить последнее содержимое функции, это на самом деле очень просто.Нам нужно только удалить предыдущий узел dom и заменить его последним узлом dom.В то же время нам также нужно записать статус содержимого в узел. Когда узел заменяется последним После узла , дополнительно обновите исходное состояние содержимого
// ./src/main.js
import createChild from './child'
const child = createChild()
document.body.appendChild(child)
// 这里需要额外注意的是,child 变量每一次都会被移除,所以其实我们一个记录一下每次被修改前的 child
let lastChild = child
module.hot.accept('./child', ()=> {
// 记录状态
const value = lastChild.innerHTML
// 删除节点
document.body.remove(child)
// 创建最新节点
lastChild = createChild()
// 恢复状态
lastChild.innerHTMl = value
// 追加内容
document.body.appendChild(lastChild)
})
На данный момент реализована полная реализация логики замены горячего обновления дочернего модуля вручную Заинтересованные студенты также могут реализовать ее самостоятельно~
:::Советы
Советы: В процессе ручной обработки логики HMR, если во время процесса HMR возникает ошибка и HRM выходит из строя, по сути, вам нужно только добавитьhot: true 修改为 hotOnly: true
Только что
:::
напиши в конце
Я надеюсь, что с помощью этой статьи я смогу помочь вам углубить ваше понимание HMR и в то же время решить проблемы, возникающие в сценариях разработки (например, самостоятельно внедрять горячие обновления модулей вне фреймворка).