Замечательное учебное пособие по Webpack HMR (с анализом исходного кода)

Webpack
Замечательное учебное пособие по Webpack HMR (с анализом исходного кода)

Обзор последних оригинальных статей 😊:

Учебные главы:«Анализ принципов Webpack HMR»
了不起的 Webpack HMR 学习指南.png

1. Введение в HMR

Горячая замена модуля (далее: горячая замена модуля HMR) — очень полезная функция, предоставляемая Webpack,Это позволяет обновлять различные модули во время работы JavaScript, не требуя полного обновления..

Горячая замена модулей (или HMR) — одна из самых полезных функций, предлагаемых webpack, она позволяет обновлять все виды модулей во время выполнения без необходимости полного обновления. --《Горячая замена модуля》

Когда мы изменим код и сохраним его, Webpack переупаковывает код, а HMR заменит, добавит или удалит модули во время работы приложения без перезагрузки всей страницы.
HMR значительно ускоряет разработку следующими способами:

  • Сохранить состояние приложения, потерянное при полной перезагрузке страницы;
  • Обновляйте только изменения, чтобы сэкономить драгоценное время разработки;
  • Настройка стилей выполняется быстрее — почти эквивалентно изменению стилей в отладчике браузера.

требует внимания: HMR не предназначен для производственных сред, а значит, его следует использовать только в средах разработки.

2. Как использовать HMR

Включить функцию HMR в Webpack относительно просто:

1. Способ 1: используйте devServer

1.1 Настройка параметров devServer

просто нужноwebpack.config.jsдобавлено вdevServerпараметры и установитьhotСтоимостьtrueи использоватьHotModuleReplacementPlugin а такжеNamedModulesPlugin(Необязательно) Два плагина:

// 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,   // 启动模块热更新 HMR
+   open: true,  // 开启自动打开浏览器页面
+ },
  plugins: [
+   new webpack.NamedModulesPlugin(),
+   new webpack.HotModuleReplacementPlugin()
  ]
}

1.2 Добавить скрипты

затем вpackage.jsonСредняяscriptsкоманда:

// package.json

{
  // ...
  "scripts": {
+    "start": "webpack-dev-server"
  },
  // ...
}

2. Способ 2. Использовать параметры командной строки

Другой - путем добавления--hotпараметры для реализации. Добавить к--hotПосле параметра devServer скажет Webpack автоматически импортироватьHotModuleReplacementPlugin, не требуя от нас ввода его вручную.
Также часто в паре с--openдля автоматического открытия браузера на странице.
Здесь удалите два плагина, добавленные ранее:

// webpack.config.js

const path = require('path')
const webpack = require('webpack')
module.exports = {
 // ...
- plugins: [
-   new webpack.NamedModulesPlugin(),
-   new webpack.HotModuleReplacementPlugin()
- ]
}

затем изменитьpackage.jsonв файлеscriptsКонфигурация:

// package.json

{
  // ...
  "scripts": {
-    "start": "webpack-dev-server"
+    "start": "webpack-dev-server --hot --open"
  },
  // ...
}

3. Простой пример

На основе приведенной выше конфигурации мы просто реализуем сценарий:index.jsимпортировать файлhello.jsмодуль, когдаhello.jsПри смене модуляindex.jsМодуль будет обновляться.
Код модуля реализован следующим образом:

// hello.js
export default () => 'hi leo!';

// index.js
import hello from './hello.js'
const div = document.createElement('div');
div.innerHTML = hello();

document.body.appendChild(div);

затем вindex.htmlИмпортируйте упакованный файл JS и выполнитеnpm startЗапустите проект:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
</head>
<body>
 <div>了不起的 Webpack HMR 学习指南</div>
 <script src="bundle.js"></script>
</body>
</html>

4. Реализуйте обновления мониторинга

когда мы проходимHotModuleReplacementPluginЕсли в плагине включен HMR, его интерфейс будет доступен глобально.module.hotсвойства ниже. Как правило, вы можете проверить, доступен ли интерфейс, прежде чем начать его использовать.
Например, вы можете сделать этоacceptОбновленный модуль:

