Серверный рендеринг
При серверном рендеринге Reactive Route выступает матчером с выполнением жизненного цикла, цепочками редиректов и валидацией входящих данных. Для этого достаточно вызвать router.init и обработать редиректы.
Для составления более полной картины используем полноценный сервер с Express.js.
Сервер
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));
});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));
});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())
);
});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 фреймворков.
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>
);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')!
);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')!
);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 здесь выступает в виде описания роутов с валидацией и при необходимости асинхронным жизненным циклом для загрузки данных, и может в ряде случаев заменить традиционно используемые библиотеки для серверного роутинга.