Skip to content

Серверный рендеринг ​

При серверном рендеринге Reactive Route выступает матчером с выполнением жизненного цикла, цепочками редиректов и валидацией входящих данных. Для этого достаточно вызвать router.init и обработать редиректы.

Для составления более полной картины используем полноценный сервер с Express.js.

Сервер ​

tsx
import { renderToString } from 'react-dom/server';
import { getRouter, RouterContext } from 'router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = renderToString(
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  );

  res.send(template.replace(`<!-- HTML -->`, html));
});
tsx
import { renderToString } from 'preact-render-to-string';
import { getRouter, RouterContext } from 'router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = renderToString(
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  );
  
  res.send(template.replace(`<!-- HTML -->`, html));
});
tsx
import { generateHydrationScript, renderToString } from 'solid-js/web';
import { getRouter, RouterContext } from 'router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
<!-- HYDRATION -->
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = renderToString(() => (
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  ));
  
  res.send(template
    .replace(`<!-- HTML -->`, html)
    .replace(`<!-- HYDRATION -->`, generateHydrationScript())
  );
});
ts
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { getRouter, routerStoreKey } from './router';

import fs from 'node:fs';

import express from 'express';
import { RedirectError } from 'reactive-route';

import { App } from 'components/App';

const app = express();

app.get('*', async (req, res) => {
  const template = `
<html>
<body><div id="app"><!-- HTML --></div></body>
</html>`;

  const router = getRouter();

  try {
    await router.init(req.originalUrl);
  } catch (error: unknown) {
    if (error instanceof RedirectError) {
      console.log(`Some beforeEnter redirected to ${error.message}`);

      return res.redirect(error.message);
    }

    return res.status(500).send('Unexpected error');
  }

  const html = await renderToString(
    createSSRApp(App, { router }).provide(routerStoreKey, { router })
  );

  res.send(template.replace(`<!-- HTML -->`, html));
});

В данном примере в итоговый html не вставляются ссылки на js и css файлы, обычно это делается бандлером. Полный код с настройкой esbuild можно посмотреть в Примерах.

Клиент ​

На клиентской стороне добавляется только skipLifecycle: true, так как жизненный цикл был вызван на сервере, и используются соответствующие hydrate методы UI фреймворков.

tsx
import { hydrateRoot } from 'react';

import { App } from './App';
import { getRouter, RouterContext } from './router';

const router = getRouter();

await router.init(location.href, { 
  skipLifecycle: true
});

hydrateRoot(
  document.getElementById('app')!,
  <RouterContext.Provider value={{ router }}>
    <App />
  </RouterContext.Provider>
);
tsx
import { hydrate } from 'preact';

import { App } from './App';
import { getRouter, RouterContext } from './router';

const router = getRouter();

await router.init(location.href, {
  skipLifecycle: true
});

hydrate(
  <RouterContext.Provider value={{ router }}>
    <App />
  </RouterContext.Provider>,
  document.getElementById('app')!
);
tsx
import { hydrate } from 'solid-js/web';

import { App } from './App';
import { getRouter, RouterContext } from './router';

const router = getRouter();

await router.init(location.href, {
  skipLifecycle: true
});

hydrate(
  () => (
    <RouterContext.Provider value={{ router }}>
      <App />
    </RouterContext.Provider>
  ),
  document.getElementById('app')!
);
ts
import { createSSRApp } from 'vue';

import { App } from './App';
import { getRouter, routerStoreKey } from './router';

const router = getRouter();

await router.init(location.href, {
  skipLifecycle: true
});

createSSRApp(App, { router })
  .provide(routerStoreKey, { router })
  .mount('#app');

MPA ​

Режим Multi Page Application работает "из коробки", если компонент Link был создан по примеру из документации (т.е. использует нативный href). При переходе на новые страницы сервер будет отдавать готовый html и приложение остается рабочим даже с отключенным JavaScript в браузере.

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

No AI participated in the development. MIT License