if (module.hot) {
  module.hot.accept('./library.js', function() {
    // 使用更新过的 library 模块执行某些操作...
  })
}

оmodule.hotДля получения дополнительных API вы можете просмотреть официальную документацию"API замены горячего модуля".
Возвращаясь к примеру выше, мы тестируем функциональность модуля обновления.
В это время мы модифицируемindex.jsкод для мониторингаhello.jsОбновления в модулях:

import hello from './hello.js';
const div = document.createElement('div');
div.innerHTML = hello();
document.body.appendChild(div);

+ if (module.hot) {
+   module.hot.accept('./hello.js', function() {
+     console.log('现在在更新 hello 模块了~');
+     div.innerHTML = hello();
+   })
+ }

затем изменитьhello.jsСодержимое файла, тестовый эффект:

- export default () => 'hi leo!';
+ export default () => 'hi leo! hello world';

Когда мы сохраняем код, вывод консоли"现在在更新 hello模块了~", а на странице"hi leo!"также обновлено до"hi leo! hello world", что доказывает, что мы прослушиваем обновление файла.
image.png

Это введение в простое использование Webpack HMR Для более подробного ознакомления, пожалуйста, прочитайте официальную документацию.《Горячая замена модуля》.

5. Общие настройки и навыки devServer

5.1 Общая конфигурация

В зависимости от структуры каталоговcontentBase,openPageПараметры должны быть настроены с соответствующими значениями, иначе среда выполнения не должна сразу получить доступ к вашей домашней странице. Также обратите внимание на свойpublicPath, следует подумать о пути, сгенерированном после упаковки статических ресурсов, в зависимости от вашей структуры каталогов.

devServer: {
  contentBase: path.join(__dirname, 'static'),    // 告诉服务器从哪里提供内容(默认当前工作目录)
  openPage: 'views/index.html',  // 指定默认启动浏览器时打开的页面
  index: 'views/index.html',  // 指定首页位置
  watchContentBase: true, // contentBase下文件变动将reload页面(默认false)
  host: 'localhost', // 默认localhost,想外部可访问用'0.0.0.0'
  port: 8080, // 默认8080
  inline: true, // 可以监控js变化
  hot: true, // 热启动
  open: true, // 启动时自动打开浏览器(指定打开chrome,open: 'Google Chrome')
  compress: true, // 一切服务都启用gzip 压缩
  disableHostCheck: true, // true:不进行host检查
  quiet: false,
  https: false,
  clientLogLevel: 'none',
  stats: { // 设置控制台的提示信息
    chunks: false,
    children: false,
    modules: false,
    entrypoints: false, // 是否输出入口信息
    warnings: false,
    performance: false, // 是否输出webpack建议(如文件体积大小)
  },
  historyApiFallback: {
    disableDotRule: true,
  },
  watchOptions: {
    ignored: /node_modules/, // 略过node_modules目录
  },
  proxy: { // 接口代理(这段配置更推荐:写到package.json,再引入到这里)
    "/api-dev": {
      "target": "http://api.test.xxx.com",
      "secure": false,
      "changeOrigin": true,
      "pathRewrite": { // 将url上的某段重写(例如此处是将 api-dev 替换成了空)
        "^/api-dev": ""
      }
    }
  },
  before(app) { },
}

5.2 Совет 1: Файловая форма для вывода кода DEV-сервера

Вывод кода с помощью Dev-Server обычно находится в памяти, но он также может быть записан на жесткий диск, создавая физические файлы:

devServer:{
  writeToDisk: true,
}

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

5.3 Совет 2. Запустите службу с локальным IP-адресом по умолчанию

Иногда при запуске службы вы хотите использовать локальный IP-адрес для открытия по умолчанию:

devServer:{
  disableHostCheck: true, // true:不进行host检查
  // useLocalIp: true, // 建议不在这里配置
  // host: '0.0.0.0', // 建议不在这里配置
}

Вам также необходимо настроить хост как0.0.0.0, эту конфигурацию рекомендуется дописывать к команде scripts, а не жестко прописывать в конфигурации, иначе не хочется в будущем возвращаться и кидать таким образом, воспользуйтесь уловкой и добавьте новую команду :

