Серверный рендеринг (SSR)
PreffX отрисовывает ваши компоненты на сервере в HTML-строку, а затем выполняет гидрацию того же дерева на клиенте. Сервер подготавливает разметку плюс скрипт предварительной загрузки для каждого корня, содержащий разрешённые асинхронные данные — клиент считывает этот скрипт и передаёт значения в дерево вместо повторной загрузки.
Утилиты SSR импортируются из preffx/server.
createRoot на сервере
Сервер экспортирует собственный createRoot, который повторяет клиентский, но возвращает метод renderToString вместо mount.
import { createRoot } from 'preffx/server';
import { App } from './App';
const server = createRoot();
const { preload, serialize } = server.renderToString(App, {
props: { name: 'PreffX' }
});
await preload(); // resolve every async resource
const html = serialize(); // markup + preload data scriptОбычно на каждый запрос создаётся новый корень, поскольку приложение может зависеть от URL запроса (маршрутизация, язык) или от контекста уровня запроса:
import { createRoot } from 'preffx/server';
export async function render(url: URL) {
const root = createRoot({
defaultURL: url,
defaultLang: 'en',
context: { tenant: 'acme' }
});
const { preload, serialize } = root.renderToString(App, {});
await preload();
return { appHtml: serialize() };
}API
createRoot(config?)
Принимает те же параметры корня, что и клиентский корень, поэтому конфигурация остаётся синхронизированной:
| Опция | Тип | Описание |
|---|---|---|
defaultURL | string | URL | Начальный URL для маршрутизации |
defaultLang | string | Начальный язык для i18n |
prefix | string | Префикс корня (должен совпадать с клиентским) |
context | object | Контекст уровня запроса, разделяемый с деревом |
Возвращает объект с единственным методом:
renderToString(component, config?)
component— корневой компонент (PCилиAPC).config— необязательный, содержит:props— свойства корневого компонента.serializer— пользовательская функция сериализации предзагруженных данных (по умолчаниюJSON.stringify).
Возвращает { preload, serialize }:
| Метод | Подпись | Описание |
|---|---|---|
preload | preload(timeout?) | Разрешает все зарегистрированные асинхронные ресурсы. Необязательный таймаут делает медленный ресурс "best-effort" — страница всё равно отрисовывается, а клиент выполняет повторный запрос. |
serialize | serialize(): string | Формирует HTML-строку (разметка + скрипт данных предзагрузки для корня). |
Ресурсы на сервере
Внутри компонента resource ведёт себя прозрачно на сервере: вместо выполнения эффекта он регистрирует свой загрузчик для preload(). Используйте один и тот же компонент и на сервере, и на клиенте — ветвление не требуется.
import type { PC } from 'preffx';
export const UserCard: PC = (_props, { resource, computed }) => {
const [user] = resource(async () => {
const res = await fetch('/api/user');
return res.json();
});
const display = computed(() => user.state.value?.name ?? 'loading');
return (
<article className="user">
<h1>{display}</h1>
</article>
);
};- До выполнения
preload()значениеuser.state.valueравноnull, аpending—true, поэтому отрисовывается запасное значение ('loading'). - После
preload()разрешённое значение встраивается в разметку, иserialize()также записывает его в скрипт данных предзагрузки. - На клиенте во время гидрации ресурс считывает предзагруженное значение и пропускает запрос, оставляя
pendingравнымfalse.
Гидрация на клиенте
Клиентский createRoot().mount() обнаруживает скрипт данных предзагрузки, созданный сервером, передаёт его в дерево и выполняет гидрацию существующего DOM вместо его создания заново — дополнительный вызов не требуется.
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, {
node: document.getElementById('app')!
});Требования для успешной гидрации:
- Параметры клиентского
createRoot(prefix,defaultLang,defaultURL,context) должны совпадать с серверными. - Отрисованная разметка должна быть вставлена точно туда, где её ожидает клиент (контейнер, на который ссылается
mount).
Пример: полный цикл запроса
// entry-server.tsx
import { createRoot } from 'preffx/server';
import { App } from './App';
export async function render(url: URL) {
const server = createRoot({ defaultURL: url });
const { preload, serialize } = server.renderToString(App, {});
await preload();
return { appHtml: serialize() };
}// entry-client.tsx
import { createRoot } from 'preffx';
import { App } from './App';
createRoot().mount(App, { node: document.getElementById('app')! });Node-сервер отрисовывает appHtml в HTML-оболочку (например, index.html) и отправляет её браузеру; затем клиентский скрипт выполняет гидрацию того же дерева.