Backend

MaxКак настроить server-side rendering в Vite и React без фреймворков

MaxКак настроить server-side rendering в Vite и React без фреймворков

 

Когда мы слышим про SSR (Server-Side Rendering) в экосистеме React, первым делом в голову приходит Next.js или Remix. Это отличные инструменты, но они навязывают свою архитектуру, систему роутинга и правила развертывания. Иногда проект требует максимальной легкости, полного контроля над каждым байтом или интеграции в уже существующий бэкенд, где «тяжелый» фреймворк будет избыточен.

В этой статье мы разберем, как собрать SSR-механизм самостоятельно, используя связку Vite, React и Node.js. Мы пройдем путь от базовой конфигурации до гидратации клиента.

Зачем это нужно, если есть Next.js?

Прежде чем переходить к коду, ответим на главный вопрос: зачем идти по пути боли?

  1. Полный контроль. Вы сами решаете, как обрабатываются запросы и как работает кэширование.
  2. Минимальный оверхед. Вы не тащите за собой сотни зависимостей фреймворка.
  3. Обучение. Понимание того, как работает renderToString и гидратация, делает вас более сильным инженером.

Шаг 1: Подготовка фундамента

Начнем со стандартного проекта Vite. Нам понадобится React и базовый набор инструментов.

bash
npm create vite@latest my-ssr-app — —template react
cd my-ssr-app
npm install
npm install express # Нам понадобится сервер для обработки запросов

Для реализации SSR нам нужно разделить точку входа на две части: одну для сервера (где React будет генерировать HTML-строку), другую для клиента (где React «оживит» этот HTML).

Шаг 2: Структура проекта

Чтобы не запутаться, организуем файлы следующим образом:

    • src/entry-client.jsx — точка входа для браузера.
    • src/entry-server.jsx — точка входа для сервера.
    • server.js — Express-сервер, который будет управлять рендерингом.
    • index.html — шаблон, в который мы будем вставлять контент.

Настраиваем клиентскую часть (src/entry-client.jsx)

Здесь всё почти так же, как в обычном SPA, но вместо createRoot мы используем hydrateRoot. Это критически важно: гидратация не создает DOM заново, а «привязывает» обработчики событий к уже существующей разметке, пришедшей с сервера.

javascript
import React from ‘react’
import ReactDOM from ‘react-dom/client’
import App from ‘./App’

ReactDOM.hydrateRoot(
document.getElementById(‘root’),

)

### Настраиваем серверную часть (`src/entry-server.jsx`)

Серверная точка входа должна экспортировать функцию, которая принимает приложение и возвращает строку HTML.

javascript
import React from ‘react’
import ReactDOMServer from ‘react-dom/server’
import App from ‘./App’

export function render() {
const html = ReactDOMServer.renderToString(

)
return { html }
}

## Шаг 3: Создание «умного» сервера на Express

Самая сложная часть — это заставить Vite работать в режиме SSR. Нам нужно, чтобы сервер понимал, какие файлы нужно транспилировать на лету, а какие отдавать как статику.

Создаем файл `server.js` в корне проекта:

javascript
import fs from ‘fs’
import path from ‘path’
import express from ‘express’
import { createServer as createViteServer } from ‘vite’

async function createServer() {
const app = express()

// Создаем экземпляр Vite в режиме middleware
const vite = await createViteServer({
server: { middlewareMode: true },
appType: ‘custom’
})

// Используем middleware Vite для обработки модулей
app.use(vite.middlewares)

app.use(‘*’, async (req, res) => {
const url = req.originalUrl

try {
// 1. Читаем index.html
let template = fs.readFileSync(
path.resolve(‘index.html’),
‘utf-8’
)

// 2. Применяем трансформации Vite к HTML (подстановка путей и т.д.)
template = await vite.transformIndexHtml(url, template)

// 3. Загружаем серверную точку входа
const { render } = await vite.ssrLoadModule(‘/src/entry-server.jsx’)

// 4. Рендерим приложение в строку
const { html: appHtml } = await render()

// 5. Вставляем отрендеренный HTML в шаблон
const html = template.replace(``, appHtml)

res.status(200).set({ ‘Content-Type’: ‘text/html’ }).end(html)
} catch (e) {
vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

app.listen(5173, () => {
console.log(‘Сервер запущен на http://localhost:5173’)
})
}

createServer()

**Важное замечание:** Не забудьте добавить в ваш `index.html` специальный комментарий `` внутри тега `

`. Именно его сервер будет заменять на реальный контент.

## Шаг 4: Разбор подводных камней (Troubleshooting)

Когда вы начнете запускать этот код, вы неизбежно столкнетесь с рядом проблем. Вот список того, что обычно «ломается» при ручном SSR:

1. **Доступ к `window` и `document`.**
На сервере нет браузерного API. Если в вашем компоненте (или в сторонней библиотеке) есть вызов `window.localStorage`, сервер упадет с ошибкой `window is not defined`.
*Решение:* Оборачивайте такие вызовы в `useEffect` (который не выполняется на сервере) или проверяйте `typeof window !== ‘undefined’`.

2. **Разрыв гидратации (Hydration Mismatch).**
Если HTML, сгенерированный на сервере, отличается от того, что React ожидает увидеть на клиенте при первом рендере, вы получите предупреждение в консоли.
*Причины:* Использование `Math.random()`, работа с текущей датой (`new Date()`) или разная разметка для сервера и клиента.

3. **Управление состоянием.**
Если сервер загрузил данные из API и отрендерил их, клиент должен получить эти же данные, иначе он «затрет» серверный контент своими начальными (пустыми) значениями. Для этого используется механизм «десериализации состояния» (передача данных через `window.__INITIAL_STATE__`).

## Шаг 5: Оптимизация и продакшн

Описанный выше метод работает в режиме разработки. Для продакшена вам потребуется:
1. Собрать клиентскую часть через `vite build —outDir dist/client`.
2. Собрать серверную часть через `vite build —ssr src/entry-server.jsx —outDir dist/server`.
3. Переписать `server.js` так, чтобы он отдавал статику из `dist/client` и использовал скомпилированный JS-файл сервера, а не `ssrLoadModule`.

## Итоговый чек-лист реализации

Чтобы убедиться, что вы ничего не упустили, проверьте свою реализацию по следующим пунктам:

1. [ ] Установлен `express` и настроен middleware Vite.
2. [ ] Создан раздельный вход для клиента (`hydrateRoot`) и сервера (`renderToString`).
3. [ ] В `index.html` добавлен маркер для вставки контента.
4. [ ] Все обращения к браузерному API вынесены в `useEffect` или защищены проверками.
5. [ ] Настроены скрипты запуска в `package.json` (например, `»dev:ssr»: «node server.js»`).

## Заключение

Создание SSR с нуля — это отличный способ понять, как на самом деле работают современные фреймворки. Мы увидели, что Vite предоставляет мощный инструментарий для трансформации модулей, который позволяет избежать ручной настройки Webpack или Rollup.

Хотя для большинства коммерческих проектов Next.js будет более прагматичным выбором, умение собрать SSR-систему самостоятельно дает вам гибкость. Теперь вы можете внедрять серверный рендеринг только там, где он действительно нужен, не переписывая всё приложение под жесткие стандарты фреймворков.