#инструменты #JS #Лаборатория_веб_платформы
Представьте, что вы начинаете разрабатывать JS-приложение. При разработке вы используете локальный сервер, запускаете проект, далее собирается JS-бандл и приложение открывается в браузере. В вашем проекте пока что немного модулей, и приложение «собирается» быстро. Правда в процессе разработки вам приходится полностью обновлять страницу, чтобы увидеть, как работают ваши написанные скрипты, и состояние приложения теряется при перезагрузке. Со временем также модулей в приложении становится много и полная «пересборка» теперь длится долго.
Решение этой проблемы — инкрементальная пересборка. То есть билд только тех модулей, которые вы только что изменили, вместо пересборки бандла всего приложения целиком.
Ок, сделали. Перебилд теперь проихоходит быстрее, но страницу всё ещё приходится полностью перезагружать для обновления. Теперь задача в том, чтобы точечно обновить загруженный код в вашем браузере с запущенным сервером. Как это сделать «на горячую», без перезагрузки всей страницы целиком? Для этого придуман подход Hot Module Replacement (HMR).
HMR — это способ точечного обновления в браузере только изменившихся JS-модулей приложения вместо полной перезагрузки страницы. Дальше я рассмотрю, как HMR реализован в сборщиках на нативных ES-модулях — например, в Snowpack или Vite.
В HMR-движке на нативных ES-модулях есть две части: серверная и клиентская.
На сервере из всех модулей и их зависимостей (import и export) строится дерево: в корне — «главный» модуль (что-то типа app.js), от него расходится древовидная цепочка подключаемых JS-модулей, от которых модуль зависит.
Также на сервере стороне подготавливаются все файлы модулей и запускается WebSocket Server, который будет ждать сообщения от клиента, с запросом на пересборку дерева или на перезагрузку страницы целиком.
Клиент подключается к серверу по WebSocket. Когда изменяется один из файлов JS-модулей, от клиента на сервер отправляется сообщение, какой именно модуль изменился.
Когда сервер получает сообщение от клиента, он начинает анализировать зависимости этого модуля. Сервер идёт вверх по цепочке родительских зависимостей и инвалидирует их до тех пор, пока не дойдёт до «граничного» модуля — это последний элемент в цепочке, который явно помечен как принимающий HMR-обновления. Именно этот файл и все его зависимости сервер «перебилдит», а клиент запросит на обновление. В случае, если этим «граничным» модулем окажется корневой модуль app.js, то приложение перезагрузится целиком.
Так как в движке используется механизм нативных ES-модулей найти «граничный» родительский модуль и заменить его — достаточно. Все дочерние зависимости модуля уже обработаются и загрузятся автоматически самим браузером.
Итоговый найденный «граничный» модуль теперь нужно «подменить», а затем перестроить дерево зависимостей (убрать старые связи и добавить новые), ведь во внесённых изменениях могли быть подключены новые модули.
Как же именно «подменяется» модуль со старого на новый?
В случае, если используются нативные ES-модули, выполняется динамический
import интересующего модуля. А если преварительно изменившиеся файлы ещё нужно «сбилдить», то перед импортом ещё выполняется этот этап билда.Чтобы импортировать свежий файл (не кешированную браузером версию), то к импорту можно добавить уникальную метку, например, текущий timestamp:
import(muduleName + `?time=${updateID}`)
Остаётся решить вопрос, как при такой «подмене» передать в новый модуль состояние из старого модуля.
Для этого на колбэке динамического импорта нового модуля нужно забрать интересущие данные из старого модуля и записать данные в новый.
Продолжение 👇