Принципиальный анализ Webpack HMR

сервер JavaScript браузер Webpack
Принципиальный анализ Webpack HMR

Горячая замена модуля (сокращенно 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, я подумал, что это потрясающе, и у меня всегда возникали вопросы.

  1. Webpack может упаковывать различные модули в файлы пакетов или несколько файлов блоков, но когда я разрабатываю через webpack HMR, я не нахожу файлы, упакованные с помощью webpack, в моем каталоге dist Куда они идут?
  2. просмотревwebpack-dev-serverфайл package.json, который, как мы знаем, зависит отwebpack-dev-middlewareбиблиотеку, так какую роль играет промежуточное ПО webpack-dev в процессе HMR?
  3. В процессе использования HMR через инструменты разработчика Chrome я знаю, что браузер взаимодействует с webpack-dev-server через websocket, но в сообщении websocket не найден новый код модуля. Как упакованный новый модуль отправляется в браузер? Почему новый модуль не отправляется в браузер с сообщением через websocket?
  4. Когда браузер получает последний код модуля, как HMR заменяет старый модуль новым и как он обрабатывает зависимости между модулями в процессе замены?
  5. Когда модуль заменяется в горячем режиме, если модуль не может быть заменен, существует ли резервный механизм?

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

Рисунок 1: Схема рабочего процесса HMR

На приведенном выше рисунке представлена ​​блок-схема горячего обновления модуля для веб-пакета и веб-сервера для разработки приложений.

  • Красное поле в нижней части рисунка выше — это сторона сервера, а оранжевое поле вверху — сторона браузера.
  • Зеленое поле — это область, контролируемая кодом веб-пакета. Синее поле — это область, контролируемая кодом webpack-dev-server, пурпурное поле — это файловая система, в которой происходят изменения файлов, а голубое поле — это само приложение.

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

  1. Первый шаг, в режиме просмотра веб-пакета, когда файл в файловой системе изменяется, веб-пакет отслеживает изменение файла, перекомпилирует и упаковывает модуль в соответствии с файлом конфигурации и сохраняет упакованный код в памяти через простой объект JavaScript. .
  2. Вторым шагом является интерфейсное взаимодействие между webpack-dev-server и webpack, и на этом этапе, в основном взаимодействие между промежуточным ПО dev-server webpack-dev-middleware и webpack, webpack-dev-middleware вызывает webpack для раскрытия кода. изменяется и указывает webpack упаковать код в память.
  3. Третий шаг — это мониторинг изменений файлов с помощью webpack-dev-server, этот шаг отличается от первого тем, что он не отслеживает изменения кода и не перепаковывает их. Когда мы настраиваем в файле конфигурацииdevServer.watchContentBaseКогда правда, Сервер Он будет отслеживать изменения статических файлов в этих папках конфигурации и уведомлять браузер о необходимости выполнить перезагрузку приложения в режиме реального времени после изменения. Обратите внимание, что здесь обновление браузера и HMR — это два разных понятия.
  4. Четвертый шаг — это также работа кода webpack-dev-server, которая в основном выполняетсяsockjs(зависимость от webpack-dev-server) Установить длинное соединение по вебсокету между браузером и сервером, сообщить браузеру информацию о статусе каждого этапа компиляции и упаковки вебпака, а также включить мониторинг статических файлов сервером на третьем шаге. изменение информации. Сторона браузера в соответствии с этими сообщения сокетов выполняют различные операции. Разумеется, самой важной информацией, передаваемой сервером, является хеш-значение нового модуля, и последующие шаги будут выполнять горячую замену модуля на основе этого хэш-значения.
  5. Сторона webpack-dev-server/client не может ни запрашивать обновленный код, ни выполнять операции горячего модуля и передавать эти задачи webpack.Работа webpack/hot/dev-server основана на webpack-dev. -server/client и конфигурация dev-server определяют, следует ли обновлять браузер или выполнять горячее обновление модуля. Конечно, если вы просто обновите браузер, дальнейших шагов не будет.
  6. HotModuleReplacement.runtime является хабом HMR клиента, получает хеш-значение нового модуля, переданного ему на предыдущем шаге, отправляет Ajax-запрос на сервер через JsonpMainTemplate.runtime, и сервер возвращает json, содержащий все обновления для обновления.После получения обновленного списка хэш-значения модуля модуль снова запрашивает последний код модуля через jsonp. Это шаги 7, 8 и 9 на изображении выше.
  7. 10-й шаг является ключевым шагом для определения успеха HMR.На этом этапе HotModulePlugin сравнит старый и новый модули, чтобы решить, следует ли обновлять модуль.Приняв решение об обновлении модуля, проверьте зависимости между модулями и обновите module Также обновите зависимости между модулями.
  8. На последнем шаге, когда 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 будет хорошим выбором, и я надеюсь, что эта статья поможет вам прочитать исходный код, который моя настоящая цель письма.