Pull to refresh
69.19
Voximplant
Облачная платформа голосовой и видеотелефонии

WebPack: как внутри устроено Hot Reloading

Reading time 7 min
Views 43K
Наша платформа voximplant активно использует javascript. С помощью него клиенты управляют в реальном времени звонками, на нем работает наша backend логика и большинство frontend. Javascript мы любим, ценим и стараемся быть в курсе последних новостей. Сейчас наши разработчики активно экспериментируют с перспективной связкой webpack + typescript + react (кстати, для typescript мы сделали type definitions к нашему web sdk, но об этом как-нибудь в другой раз).

Особенно нам нравится «hot module replacement»: возможность при изменении исходников очень быстро отобразить изменения в браузере без перезагрузки страницы. Выглядит как магия. К сожалению, документировано тоже как магия — по словам eyeofhell, нашего технического евангелиста, «пример на офсайте — это уникальная комбинация частных случаев и особых команд, любое изменение в которых делает его неработоспособным». На наш взгляд все не так плохо, за пару вечеров вполне можно разобраться. Но и не так просто, как хотелось бы. Поэтому специально для Хабра под катом мы максимально просто и понятно расскажем как работает под капотом вся эта машинерия.

Магия как она есть


Официальный пример использования hot module replacement очень прост. Авторы предлагают создать файл style.css с одним стилем:

body {
    background: red;
}


И файл entry.js, который использует основную фичу webpack, команду require, чтобы добавить .css файл к содержимому страницы. Ну и создает элемент типа input, на котором можно проверить hot module replacement:

require("./style.css");
document.write("<input type='text' />");


Далее предлагается запустить webpack с помощью заклинания

webpack-dev-server ./entry --hot --inline --module-bind 'css=style!css'


И открыть страницу, доступную по адресу localhost:8080/bundle. После чего можно наблюдать магию hot module replacement: если ввести в поле input какой-нибудь текст, переместить курсор на один из символов этого текста, а затем поменять цвет в файле style.css — то цвет фона страницы поменяется практически сразу, при этом не потеряется введенный текст и даже позиция курсора останется прежней.

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

  1. Что это за ./entry?
  2. Что делают и чем отличаются --hot и --inline?
  3. Что это за --module-bind такой?
  4. Почему, если добавить react.js, hot reload перестает работать?


Разоблачение магии


Начнем с самого простого. ./entry и --module-bind — это читерские аргументы, которые позволяют в целях демонстрации запускать webpack без конфигурационного файла webpack.config.js. Первый позиционный аргумент всего лишь имя javascript файла, являющегося «точкой входа» в программу, именно его код будет запускаться при выполнении скомпилированной bundle. Многих разработчиков смущает то, что этот аргумент не выглядит как имя файла. На самом деле это имя файла. Просто в целях экономии символов авторы примера воспользовались одной из особенностей webpack: файлы в require и в командной строке можно указывать без расширения, webpack автоматически попробует найти такой файл .js (или с другими расширениями, если это настроено в конфигурации). Аргумент --module-bind позволяет без конфигурационного файла указать используемые загрузчики, в данном случае для файлов с расширением css будет использован сначала загрузчик css-loader а затем загрузчик style-loader. Как нетрудно догадаться, суффикс -loader тоже можно не указывать, и авторы примера пользуются этим для экономии нескольких символов и запутывания читателей.

Режим работы «iframe automatic refresh» и встроенный веб сервер


На самом деле у webpack три режима работы автоматического обновления страницы. Самый простой режим называется iframe mode: он включается автоматически, если webpack запустить без ключей командной строки --inline и --hot, то есть вот так:

webpack-dev-server ./entry --module-bind 'css=style!css'


Запущенный веб сервер будет отдавать браузеру следующие страницы:

  1. localhost:8080/webpack-dev-server Покажет меню, в котором можно посмотреть исходик созданной в памяти bundle или открыть в браузере специальную html страницу, единственное назначение которой — выполнить javascript код bundle
  2. localhost:8080/webpack-dev-server/ От предыдущей ссылки отличается присутствием слеша на конце. Список файлов в папке, где запущен сервер. Клик по файлу покажет его в iframe и будет автоматически перезагружать, если файл изменится.
  3. localhost:8080/webpack-dev-server/bundle Та самая страница из первого пункта. Открывается в iframe и автоматически перезагружается. Будет автоматически перезагружаться при изменении любого файла, который приводит к перекомпиляции bundle
  4. localhost:8080/ и localhost:8080/bundle Ловушка для невнимательных. То же что во втором и третьем пункте, но файлы и bundle открываются не в iframe. Перезагружаться не будет. Зачем она? Для второго режима работы, --inline. Зачем показывать в первом режиме работы? Чтобы запутать разработчиков, конечно же. Ну и чтобы раздавать статику без iframe.


Режим работы «inline automatic refresh» и встраиваемый refresh client


Второй режим работы активируется ключом командной строки --inline и предсказуемо называется «inline» режимом. В этом режиме все несколько сложнее: в bundle добавляется модуль «refresh client», исходный код которого можно посмотреть в файле webpack-dev-server/client/index.js. Этот модуль будет загружен с помощью require перед вашим собственным кодом. Более того, если посмотреть в сгенерированный bundle (с помощью меню веб сервера, о котором я писал выше), то можно увидеть что этот require не совсем обычный:

