Изоморфные приложения

Изоморфные приложения (или универсальные) — это приложения, способные рендериться как на стороне сервера, так и на стороне клиента. В контексте Riot.js такой подход позволяет ускорить загрузку страницы, улучшить SEO и обеспечить плавный переход между страницами без полной перезагрузки.


Архитектура изоморфного приложения

1. Разделение логики: Изоморфное приложение строится вокруг общей бизнес-логики, которая может выполняться как на сервере, так и на клиенте. Это требует строгого разделения слоёв:

  • Слой данных — содержит функции для получения данных (например, API-запросы к базе данных). Должен быть доступен и серверу, и клиенту.
  • Слой компонентов — Riot-компоненты, которые могут быть отрендерены на сервере (SSR) и инициализированы на клиенте.
  • Слой маршрутизации — отвечает за навигацию. На сервере используется для генерации HTML на основе URL, на клиенте — для динамического переключения страниц без перезагрузки.

2. Серверный рендеринг (SSR) в Riot.js: Riot.js поддерживает SSR через функцию render из пакета riot. Пример рендеринга компонента на сервере:

import { render } from 'riot';
import App from './components/App.riot';

function renderPage(data) {
  const html = render(App, { data });
  return `
    <!DOCTYPE html>
    <html lang="ru">
    <head>
      <meta charset="UTF-8">
      <title>Изоморфное приложение на Riot.js</title>
    </head>
    <body>
      <div id="app">${html}</div>
      <script src="/bundle.js"></script>
    </body>
    </html>
  `;
}

Здесь компонент App получает данные на сервере и возвращает HTML, который сразу отображается в браузере.


Гидратация компонентов на клиенте

После серверного рендеринга необходимо “гидратировать” компоненты на клиенте — привязать их к уже существующему DOM, чтобы события и реактивные обновления заработали:

import { mount } from 'riot';
import App from './components/App.riot';

const data = window.__INITIAL_DATA__;
mount(App, { data }, '#app');

window.__INITIAL_DATA__ — это объект с данными, переданный с сервера, что позволяет избежать повторного запроса данных.


Работа с роутером

Изоморфные приложения часто используют маршрутизацию. В Riot.js популярны следующие подходы:

  • Клиентская маршрутизация: с помощью riot-router или сторонних библиотек. Компоненты подгружаются динамически без перезагрузки страницы.
  • Серверная маршрутизация: Express, Koa или другой серверный фреймворк обрабатывает запросы и рендерит соответствующие компоненты.

Пример маршрутизации на Express:

import express from 'express';
import { render } from 'riot';
import App from './components/App.riot';

const app = express();

app.get('*', async (req, res) => {
  const data = await fetchData(req.url);
  const html = render(App, { data });
  res.send(`
    <!DOCTYPE html>
    <html lang="ru">
      <head><title>Riot SSR</title></head>
      <body>
        <div id="app">${html}</div>
        <script>window.__INITIAL_DATA__ = ${JSON.stringify(data)}</script>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000);

Управление состоянием

В изоморфных приложениях состояние компонентов должно быть изолированным и сериализуемым. Для этого часто используют паттерн store:

// store.js
export default function createStore(initialState = {}) {
  let state = initialState;
  const listeners = new Set();

  return {
    getState: () => state,
    setState: (newState) => {
      state = { ...state, ...newState };
      listeners.forEach(fn => fn(state));
    },
    subscribe: (fn) => listeners.add(fn),
    unsubscribe: (fn) => listeners.delete(fn)
  };
}

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


Динамическая загрузка компонентов

Для уменьшения размера бандла и ускорения загрузки применяются техники code splitting и lazy loading компонентов:

import { mount } from 'riot';

async function loadComponent() {
  const module = await import('./components/LazyComponent.riot');
  mount(module.default, {}, '#lazy');
}

document.querySelector('#load-btn').addEventListener('click', loadComponent);

Оптимизация и кеширование

  • Кеширование HTML на сервере: для часто посещаемых страниц.
  • Предварительная загрузка данных: сервер формирует HTML с данными, что снижает количество запросов с клиента.
  • Минификация и tree-shaking: уменьшение бандла, исключение неиспользуемого кода.

Важные моменты при разработке

  • Все компоненты должны корректно работать в SSR: нельзя использовать объекты window и document напрямую на сервере.
  • Состояние должно быть сериализуемым, иначе данные не смогут быть переданы клиенту.
  • Асинхронные операции на сервере должны завершаться до рендеринга компонента.

Изоморфный подход в Riot.js позволяет создавать быстрые, SEO-дружелюбные приложения с плавным пользовательским интерфейсом, минимизируя задержку между сервером и клиентом. Правильная архитектура и соблюдение правил SSR обеспечивает масштабируемость и поддерживаемость проекта.