Как однажды саркастически заметил мой коллега, было время, когда 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 вместо исходного кода мы сгенерируем и будем обслуживать модуль прокси с возможностью самообновления:
  1. Мы импортируем каждое экспортированное имя из исходного модуля.
  2. Мы назначаем наш импорт локальным переменным модуля. Мы используем изменяемые let привязки, чтобы потом их можно было переназначить.
  3. Мы экспортируем локальные переменные модуля под оригинальными именами.
  4. Мы подписываемся на уведомления об обновлениях исходного модуля.
  5. Каждый раз, когда мы получаем обновление, мы повторно импортируем модуль с новым параметром запроса mtime. Из-за семантики URL-адреса браузер загрузит обновленный модуль.
  6. Мы обновляем локальные переменные модуля до значений, полученных после шага 5.

Этот прокси-модуль является основной идеей этого метода. Поскольку он имеет тот же экспорт и обслуживается по одному и тому же URL-адресу, он может прозрачно заменять оригинал. Из-за того, что модули ES2015 имеют живую привязку, все обновления экспорта прокси-модуля будут автоматически получены каждым потребителем.

У этого подхода также есть два ограничения:

  1. Мы не сможем обновить модуль, если изменились названия экспорта или их количество. В этом случае мы вернемся к полной перезагрузке страницы.
  2. Если исходный модуль экспортирует изменяемые привязки, как в нашем примере counter, мы не можем сгенерировать прокси-модуль: поскольку мы переназначаем все экспорты на разные переменные, они не могут обновляться должным образом, как в исходном модуле. Поэтому мы не генерируем прокси для таких модулей и прибегаем к полной перезагрузке страницы, если они меняются.

Есть также несколько вещей, которые я упустил из этой статьи: в последней демонстрации есть API для реакции на перезагрузку модуля (accept и dispose callbacks), а уведомления об обновлении распространяются на родительский модуль, пока они не будут обработаны.

Вы можете найти исходный код и демонстрацию в моем репозитории GitHub или попробовать это на своем собственном коде с помощью этого пакета npm. Дайте мне знать, что вы думаете. Есть ли у этого или подобного подхода перспективы?