"dev-ip": "yarn run dev --host 0.0.0.0 --useLocalIp"

5.4 Совет 3. Укажите имя домена отладки для запуска

Иногда при запуске желательно указать имя домена отладки, например:local.test.baidu.com:

devServer:{
  open: true,
  public: 'local.test.baidu.com:8080', // 需要带上端口
  port: 8080,
}

В то же время необходимо127.0.0.1Измените на указанный хост, вы можете использовать такие инструменты, как iHost, для изменения, каждый инструмент похож, формат следующий:

127.0.0.1 local.test.baidu.com

Он автоматически откроется после запуска службыlocal.test.baidu.com:8080посещение

5.5 Совет 4. Включите сжатие gzip

devServer:{
  compress: true,
}

3. Введение в основные принципы HMR

Из предыдущего введения мы знаем, что основная функция HMR состоит в том, чтобыЗамена, добавление или удаление модулей во время работы приложения без перезагрузки всей страницы.
Итак, как изменения файлов, генерируемые скомпилированным исходным кодом Webpack во время компиляции, и реализация заменяющего модуля во время выполнения, как они связаны?

Ответив на эти два вопроса, давайте кратко рассмотрим основной рабочий процесс HMR (упрощенная версия):

HMR 工作流程图.png
Диаграмма рабочего процесса HMR.png

Затем запустите анализ рабочего процесса HMR:

  1. Когда Webpack (Watchman) прослушивает изменения в коде файла/модуля в проекте, он уведомляет инструмент сборки (Packager) в Webpack об изменениях, а именно подключаемый модуль HMR;
  2. После обработки плагином HMR результат отправляется в среду выполнения (HMR Runtime) приложения (Application);
  3. Наконец, измененные файлы/модули обновляются (добавляются/удаляются или заменяются) в системе модулей с помощью среды выполнения HMR.

Среди них среда выполнения HMR внедряется инструментом сборки во время компиляции.Файлы во время компиляции сопоставляются с модулями во время выполнения через унифицированный идентификатор модуля, а ряд API-интерфейсов предоставляется извне для фреймворков прикладного уровня (таких как React) для вызов.

💖Уведомление💖: Рекомендуется сначала понять общий процесс на изображении выше, а затем прочитать его позже. Не волнуйся, я жду тебя~😃

В-четвертых, полный принцип HMR и анализ исходного кода

Из содержания предыдущего раздела мы, вероятно, знаем простой рабочий процесс HMR, поэтому, возможно, у вас все еще может быть много сомнений: что такое уведомление об обновлении файла для плагина HMR? Как подключаемый модуль HMR отправляет обновления в среду выполнения HMR? И так далее.

Затем приступаем к подробному анализу всего процесса горячего обновления модуля HMR в сочетании с исходным кодом.Прежде всего, давайте сначала посмотрим на блок-схему.Вы не можете понять название метода на рисунке (красный шрифт и желтый фон цветная часть):

Webpack HMR.png
Webpack HMR.png

На рисунке выше показан полный рабочий процесс HMR из нашей модификации кода до завершения горячего обновления модуля. Процесс был отмечен красными арабскими цифрами на рисунке.

Чтобы понять, как работает вышеизложенное, давайте сначала разберемся с концепциями имен на рисунке:

  • Webpack-dev-server: подключаемый модуль сервера, эквивалентный экспресс-серверу, запускает веб-службу, применимую только к среде разработки;
  • Webpack-dev-middleware :ОдинWebpack-dev-serverРоль промежуточного программного обеспечения можно обобщить следующим образом: в режиме наблюдения отслеживать изменения ресурсов, а затем автоматически упаковывать.
  • Webpack-hot-middleware: в сочетании с промежуточным программным обеспечением, используемым Webpack-dev-middleware, он может реализовать обновление браузера без обновления, то есть HMR;

Давайте вместе изучим весь принцип работы HMR:

1. Отслеживайте изменения кода, перекомпилируйте и упакуйте

