Наша платформа 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 довольно простое и неплохо документировано.

Комментарии (11)


  1. aush
    17.11.2015 10:30
    +4

    Продолжайте. Сам как раз сейчас этим занимаюсь и как раз связку webpack + typescript + react рассматриваю. Меня, при этом, еще и Redux очень интересует, как сам по себе, так и за github.com/gaearon/redux-devtools.


  1. justusebrain
    18.11.2015 01:22
    -1

    Классно, что вы любите вебпак, всё новое и удобное, а разработчиков, которые пишут сценарии с VoxEngine, не любите вообще — ни адекватных логов, ни нормального дебага, ни-че-го.


    1. akvakh
      18.11.2015 08:15

      Печально это слышать, учитывая наши подробнейшие логи и встроенный дебагер :). Если есть какие-то сложности — напишите eyeofhell, он поможет.


    1. aylarov
      18.11.2015 16:10

      Мда… просто кину пару ссылок, комментировать тут как-то иначе сложно и бессмысленно:

      http://voximplant.com/docs/quickstart/13/debugging/
      http://voximplant.com/blog/debugging-voxengine-scenarios/


      1. justusebrain
        18.11.2015 17:12

        Камон, вы кидаете ссылки, на первой информация сводится к «у нас есть дебаггер, вот скриншот», на второй — «у нас есть дебаггер, вот вам 2 скриншота и видео, посмотрите 8 минут как наши парни выступают, мб научитесь им пользоваться». Серьёзно?


        1. aylarov
          18.11.2015 17:16

          Более чем серьезно, т.к. вы написали

          ни адекватных логов, ни нормального дебага, ни-че-го.
          , вам дали ссылки на информацию по наличию данных вещей в платформе, а то что вам лень в них разбираться — это не проблема платформы.


          1. justusebrain
            18.11.2015 17:52

            Алексей, я уже прочёл то, что вы написали, настолько там мало информации.
            В первой ссылке написано «у нас есть логгер, вы можете писать кастомные логи. Ещё есть дебаггер». Классно, это называется «подробнейшие логи»? Ваши подробнейшие логи — это куча служебной информации Sent event to JS onPhoneEvent with params [{headers = {X-mera-expires = 86460 ; } ; id = adnawioDJ@ODNWDOwanidan; name = Call.Connected ; } ; ], но мало что полезного для меня.
            Во второй ссылке написано «есть дебаггер, вот его можно найти здесь, работает он как FireBug в файрфоксе, после нажатия на кнопку «старт» запустится сессия». Класс, а подробнее можно? Что за Watch Expressions, что мне туда вписывать-то? Что значит блок Run your code? Что значат иконки в Controls? Непонятно.


            1. justusebrain
              18.11.2015 17:56

              Плюс, интересно, что вы скажете по поводу ошибки 2015-11-12 09:43:42 JS error: {code = 4.0000E+00 ; details = {column = 1.0000E+00 ; exception_class = Error ; exception_msg = Module 'main' not found ; file = ; line = 1.0000E+00 ; stack = [@:1 ; ] ; } ; msg = Error: Module 'main' not found ; }

              Как она вообще в продакшене появилась? Несколько звонков из-за неё пропали


              1. eyeofhell
                18.11.2015 18:51

                Судя по логу, случилось что-то неприятное внутри нашей платформы и звонок попал на тот момент, когда она восстанавливала целостность. Мы не говорим что у нас 100% аптайм — иногда случаются проблемы с телекомами, намного реже — с нашими серверами. Мы внимательно все мониторим и готовы помочь клиентам во всех случаях, вне зависимости от того, на чьей стороне проблема.


            1. eyeofhell
              18.11.2015 18:45
              +2

              а подробнее можно? Что за Watch Expressions, что мне туда вписывать-то? Что значит блок Run your code? Что значат иконки в Controls? Непонятно.


              Пишите в личку или на fb что хотите сделать, что не получается. Я помогу. Надеюсь, вы не сильно обидитесь, если я скажу что за многолетнюю историю нашей платформы вы — первый, кто спрашивает «Что за Watch Expressions, что мне туда вписывать-то?» :). Если это не троллинг и с этим есть действительно есть проблемы — я без вопросов расширю документацию, мне не трудно. Но описывать каждый чих «про запас» — не лучшее решение.


              1. justusebrain
                18.11.2015 23:40

                Хорошо, буду иметь в виду в следующий раз.

                PS: это не совсем троллинг, мне действительно несложно загуглить и в этом разобраться (каюсь, редко пользовался дебаггерами в остальных инструментах и поэтому не знал), проблема в том, что когда я занимаюсь сценарием, я не хочу гуглить, я хочу пробежаться по документации и понять, как я могу протестировать сценарий.