Как однажды саркастически заметил мой коллега, было время, когда JavaScript просто работал в браузерах, а теперь это, по сути, компилируемый язык. Не поймите меня неправильно, такие инструменты, как webpack, rollup или parcel, принесли огромные преимущества современной фронтенд-разработке. Несмотря на то, что к настоящему времени браузеры имеют поддержку собственных модулей, вы все равно должны объединить свой код для производства. А как же развитие? Что ж, упаковщики JavaScript также улучшили там несколько вещей: одно улучшение, которое мне особенно нравится, - это горячая перезагрузка. На самом деле, мне это так нравится, что я решил посмотреть, можно ли реализовать что-то подобное, используя собственные модули ES2015.
Для тех, кто не знаком, горячая перезагрузка - это возможность заменять обновленные модули без перезагрузки всей страницы. Обычно вся система состоит из:
- Наблюдатель за файлами
- Соединение WebSocket или EventSource, которое уведомляет клиентов об изменениях файлов.
- Код времени выполнения, обрабатывающий горячие обновления.
Вы можете найти более подробную информацию о внутреннем устройстве и общедоступном API этой функции в документации webpack или parcel. В этой статье я приведу небольшой пример:
Мы импортируем переменную text
из модуля text.mjs
и помещаем ее в документ body
. Затем мы подписываемся на text.mjs
обновления и синхронизируем основной текст с содержимым этого модуля.
Теперь мы попробуем реализовать эту функциональность для нативных модулей ES2015, не связывая их.
Однажды WHATWG Loader Spec будет завершена и, надеюсь, предоставит все необходимые API-интерфейсы для простой реализации горячей замены модуля. Но даже сегодня собственные модули JavaScript имеют несколько интересных свойств, которые в некоторых случаях делают это возможным. Во-первых, импорт - это живые привязки:
В этом примере каждый раз, когда мы меняем counter
, все другие зависимые от него модули получают новое значение, как если бы функции counter
и increment
были определены в одном и том же модуле.
Второе свойство, важное для части перезагрузки: имена модулей являются URL-адресами, а разные URL-адреса считаются разными модулями, даже если в конце они разрешаются в один и тот же файл. Чтобы продемонстрировать это, давайте немного изменим предыдущий пример:
Поскольку counter
теперь импортируется с другого URL-адреса, вызов increment
не меняет его. counter.mjs
и counter.mjs?someQueryParameter
- два независимых модуля. Если мы посмотрим на вкладку сети в инструментах разработчика браузера, мы также увидим их как два разных запроса:
Зная это, мы можем реализовать сервер, который сможет перезагружать модуль ES2015 с некоторыми ограничениями (см. Ниже). Это будет работать следующим образом:
- На сервере будет запущено средство отслеживания файлов и конечная точка WebSocket. Каждый раз, когда модуль изменяется, он будет транслировать измененное имя модуля и время модификации всем подключенным клиентам.
- Во внешнем интерфейсе мы реализуем небольшой клиент WebSocket, который позволит нам подписываться на обновления определенного модуля.
- Под
module.mjs?mtime=<anything>
route мы будем обслуживать исходный модуль. - В разделе
module.mjs
вместо исходного кода мы сгенерируем и будем обслуживать модуль прокси с возможностью самообновления:
- Мы импортируем каждое экспортированное имя из исходного модуля.
- Мы назначаем наш импорт локальным переменным модуля. Мы используем изменяемые
let
привязки, чтобы потом их можно было переназначить. - Мы экспортируем локальные переменные модуля под оригинальными именами.
- Мы подписываемся на уведомления об обновлениях исходного модуля.
- Каждый раз, когда мы получаем обновление, мы повторно импортируем модуль с новым параметром запроса
mtime
. Из-за семантики URL-адреса браузер загрузит обновленный модуль. - Мы обновляем локальные переменные модуля до значений, полученных после шага 5.
Этот прокси-модуль является основной идеей этого метода. Поскольку он имеет тот же экспорт и обслуживается по одному и тому же URL-адресу, он может прозрачно заменять оригинал. Из-за того, что модули ES2015 имеют живую привязку, все обновления экспорта прокси-модуля будут автоматически получены каждым потребителем.
У этого подхода также есть два ограничения:
- Мы не сможем обновить модуль, если изменились названия экспорта или их количество. В этом случае мы вернемся к полной перезагрузке страницы.
- Если исходный модуль экспортирует изменяемые привязки, как в нашем примере
counter
, мы не можем сгенерировать прокси-модуль: поскольку мы переназначаем все экспорты на разные переменные, они не могут обновляться должным образом, как в исходном модуле. Поэтому мы не генерируем прокси для таких модулей и прибегаем к полной перезагрузке страницы, если они меняются.
Есть также несколько вещей, которые я упустил из этой статьи: в последней демонстрации есть API для реакции на перезагрузку модуля (accept
и dispose
callbacks), а уведомления об обновлении распространяются на родительский модуль, пока они не будут обработаны.
Вы можете найти исходный код и демонстрацию в моем репозитории GitHub или попробовать это на своем собственном коде с помощью этого пакета npm. Дайте мне знать, что вы думаете. Есть ли у этого или подобного подхода перспективы?