Во-первых, в соответствии с конфигурацией devServer используйтеnpm startЗапустит Webpack-dev-server, чтобы запустить локальный сервер и войти в режим просмотра Webpack, затем инициализирует Webpack-dev-middleware в Webpack-dev-middleware, вызвавstartWatch()способ просмотра файловой системы:

// webpack-dev-server\bin\webpack-dev-server.js
// 1.启动本地服务器 Line 386
server = new Server(compiler, options);

// webpack-dev-server\lib\Server.js
// 2.初始化 Webpack-dev-middleware Line 109
this.middleware = webpackDevMiddleware(compiler, Object.assign({}, options, wdmOptions));

// webpack-dev-middleware\lib\Shared.js
// 3.开始 watch 文件系统 Line 171
startWatch: function() {
 //...
 // start watching
 if(!options.lazy) {
  var watching = compiler.watch(options.watchOptions, share.handleCompilerCallback);
  context.watching = watching;
 }
 //...
}
share.startWatch();
// ...

когдаstartWatch()После выполнения метод перейдет в режим наблюдения, если будет обнаружено, что код в файле был изменен, согласно конфигурационному файлуПерекомпилируйте и упакуйте модуль.

2. Сохраните результат компиляции

Webpack взаимодействует с Webpack-dev-middleware, Webpack-dev-middleware вызывает API Webpack для отслеживания изменений кода и уведомляет Webpack о передаче перекомпилированного кода через объекты JavaScript.хранится в памяти.

Мы найдем это вoutput.pathУказанныйdistКаталог не сохраняет файлы результатов компиляции, почему так?

На самом деле, Webpack хранит результаты компиляции в памяти, т.к.Доступ к коду в памяти быстрее, чем доступ к файлам в файловой системе., что снижает накладные расходы на запись кода в файл.

Webpack может сохранять код в памяти благодаря Webpack-dev-middlewarememory-fsзависимая библиотека, она будетoutputFileSystemзаменяетсяMemoryFileSystem, код реализации выводится в память. Часть исходного кода выглядит следующим образом:

// webpack-dev-middleware\lib\Shared.js Line 108

// store our files in memory
var fs;
var isMemoryFs = !compiler.compilers && 
    compiler.outputFileSystem instanceof MemoryFileSystem;
if(isMemoryFs) {
 fs = compiler.outputFileSystem;
} else {
 fs = compiler.outputFileSystem = new MemoryFileSystem();
}
context.fs = fs;

Вышеуказанный код первых судейfileSystemтак или иначеMemoryFileSystemнапример, если нет, используйтеMemoryFileSystemПример замены компилятора передoutputFileSystem. Таким образом, код файла bundle.js сохраняется в памяти как простой объект JavaScript.Когда браузер запрашивает файл bundle.js, devServer напрямую находит сохраненный выше объект JavaScript в памяти и возвращает его браузеру.

3. Отслеживайте изменения файлов и обновляйте браузер.

Webpack-dev-server начинает отслеживать изменения файлов.В отличие от шага 1, это не мониторинг изменений кода, а также повторная компиляция и упаковка.
Когда мы настраиваем в файле конфигурацииdevServer.watchContentBase дляtrue, Webpack-dev-server будет прослушивать изменения в статических файлах в папке конфигурации и, когда изменения происходят, уведомлять браузер о выполненииОбновление браузера, что не то же самое, что HMR.

// webpack-dev-server\lib\Server.js
// 1. 读取参数 Line 385
if (options.watchContentBase) { defaultFeatures.push('watchContentBase'); }

// 2. 定义 _watch 方法 Line 697
Server.prototype._watch = function (watchPath) {
 // ...
  const watcher = chokidar.watch(watchPath, options).on('change', () => {
    this.sockWrite(this.sockets, 'content-changed');
  });

  this.contentBaseWatchers.push(watcher);
};

// 3. 执行 _watch() 监听文件变化 Line 339
watchContentBase: () => {
    if (/^(https?:)?\/\//.test(contentBase) || typeof contentBase === 'number') {
        throw new Error('Watching remote files is not supported.');
    } else if (Array.isArray(contentBase)) {
        contentBase.forEach((item) => {
            this._watch(item);
        });
    } else {
        this._watch(contentBase);
    }
}