/* WEBPACK VAR INJECTION */}.call(exports, "?http://localhost:8080"))


Это результат выполнения вот такого кода:

require("index?http://localhost:8080")


Этот слабодокументированный синтаксис «webpack resource query» позволяет передавать произвольные параметры в загружаемый через require код. В данном случае webpack-dev-server генерирует bundle, который при загрузке refresh client передает ему адрес запущенного на машине разработчика webpack-dev-server. Зачем ему адрес? Конечно же чтобы подключиться к серверу через socketio и ждать нотификации об изменениях файлов. Получив такую нотификацию, refresh client перезагрузит страницу. По сути происходит то же что и с iframe, но без iframe. Это позволяет отлаживать чувствительный к url код и используется как вспомогательный механизм для третьего, самого интересного режима работы: hot module replacement

hot module replacement: сильное колдунство для быстрой разработки


Как уже догадался внимательный читатель, третий режим работы включается добавлением ключа командной строки --hot, который возвращает нас к тому заклинанию, с которого началась эта статья. Но здесь не все так просто. «Hot module replacement» — это функциональность webpack, предназначенная не только для быстрой подгрузки изменений на машине разработчика, но и для обновления сайтов в production. При использовании ключа --hot bundle будет собран с поддержкой hot module replacement: соответствующий код и api добавляется в загрузчик webpack, за это отвечает HotModuleReplacementPlugin. Ключ --hot понимает как webpack-dev-server, так и webpack. С помощью hot module replacement api разработчик может запрашивать свой сервер на предмет «а не обновилось ли что», отсылать команду «обновись» дереву модулей и управлять тем, как модули обновляются без перезагрузки страницы.

Здесь два ключевых момента:

  • Код, который узнает о факте обновления, должен написать разработчик. Webpack считает хеши модулей и предоставляет ajax api для загрузки обновления с сервера — но вызвать метод module.hot.check разработчик должен сам. Это не навязывает какой-то способ общения с сервером и позволяет разработчикам интегрировать hmr в существующие проекты: узнавать о наличии обновлений можно любым способом, начиная от кнопки «проверить обновления» с ajax запросом и заканчивая websocket подключением от страницы к backend.
  • Webpack не обновляет модули сам. Он дает модулям возможность подписаться на callback module.hot.accept, module.hot.decline и module.hot.dispose чтобы реагировать на полученное от сервера обновление своего кода. К примеру, код модуля, отвечающего за загрузку css, может применить обновленные стили. А код модуля, создающего интерфейс ReactJS, вызвать новую версию render(), чтобы перерисовать себя.


Учитывая эти два момента, просто добавление кода hot module replacement ничего не даст — только увеличит размер bundle на несколько килобайт. Нужен еще код, который будет общаться с сервером, узнавать о наличии обновлений и вызывать module.hot.check. И такой код есть! webpack-dev-server, запущенный с ключом --hot, добавляет в собираемый bundle модуль «hot loader», исходник которого можно посмотреть в файле webpack/hot/dev-server.js. Этот модуль, так же как модуль «refresh client», будет загружен перед вашим кодом. Делает он интересную штуку: подписывается на dom event с именем webpackHotUpdate и при получении этого эвента использует hot module replacement api для обновления дерева модулей. Если модули не обновились (то есть в модулях либо нет кода обновления, либо код вернул статус невозможности обновиться), то hot loader перезагружает страницу целиком.

А кто же отсылает эвент webpackHotUpdate? Это делает «refresh client». Тот самый, который добавляется ключем --inline, поддерживает websocket подключение к webpack-dev-server и следит за изменениями файлов. При использовании ключа --hot, webpack-dev-server отправляет refresh client по websocket сообщение «hot», которое переключает refresh client в «hot mode». В этом режиме он перестает обновлять страницу сам, а вместо этого отсылает эвент webpackHotUpdate.

Последний вопрос: откуда берется код, который обновляет CSS стили? Как я уже написал выше, webpack сам ничего обновлять не будет и просто вызовет callback, на который может подписаться модуль. Откуда там этот callback? Сюрприз — style-loader имеет встроенную поддержку «hot module replacement». Специально для того, чтобы работал пример из документации.

Выводы


  • Если hot module replacement не работает — проверьте что выбран правильный режим и что используемые loader'ы его поддерживают. «refresh client» и «hot loader» отчитываются в лог о происходящем.
  • Если вместо изменения части страницы она перезагружается целиком — тоже смотрите в лог, там вам расскажут какой из модулей не смог hot module replacement.
  • Технологию можно использовать не только при отладке на машине разработчика, для этого нужно будет реализовать на стороне клиента и сервера то, что за вас делает webpack-dev-server.
  • Поддержку hot module replacement можно добавлять в свои модули и радоваться мгновенному обновлению страницы без перезагрузки во время разработки. Соответствующее api довольно простое и неплохо документировано.
Tags:
Hubs:
+26
Comments 12
Comments Comments 12

Articles

Information

Website
www.voximplant.com
Registered
Founded
Employees
101–200 employees
Location
Россия