Перейти к содержанию
Mercury
Русский
Дизайн книга
Меню
Русский

Цикл разработки и горячая замена

Каждые 10 мс mx dev проверяет файлы, которые запрашивала страница, компилирует изменения и передаёт обновление или команду перезагрузки через сокет vite-hmr.

mx dev обеспечивает рабочий цикл: вы сохраняете файл, и открытая страница получает изменение. Здесь прослежен путь одного сохранения от диска до страницы.

Что обслуживает mx dev

usage: mx dev [--root <dir>] [--port <n>] [--importmap]

Сервер слушает 127.0.0.1 и выводит listening http://127.0.0.1:<port>/. По этому адресу доступны четыре вида путей:

  • / — страница: index.html с картой импортов на месте <!-- mercury:importmap -->. Сразу после карты вставляются <meta name="mx-dev-token"> и модульный скрипт клиента горячей замены. Клиент добавляется на каждую страницу, поэтому правка доходит до неё, даже если ни один её модуль ещё не поддерживает горячую замену.
  • /@mx/client.js — клиент горячей замены.
  • /@mx/<package name>/<path> — файлы участника. Участник с исходниками компилируется по запросу и отслеживается; остальные обслуживаются из сборки и не отслеживаются.
  • Все остальные пути — собственные файлы приложения. Модули (js, mjs, jsx, ts, mts или tsx) компилируются по запросу; остальные файлы отдаются без изменений.

Как замечается сохранение

mx dev не запрашивает у операционной системы события изменений. Каждые 10 мс он проверяет каждый файл, который запрашивала страница: один stat на файл, обычно не более нескольких десятков. Опрос одинаково работает на разных машинах и в контейнерах, без inotify, FSEvents и kqueue.

Изменение обрабатывается только после двух дополнительных проверок без новых изменений — как минимум 20 мс покоя. Поэтому сохранение, состоящее из нескольких операций записи, не компилируется в промежуточном состоянии. Если хеш сохранённых байтов совпадает с прежним, изменения нет.

От правки к обновлению

Дальнейшее действие зависит от изменённого файла:

Сохранённый файл Что получает страница
не модуль, например таблица стилей или JSON full-reload
модуль, который ещё ни разу не выполнялся full-reload: заменять нечего
модуль, новый текст которого отклонён error; страница продолжает выполнять имеющийся код
модуль, который компилируется обход, описанный ниже

Обход. От изменённого модуля mx dev поднимается по импортирующим его модулям до тех, которые принимают обновление, и помечает каждый пройденный модуль. Затем каждая открытая страница получает одно сообщение update. При повторном импорте помеченного модуля к адресу добавляется ?t=<stamp>, чтобы браузер загрузил новый текст вместо кешированной копии. Если обход достигает модуля, который никто не импортирует и который не принимает обновление, страница получает full-reload.

На странице клиент обрабатывает update последовательно: вызывает dispose старого модуля, импортирует <path>?t=<timestamp> и передаёт новый модуль обработчику accept. Если импорт не удался, страница сохраняет работавший модуль; следующее сохранение обрабатывается обычным порядком.

Какие модули принимают обновление

Это решает компилятор, а не поиск по тексту. Модуль принимает обновление, если результат компиляции содержит hmrRoot; mx не ищет в исходниках вызов accept.

Hypercompiler помечает модуль, который выполняет рендеринг. Если модуль импортирует render или hydrate из @fiberfx/hyperscript, компилятор привязывает их к объекту управления корнем из @fiberfx/hyperscript/hmr, и модуль принимает собственное обновление. Всё, чем этот объект не может управлять, приводит к перезагрузке; то же относится к модулю, до которого можно добраться только через импорты других модулей.

При этом клиент предоставляет каждому модулю import.meta.hot с четырьмя членами: data, accept, dispose и invalidate. accept принимает обработчик или вызывается без аргументов; приём обновления другого модуля не входит в контракт. invalidate передаёт обновление импортирующим модулям. Если обход возвращается к исходному модулю, как возможно при взаимных импортах, страница полностью перезагружается.

Когда правка отклонена

Сохранение, которое не компилируется, не ломает страницу:

  • каждая открытая страница получает сообщение error с текстом <path>: <the refusal>, которое клиент выводит в консоль как mx dev: …;
  • страница продолжает выполнять уже имеющийся модуль;
  • пока файл отклоняется, запрос этого модуля возвращает 500 с причиной отказа;
  • следующее сохранение запускает новую попытку.

Предусмотрены ещё два случая:

  • Нет открытых страниц. Правка, сделанная без открытой страницы, всё равно перезагрузит следующую подключившуюся страницу: она могла загрузить старый текст.
  • Сервер недоступен. Клиент опрашивает его раз в секунду и перезагружает страницу, как только mx dev снова отвечает.

Сокет и его защита

Сокет горячей замены находится по адресу / и использует протоколы Vite с соответствующим согласованием соединения:

  • запрос upgrade с vite-hmr получает 101, затем сокет открывается сообщением {"type":"connected"};
  • запрос с vite-ping получает 101 и немедленное закрытие с кодом 1000;
  • другой путь или протокол получает 400: an HMR socket is at / and speaks vite-hmr or vite-ping.

mx dev обслуживает только эту машину и проверяет это в каждом запросе:

  • Host. Если Host не обозначает loopback, ответ — 403: this host name is not allowed: mx dev answers loopback names only. Это блокирует страницу, использующую DNS rebinding.
  • Origin. Запрос сокета со страницы, чей origin не относится к loopback, получает 403.
  • Токен. Сокет страницы vite-hmr должен передавать её ?token=, который клиент читает из <meta name="mx-dev-token">. Без токена ответ — 400.

Участники рабочей области

Участники приложения — каталоги, на которые указывают замены в его mx.mod. Внутри mx.work один пакет может поступать из двух мест: строки replace и участника рабочей области в другом каталоге. mx dev и все остальные команды разрешения зависимостей отклоняют это, называя оба каталога:

<name> is both <app>'s replace (<dir>) and the mx.work's member (<dir>): remove the replace, or the member from mx.work, or build with MXWORK=off

Если replace указывает на собственный каталог участника, источник один и проверка проходит. Две строки replace для одного пакета отклоняются с … one directory per package.

Что поставляется

Описанный цикл реализован. Известны два ограничения:

  • Код приложения. На этом сайте сохранённая правка в app/ применяется на месте, после чего страница один раз перезагружается. Причина этой перезагрузки исследуется.
  • Участники и модели. Правка исходников участника доходит до страницы через полную перезагрузку. Модуль модели, который не выполняет рендеринг, не принимает обновления, поэтому его правка перезагружает страницу. Замену графа модели Hypergraph на месте mx dev пока не обеспечивает.