4. Установить WS и синхронизировать состояние этапа компиляции

Весь этот шаг выполняется на Webpack-dev-сервере, в основном черезsockjs(зависимость Webpack-dev-server), между стороной браузера (Client) и стороной сервера (Webpack-dev-middleware) Webpack-dev-serverУстановите длинное соединение WebSocket.

Затем синхронизируйте информацию о состоянии каждого этапа компиляции и упаковки Webpack с браузером. Есть два важных шага:

  • отправить статус

Webpack-dev-server прослушивает компиляцию через Webpack APIdoneСобытие, когда компиляция завершена, Webpack-dev-server проходит_sendStatsМетод отправляет хеш-значение скомпилированного нового модуля в браузер с помощью сокета.

  • сохранить состояние

Браузер будет_sendStatsприслалиhashсохрани это,Будет использоваться после горячего обновления модуля.image.png

// webpack-dev-server\lib\Server.js

// 1. 定义 _sendStats 方法 Line 685
// send stats to a socket or multiple sockets
Server.prototype._sendStats = function (sockets, stats, force) {
  //...
  this.sockWrite(sockets, 'hash', stats.hash);
};

// 2. 监听 done 事件 Line 86
compiler.plugin('done', (stats) => {
   // 将最新打包文件的 hash 值(stats.hash)作为参数传入 _sendStats()
    this._sendStats(this.sockets, stats.toJson(clientStats));
    this._stats = stats;
});

// webpack-dev-server\client\index.js
// 3. 保存 hash 值 Line 74
var onSocketMsg = {
  // ...
  hash: function hash(_hash) {
    currentHash = _hash;
  },
  // ...
}
socket(socketUrl, onSocketMsg);

5. Опубликуйте сообщение на стороне браузера

когдаhashПосле отправки сообщения сокет также отправит сообщениеokСообщение сообщает Webpack-dev-серверу, что, поскольку клиент (Клиент) не запрашивает код горячего обновления и не выполняет операции с модулем горячего обновления, он передается черезemit Один"webpackHotUpdate"сообщение, передайте работу Webpack.

// webpack-dev-server\client\index.js
// 1. 处理 ok 消息 Line 135
var onSocketMsg = {
  // ...
  ok: function ok() {
      sendMsg('Ok');
      if (useWarningOverlay || useErrorOverlay) overlay.clear();
      if (initial) return initial = false; // eslint-disable-line no-return-assign
      reloadApp();
  },
  // ...
}

// 2. 处理刷新 APP Line 218
function reloadApp() {
  // ...
  if (_hot) {
    // 动态加载 emitter
    var hotEmitter = require('webpack/hot/emitter');
    hotEmitter.emit('webpackHotUpdate', currentHash);
    if (typeof self !== 'undefined' && self.window) {
      // broadcast update to window
      self.postMessage('webpackHotUpdate' + currentHash, '*');
    }
  }
  // ...
}

6. Передайте хэш в HMR

Webpack/hot/dev-server слушает сторону браузераwebpackHotUpdateсообщение, передайте новое хэш-значение модуля в HotModuleReplacement.runtime клиентского основного концентратора HMR и вызовитеcheckспособ обнаружения обновлений,Определите, является ли это обновлением браузера или горячим обновлением модуля. Если браузер обновится, следующего шага не будет~~

// webpack\hot\dev-server.js
// 1.监听 webpackHotUpdate Line 42
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();
    }
});

var check = function check() {
    module.hot.check(true).then(function(updatedModules) {
        if(!updatedModules) {
            // ...
      window.location.reload();// 浏览器刷新
            return;
        }
        if(!upToDate()) {
            check();
        }
    }).catch(function(err) { /*...*/});
};

// webpack\lib\HotModuleReplacement.runtime.js
// 3.调用 HotModuleReplacement.runtime 定义的 check 方法 Line 167
function hotCheck(apply) {
    if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
    hotApplyOnUpdate = apply;
    hotSetStatus("check");
    return hotDownloadManifest(hotRequestTimeout).then(function(update) {
    //...
    });
}

