Skip to content

Feat/suspense module mounter#532

Open
heymdall-legal wants to merge 23 commits into
masterfrom
feat/suspense-module-mounter
Open

Feat/suspense module mounter#532
heymdall-legal wants to merge 23 commits into
masterfrom
feat/suspense-module-mounter

Conversation

@heymdall-legal

@heymdall-legal heymdall-legal commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Добавлена возможность монтировать модули через суспенс. Как на клиенте, так и на сервере.
Для серверного монтирования есть дополнительные ограничения, поэтому он реализован отдельным методом.

По сути это все добавление новой фичи и никак не ломает предыдущее использование.

Общая логика описана в docs/specs/ssr-spec.md. Собственно по этой спеке все и делалось.

Единственная реальная "недаработка" в спеке - шареный кеш для суспенса, который чисто теоретически может некорректно пошарится между разными запросами если модули из разных запросов используют абсолютно одинаковый набор параметров для модулей. Буду лечить, но отдельным пр-ом. По сути риск подобного сценария минимален.

В целом тут больше текста и тестов, чем самого реального кода.
Что в реальности реализовано:
В script-modules:

  • Новый апи createLazyMounter - React.lazy-совместимая фабрика. На сервере рендерит пустой
    (без вызова загрузчика). На клиенте: mount с поддержкой module.update на изменение runParams.
  • createSsrMounter - изоморфная фабрика компонента для запихивания его в suspense. Оно уже умеет рендрится на сервере и затем корректно гидрируется на клиенте без перезапроса getModuleResources. Этот вариант НЕ умеет работать с shadowDOM.
  • createServerStateModuleFetcher переписан с xmlHttpRequest на fetch. Никому ничего не должно сломать, но если вдруг кто-то сидит на ie - добавляйте полифилы сами.

Остальные изменения в пакете по сути позволяют всему этому работать.

В scripts-server:

  • createGetModulesMethod теперь позволяет указать для модулей дополнительный метод - renderToHtml, который должен возвращать html модуля, который запрошен с чужого сервера
  • Для default модулей манифест теперь будет содержать стили если модуль был запрошен для серврерного рендринга. Раньше вся загрузка производилась райнтаймом вебпака. Если не добавлять серверный рендеринг все продолжает работать как раньше

В scripts:

  • В манифест модулей, который пишется при сборке на диск теперь будут записываться стили для default модулей. Для этого сделан плагин, который находит все чанки которые сделаны wmf.

Как вообще работает подключение модулей с SSR:

На сервере хоста

  1. createSsrMounter рендерит ServerModule внутри <Suspense>. React приостанавливает рендер, пока не разрешится Suspense-ресурс.

  2. Suspense-ресурс вызывает loadServerModule, которая делает getModuleResources({ ssr: { runParams } }) — запрос к серверу модуля с флагом SSR.

  3. Сервер модуля в createGetModulesMethod видит флаг ssr, вызывает module.renderToHtml(moduleState, ssrRunParams), возвращает { html, scripts, styles, moduleState }.

  4. Компонент рендерит:

    • стили — inline <style> (default) или <link> при stylesMode: 'link'; атрибуты data-module-ssr-href и data-parent-app-id для того чтобы на клиенте рантайм не началл грузить их еще раз;
    • HTML модуля в <div> c data-module-ssr-mount-id;
    • JSON-payload ресурсов в <script type="application/json"> с data-module-ssr-payload.

Стили default (module federation) модулей добавляет AttributeModuleCssPlugin при сборке: обходит граф чанков и записывает css в запись модуля в манифесте. При SSR-запросе createGetModulesMethod отдаёт эти стили; для обычных запросов ответ остаётся прежним (css: []).

На клиенте

  1. ClientModule при первом рендере захватывает snapshot серверной разметки (она уже в DOM) и рендерит её как dangerouslySetInnerHTML — React не трогает стили и payload.

  2. Запускает лоадер, обёрнутый в createEmbeddedModuleFetcher: ищет <script type="application/json"> с data-module-ssr-payload, к сети не обращается. Если payload не найден — падает на fallback-фетчер (исходный getModuleResources).

  3. Загружает скрипты модуля — через MF runtime (default) или <script>-теги (compat). Стили с data-module-ssr-href наследуются и не загружаются повторно.

  4. Гидрация: если у модуля есть hydrate — вызывает его, React гидрирует существующую разметку. Если нет — очищает innerHTML монтируемого узла и вызывает mount (graceful degradation, warning в dev).

  5. Обновление: при изменении runParams вызывает module.update(target, params, state) — без перемонтирования.

Prerelease versions:

@alfalab/scripts-modules@0.0.0-next-20260709074302
@alfalab/scripts-server@0.0.0-next-20260709074302
arui-scripts@0.0.0-next-20260709074302

Heymdall added 20 commits July 6, 2026 00:16
- fetchResources adopts server-emitted module styles (data-module-ssr-href)
  instead of removing and re-fetching them, avoiding FOUC
- add createEmbeddedModuleFetcher: reads resources from embedded payload
- useModuleMounter/createLazyMounter call module.update() on runParams
  change when available, falling back to remount otherwise
- export MODULE_SSR_HREF_ATTRIBUTE
Isomorphic component exported from @alfalab/scripts-modules/ssr.

- server: suspends on getModuleResources with ssr flag, inlines module
  styles, renders module html into an outlet, embeds resources payload
- client: reads embedded payload (no duplicate resources request),
  hydrates server markup via module.hydrate (or mounts when absent),
  calls module.update on runParams change, cleans up on unmount
- client loader is built internally wrapping the fetcher in
  createEmbeddedModuleFetcher, so the resources endpoint is not called twice
- add exports map (root '.' unchanged, new './ssr' subpath)
- export MODULE_SSR_ROOT_ATTRIBUTE and MODULE_SSR_MOUNT_ID_ATTRIBUTE
- round-trip test: renderToPipeableStream -> serialize -> jsdom hydrate
Document in the ssr spec why createSsrMounter shipped without the
fallback prop (server/client hydration-consistency problem), the
process-global suspense cache trade-offs (cross-request sharing,
no abort wiring), and the unused link-mode style adoption path.
Also note that the factory builds its client loader internally,
superseding the §5.3 passed-in-loader signature.
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 331ff34

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@alfalab/scripts-modules Minor
@alfalab/scripts-server Minor
arui-scripts Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@heymdall-legal heymdall-legal marked this pull request as ready for review July 9, 2026 14:19
@qrik116

qrik116 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Сложно полноценно в голове скомпилировать работу и что-то дельное найти, выглядит классно

@dmitrbrvsk dmitrbrvsk self-requested a review July 12, 2026 15:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants