Skip to content

Серверный рендеринг (SSR)

PreffX отрисовывает ваши компоненты на сервере в HTML-строку, а затем выполняет гидрацию того же дерева на клиенте. Сервер подготавливает разметку плюс скрипт предварительной загрузки для каждого корня, содержащий разрешённые асинхронные данные — клиент считывает этот скрипт и передаёт значения в дерево вместо повторной загрузки.

Утилиты SSR импортируются из preffx/server.

createRoot на сервере

Сервер экспортирует собственный createRoot, который повторяет клиентский, но возвращает метод renderToString вместо mount.

ts
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 запроса (маршрутизация, язык) или от контекста уровня запроса:

ts
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?)

Принимает те же параметры корня, что и клиентский корень, поэтому конфигурация остаётся синхронизированной:

ОпцияТипОписание
defaultURLstring | URLНачальный URL для маршрутизации
defaultLangstringНачальный язык для i18n
prefixstringПрефикс корня (должен совпадать с клиентским)
contextobjectКонтекст уровня запроса, разделяемый с деревом

Возвращает объект с единственным методом:

renderToString(component, config?)

  • component — корневой компонент (PC или APC).
  • config — необязательный, содержит:
    • props — свойства корневого компонента.
    • serializer — пользовательская функция сериализации предзагруженных данных (по умолчанию JSON.stringify).

Возвращает { preload, serialize }:

МетодПодписьОписание
preloadpreload(timeout?)Разрешает все зарегистрированные асинхронные ресурсы. Необязательный таймаут делает медленный ресурс "best-effort" — страница всё равно отрисовывается, а клиент выполняет повторный запрос.
serializeserialize(): stringФормирует HTML-строку (разметка + скрипт данных предзагрузки для корня).

Ресурсы на сервере

Внутри компонента resource ведёт себя прозрачно на сервере: вместо выполнения эффекта он регистрирует свой загрузчик для preload(). Используйте один и тот же компонент и на сервере, и на клиенте — ветвление не требуется.

tsx
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, а pendingtrue, поэтому отрисовывается запасное значение ('loading').
  • После preload() разрешённое значение встраивается в разметку, и serialize() также записывает его в скрипт данных предзагрузки.
  • На клиенте во время гидрации ресурс считывает предзагруженное значение и пропускает запрос, оставляя pending равным false.

Гидрация на клиенте

Клиентский createRoot().mount() обнаруживает скрипт данных предзагрузки, созданный сервером, передаёт его в дерево и выполняет гидрацию существующего DOM вместо его создания заново — дополнительный вызов не требуется.

tsx
import { createRoot } from 'preffx';
import { App } from './App';

createRoot().mount(App, {
    node: document.getElementById('app')!
});

Требования для успешной гидрации:

  • Параметры клиентского createRoot (prefix, defaultLang, defaultURL, context) должны совпадать с серверными.
  • Отрисованная разметка должна быть вставлена точно туда, где её ожидает клиент (контейнер, на который ссылается mount).

Пример: полный цикл запроса

tsx
// 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() };
}
tsx
// entry-client.tsx
import { createRoot } from 'preffx';
import { App } from './App';

createRoot().mount(App, { node: document.getElementById('app')! });

Node-сервер отрисовывает appHtml в HTML-оболочку (например, index.html) и отправляет её браузеру; затем клиентский скрипт выполняет гидрацию того же дерева.

Опубликовано под лицензией Apache License 2.0