7. Определите, есть ли обновление

Когда вызывается HotModuleReplacement.runtimecheckметод, он вызовет JsonpMainTemplate.runtime вhotDownloadUpdateChunk(получить последний код модуля) иhotDownloadManifest(Получите, есть ли файл обновления) Два метода, исходный код этих двух методов будет расширен на следующем шаге.

// webpack\lib\HotModuleReplacement.runtime.js
// 1.调用 HotModuleReplacement.runtime 定义 hotDownloadUpdateChunk 方法 Line 171
function hotCheck(apply) {
    if(hotStatus !== "idle") throw new Error("check() is only allowed in idle status");
    hotApplyOnUpdate = apply;
    hotSetStatus("check");
    return hotDownloadManifest(hotRequestTimeout).then(function(update) {
    //...
        {
          // hotEnsureUpdateChunk 方法中会调用 hotDownloadUpdateChunk
          hotEnsureUpdateChunk(chunkId);
        }
    });
}

вhotEnsureUpdateChunkметод будет вызыватьсяhotDownloadUpdateChunk:

// webpack\lib\HotModuleReplacement.runtime.js Line 215
 function hotEnsureUpdateChunk(chunkId) {
  if(!hotAvailableFilesMap[chunkId]) {
   hotWaitingFilesMap[chunkId] = true;
  } else {
   hotRequestedFilesMap[chunkId] = true;
   hotWaitingFiles++;
   hotDownloadUpdateChunk(chunkId);
  }
 }

8. Запрос на обновление последнего списка файлов

вызовcheckметод, он сначала вызовет JsonpMainTemplate.runtime вhotDownloadManifestметод, через серверИнициировать запрос AJAX для получения файла обновления, если будетmainfestВернитесь на сторону браузера.image.pngВот несколько оригинальныхXMLHttpRequest, не все выложили~

// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest 定义 Line 22
function hotDownloadManifest(requestTimeout) {
    return new Promise(function(resolve, reject) {
        try {
            var request = new XMLHttpRequest();
            var requestPath = $require$.p + $hotMainFilename$;
            request.open("GET", requestPath, true);
            request.timeout = requestTimeout;
            request.send(null);
        } catch(err) {
            return reject(err);
        }
        request.onreadystatechange = function() {
            // ...
        };
    });
}

9. Запрос на обновление последнего кода модуля

существуетhotDownloadManifestметод также будет выполнятьсяhotDownloadUpdateChunkметод,Запросить последний код модуля через JSONPи вернуть код среде выполнения HMR.image.png

Затем новый код обрабатывается средой выполнения HMR,Определите, является ли это обновлением браузера или горячим обновлением модуля.

// webpack\lib\JsonpMainTemplate.runtime.js
// hotDownloadManifest 定义 Line 12
function hotDownloadUpdateChunk(chunkId) {
  // 创建 script 标签,发起 JSONP 请求
    var head = document.getElementsByTagName("head")[0];
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.charset = "utf-8";
    script.src = $require$.p + $hotChunkFilename$;
    $crossOriginLoading$;
    head.appendChild(script);
}

10. Обновить модуль и ссылки на зависимости

Этот шаг является основным во всем горячем обновлении модуля (HMR) через среду выполнения HMR.hotApplyметод, удалите устаревшие модули и коды и добавьте новые модули и коды для обеспечения горячего обновления.

отhotApplyИз метода видно, что горячая замена модуля в основном делится на три этапа:

  1. найти устаревшие модулиoutdatedModulesи просроченные зависимостиoutdatedDependencies;
