Настройка SSR с Vue

Server-Side Rendering (SSR) в Vue.js позволяет рендерить компоненты на сервере и отправлять готовый HTML на клиент. Это улучшает производительность, SEO и снижает время до первого отображения страницы (Time to First Paint). В отличие от классического SPA, где весь рендер происходит на клиенте, SSR позволяет генерировать полный HTML на сервере, а затем гидрировать его на клиенте для интерактивности.

Ключевые преимущества SSR:

  • Быстрый первый рендер и улучшенное SEO.
  • Поддержка старых браузеров без необходимости полного JavaScript.
  • Возможность предварительной загрузки данных на сервере.

Структура проекта для SSR

Проект для SSR обычно разделяется на несколько частей:

  1. Серверная часть (server) Обрабатывает запросы, рендерит компоненты Vue в HTML и отправляет их клиенту.

  2. Клиентская часть (client) Гидрирует серверный HTML и управляет интерактивностью.

  3. Общие компоненты (shared) Компоненты и маршруты, используемые как на сервере, так и на клиенте.

Пример структуры:

project/
│─ server/
│   └─ index.js
│─ client/
│   └─ main.js
│─ shared/
│   └─ App.vue
│   └─ components/
│       └─ Header.vue
│       └─ Footer.vue
│─ package.json

Настройка сборки

SSR требует двух сборок: серверной и клиентской. Для этого используют webpack или Vite с разными конфигурациями.

  • Клиентская сборка включает весь JavaScript для браузера.
  • Серверная сборка экспортирует функцию рендеринга, которая возвращает HTML.

Пример конфигурации с Vite:

// vite.config.js
import vue from '@vitejs/plugin-vue';

export default {
  plugins: [vue()],
  build: {
    ssr: process.env.SSR === 'true' ? 'server/main.js' : undefined,
    outDir: process.env.SSR === 'true' ? 'dist-server' : 'dist-client'
  }
};

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

Для SSR Vue предоставляет функцию renderToString из пакета @vue/server-renderer. Серверный код выглядит так:

import express from 'express';
import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';
import App from '../shared/App.vue';

const server = express();

server.get('*', async (req, res) => {
  const app = createSSRApp(App);
  const html = await renderToString(app);
  
  res.send(`
    <!DOCTYPE html>
    <html lang="ru">
      <head>
        <meta charset="UTF-8">
        <title>Vue SSR</title>
      </head>
      <body>
        <div id="app">${html}</div>
        <script type="module" src="/client/main.js"></script>
      </body>
    </html>
  `);
});

server.listen(3000);

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

Клиентская часть подключает серверный HTML и делает его интерактивным с помощью createApp и метода mount.

import { createApp } from 'vue';
import App from '../shared/App.vue';

createApp(App).mount('#app');

Гидрация гарантирует, что события и состояние компонентов работают корректно после того, как HTML был сгенерирован на сервере.

Работа с данными на сервере

Для SSR важно загружать данные до рендера, чтобы HTML содержал актуальную информацию. Обычно создают функцию asyncData или используют Vuex.

Пример с asyncData:

export default {
  data() {
    return { items: [] };
  },
  async asyncData() {
    const response = await fetch('https://api.example.com/items');
    return { items: await response.json() };
  },
  created() {
    if (this.$ssrContext) {
      Object.assign(this.$data, this.$ssrContext.asyncData);
    }
  }
};

На сервере вызывают asyncData перед рендером:

const app = createSSRApp(App);
app.$ssrContext = { asyncData: await App.asyncData() };
const html = await renderToString(app);

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

SSR обычно интегрируется с vue-router. Необходимо синхронизировать маршрут сервера и клиента:

import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router';
import Home from './pages/Home.vue';

export function createAppRouter(ssr = false) {
  return createRouter({
    history: ssr ? createMemoryHistory() : createWebHistory(),
    routes: [{ path: '/', component: Home }]
  });
}

На сервере используется createMemoryHistory, на клиенте — createWebHistory.

Асинхронная маршрутизация и данные

SSR требует загрузки данных для конкретного маршрута на сервере. Обычно создается функция fetchData для компонентов:

const matchedComponents = router.getMatchedComponents(req.url);
await Promise.all(matchedComponents.map(c => c.fetchData?.()));

Это гарантирует, что все данные будут готовы до рендера.

Интеграция с Vuex

Для состояния приложения на сервере используют отдельный экземпляр Vuex на каждый запрос:

import { createStore } from 'vuex';

export function createAppStore() {
  return createStore({
    state: () => ({ items: [] }),
    mutations: { setItems(state, items) { state.items = items; } },
    actions: { async fetchItems({ commit }) {
      const data = await fetch('https://api.example.com/items').then(r => r.json());
      commit('setItems', data);
    }}
  });
}

На сервере:

const store = createAppStore();
await store.dispatch('fetchItems');
const app = createSSRApp(App);
app.use(store);

На клиенте состояние гидрируется из window.__INITIAL_STATE__:

const store = createAppStore();
store.replaceState(window.__INITIAL_STATE__);
createApp(App).use(store).mount('#app');

Оптимизация SSR

  • Кэширование HTML: сохранять готовый HTML для часто посещаемых страниц.
  • Кэширование данных: избегать повторных запросов на сервере.
  • Lazy-loading компонентов: уменьшить время генерации страницы.
  • Предварительная загрузка критических ресурсов: скрипты и стили.

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