// webpack\lib\HotModuleReplacement.runtime.js
// 找出 outdatedModules 和 outdatedDependencies Line 342
function hotApply() { 
  // ...
  var outdatedDependencies = {};
  var outdatedModules = [];
  function getAffectedStuff(updateModuleId) {
    var outdatedModules = [updateModuleId];
    var outdatedDependencies = {};
    // ...
    return {
        type: "accepted",
        moduleId: updateModuleId,
        outdatedModules: outdatedModules,
        outdatedDependencies: outdatedDependencies
    };
 };
  function addAllToSet(a, b) {
      for (var i = 0; i < b.length; i++) {
          var item = b[i];
          if (a.indexOf(item) < 0)
              a.push(item);
      }
  }
  for(var id in hotUpdate) {
      if(Object.prototype.hasOwnProperty.call(hotUpdate, id)) {
          // ... 省略多余代码
          if(hotUpdate[id]) {
              result = getAffectedStuff(moduleId);
          }
          if(doApply) {
              for(moduleId in result.outdatedDependencies) {
                 // 添加到 outdatedDependencies
                  addAllToSet(outdatedDependencies[moduleId], result.outdatedDependencies[moduleId]);
              }
          }
          if(doDispose) {
              // 添加到 outdatedModules
              addAllToSet(outdatedModules, [result.moduleId]);
              appliedUpdate[moduleId] = warnUnexpectedRequire;
          }
      }
  }
}
  1. Удалить ссылки на просроченные модули, зависимости и все дочерние элементы из кеша;
// webpack\lib\HotModuleReplacement.runtime.js
// 从缓存中删除过期模块、依赖和所有子元素的引用 Line 442
function hotApply() {
   // ...
    var idx;
    var queue = outdatedModules.slice();
    while(queue.length > 0) {
        moduleId = queue.pop();
        module = installedModules[moduleId];
        // ...
        // 移除缓存中的模块
        delete installedModules[moduleId];
        // 移除过期依赖中不需要使用的处理方法
        delete outdatedDependencies[moduleId];
        // 移除所有子元素的引用
        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);
            }
        }
    } 
  // 从模块子组件中删除过时的依赖项
  var dependency;
  var moduleOutdatedDependencies;
  for(moduleId in outdatedDependencies) {
   if(Object.prototype.hasOwnProperty.call(outdatedDependencies, moduleId)) {
    module = installedModules[moduleId];
    if(module) {
     moduleOutdatedDependencies = outdatedDependencies[moduleId];
     for(j = 0; j < moduleOutdatedDependencies.length; j++) {
      dependency = moduleOutdatedDependencies[j];
      idx = module.children.indexOf(dependency);
      if(idx >= 0) module.children.splice(idx, 1);
     }
    }
   }
  }
}
  1. Добавьте новый код модуля в модули, когда следующий вызов__webpack_require__(переписано вебпакомrequiremethod) получен новый код модуля.
// webpack\lib\HotModuleReplacement.runtime.js
// 将新模块代码添加到 modules 中 Line 501
function hotApply() {
   // ...
    for(moduleId in appliedUpdate) {
        if(Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
            modules[moduleId] = appliedUpdate[moduleId];
        }
    }
}

hotApplyПосле выполнения метода новый код заменил старый код, но наш бизнес-код не знает этих изменений, поэтому нам нужно передатьacceptНа прикладном уровне уведомления о событиях используется новый модуль для «частичного обновления», который мы используем в нашем бизнесе следующим образом:


if (module.hot) {
  module.hot.accept('./library.js', function() {
    // 使用更新过的 library 模块执行某些操作...
  })
}

11. Обработка ошибок горячего обновления

Во время горячего обновленияhotApplyможет возникнуть в процессеabortилиfailЕсли это не так, горячее обновление возвращается к перезагрузке браузера, и горячее обновление всего модуля завершается.

// webpack\hot\dev-server.js Line 13
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();
    }
});

V. Резюме

В этой статье в основном рассказывается об использовании HMR и принципах реализации Webpack и анализа исходного кода. В анализе исходного кода диаграмма «Анализ принципа работы Webpack HMR» позволяет вам понять весь рабочий процесс HMR. Сам HMR содержит много исходного кода. Многие детали не полностью описаны в этой статье, и читатели должны сами медленно читать и понимать исходный код.

Справочная статья

1. Официальная документация《Горячая замена модуля》
2.«Анализ принципов Webpack HMR»
3."веб-пакет HMR" 
4."Настройка dev-сервера"