**CSR (Client-Side Rendering)** — классический SPA:
**SSR (Server-Side Rendering)** — рендер на сервере:
**SSG (Static Site Generation)** — генерация при сборке:
Основной API Vue SSR — функция рендера компонента в HTML-строку:
// server.js (Node.js)
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './App.vue'
const app = createSSRApp(App)
const html = await renderToString(app)
// html — строка с отрендеренным HTML:
// <div id="app" data-v-app=""><h1>Привет!</h1></div>После получения серверного HTML клиент делает его интерактивным:
// client.js
import { createSSRApp } from 'vue'
import App from './App.vue'
// createSSRApp вместо createApp — запускает гидрацию
const app = createSSRApp(App)
app.mount('#app') // Vue "оживляет" существующий HTMLГидрация: Vue проходит по DOM-дереву и "привязывает" реактивное состояние к уже существующим элементам вместо их пересоздания.
// ProductList.vue
const products = ref([])
onServerPrefetch(async () => {
// Выполняется только на сервере, перед рендером
products.value = await fetchProducts()
})
// На клиенте — данные уже в HTML, гидрация восстановит ихimport { onMounted } from 'vue'
// Проверка окружения
if (typeof window !== 'undefined') {
// Только клиентский код
}
// Хуки работают только на клиенте:
onMounted(() => {
// window, document, localStorage — доступны
// onMounted НЕ вызывается при SSR рендере
})
// Динамический импорт для клиентских библиотек:
const Chart = defineAsyncComponent(() => {
if (import.meta.env.SSR) return Promise.resolve({ render: () => null })
return import('./HeavyChart.vue')
})**Hydration mismatch** — разный HTML на сервере и клиенте:
// ❌ Проблема — Math.random() разный на сервере и клиенте
const id = ref(Math.random())
// ✅ Решение — стабильный ID
const id = useId() // Vue 3.5+**Глобальные объекты браузера** — недоступны на сервере:
// ❌ ReferenceError на сервере
const width = window.innerWidth
// ✅ Защита
const width = typeof window !== 'undefined' ? window.innerWidth : 1200Nuxt 3 — мета-фреймворк для Vue, который автоматизирует SSR:
Реализация renderToString — рендеринг компонентного дерева в HTML-строку, как в @vue/server-renderer
// Реализуем упрощённый renderToString:
// обходим дерево компонентов и генерируем HTML.
// --- Упрощённые VNode-объекты ---
function h(type, props, ...children) {
return {
type,
props: props || {},
children: children.flat().filter(c => c != null && c !== false),
}
}
// --- Renderless-компонент (серверная заглушка) ---
const ServerOnly = {
name: 'ServerOnly',
serverRender: (props, children) => children,
clientRender: () => null,
}
// --- renderToString (SSR) ---
async function renderToString(vnode, isServer = true) {
if (vnode == null || vnode === false) return ''
if (typeof vnode === 'string' || typeof vnode === 'number') {
return String(vnode)
}
const { type, props, children } = vnode
// Функциональный компонент
if (typeof type === 'function') {
const rendered = await type(props, children)
return renderToString(rendered, isServer)
}
// Объект-компонент с render/setup
if (typeof type === 'object' && type !== null) {
// onServerPrefetch — только на сервере
if (isServer && type.serverPrefetch) {
await type.serverPrefetch(props)
}
if (isServer && type.serverRender) {
const rendered = type.serverRender(props, children)
return renderToString(rendered, isServer)
}
if (!isServer && type.clientRender) {
const rendered = type.clientRender(props, children)
if (rendered == null) return ''
return renderToString(rendered, isServer)
}
if (type.render) {
const rendered = type.render(props, children)
return renderToString(rendered, isServer)
}
return ''
}
// HTML-элемент
const selfClosing = ['br', 'hr', 'img', 'input', 'link', 'meta']
const attrs = Object.entries(props)
.filter(([k]) => !k.startsWith('on'))
.map(([k, v]) => {
if (k === 'className') k = 'class'
if (typeof v === 'boolean') return v ? k : ''
return `${k}="${v}"`
})
.filter(Boolean)
.join(' ')
const attrsStr = attrs ? ' ' + attrs : ''
if (selfClosing.includes(type)) {
return `<${type}${attrsStr}>`
}
const innerParts = await Promise.all(
children.map(c => renderToString(c, isServer))
)
const inner = innerParts.join('')
return `<${type}${attrsStr}>${inner}</${type}>`
}
// --- Компоненты с серверной логикой ---
// Компонент с onServerPrefetch
const ProductList = {
name: 'ProductList',
_data: null,
async serverPrefetch(props) {
// Симуляция загрузки данных на сервере
await new Promise(r => setTimeout(r, 10))
ProductList._data = [
{ id: 1, name: 'Товар А', price: 100 },
{ id: 2, name: 'Товар Б', price: 200 },
]
console.log('[SSR] onServerPrefetch: загружены продукты')
},
render(props) {
const items = ProductList._data || []
return h('ul', { class: 'products' },
...items.map(item =>
h('li', { key: item.id, class: 'product' },
h('span', { class: 'name' }, item.name),
' — ',
h('span', { class: 'price' }, item.price + '₽')
)
)
)
}
}
// Компонент только для клиента (интерактивная часть)
const ClientCounter = {
name: 'ClientCounter',
serverRender: () => h('div', { class: 'counter-placeholder' }, 'Загрузка...'),
clientRender: (props) => h('div', { class: 'counter' }, `Счётчик: ${props.initialCount || 0}`),
}
// --- Рендер ---
async function ssrRender() {
const app = h('div', { id: 'app' },
h('h1', {}, 'Интернет-магазин'),
h(ProductList, {}),
h(ClientCounter, { initialCount: 0 }),
h('footer', { class: 'footer' }, '© 2024')
)
console.log('\n=== SSR (сервер) ===')
const serverHtml = await renderToString(app, true)
console.log(serverHtml)
console.log('\n=== CSR (клиент) ===')
const clientHtml = await renderToString(app, false)
console.log(clientHtml)
}
ssrRender()
**CSR (Client-Side Rendering)** — классический SPA:
**SSR (Server-Side Rendering)** — рендер на сервере:
**SSG (Static Site Generation)** — генерация при сборке:
Основной API Vue SSR — функция рендера компонента в HTML-строку:
// server.js (Node.js)
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './App.vue'
const app = createSSRApp(App)
const html = await renderToString(app)
// html — строка с отрендеренным HTML:
// <div id="app" data-v-app=""><h1>Привет!</h1></div>После получения серверного HTML клиент делает его интерактивным:
// client.js
import { createSSRApp } from 'vue'
import App from './App.vue'
// createSSRApp вместо createApp — запускает гидрацию
const app = createSSRApp(App)
app.mount('#app') // Vue "оживляет" существующий HTMLГидрация: Vue проходит по DOM-дереву и "привязывает" реактивное состояние к уже существующим элементам вместо их пересоздания.
// ProductList.vue
const products = ref([])
onServerPrefetch(async () => {
// Выполняется только на сервере, перед рендером
products.value = await fetchProducts()
})
// На клиенте — данные уже в HTML, гидрация восстановит ихimport { onMounted } from 'vue'
// Проверка окружения
if (typeof window !== 'undefined') {
// Только клиентский код
}
// Хуки работают только на клиенте:
onMounted(() => {
// window, document, localStorage — доступны
// onMounted НЕ вызывается при SSR рендере
})
// Динамический импорт для клиентских библиотек:
const Chart = defineAsyncComponent(() => {
if (import.meta.env.SSR) return Promise.resolve({ render: () => null })
return import('./HeavyChart.vue')
})**Hydration mismatch** — разный HTML на сервере и клиенте:
// ❌ Проблема — Math.random() разный на сервере и клиенте
const id = ref(Math.random())
// ✅ Решение — стабильный ID
const id = useId() // Vue 3.5+**Глобальные объекты браузера** — недоступны на сервере:
// ❌ ReferenceError на сервере
const width = window.innerWidth
// ✅ Защита
const width = typeof window !== 'undefined' ? window.innerWidth : 1200Nuxt 3 — мета-фреймворк для Vue, который автоматизирует SSR:
Реализация renderToString — рендеринг компонентного дерева в HTML-строку, как в @vue/server-renderer
// Реализуем упрощённый renderToString:
// обходим дерево компонентов и генерируем HTML.
// --- Упрощённые VNode-объекты ---
function h(type, props, ...children) {
return {
type,
props: props || {},
children: children.flat().filter(c => c != null && c !== false),
}
}
// --- Renderless-компонент (серверная заглушка) ---
const ServerOnly = {
name: 'ServerOnly',
serverRender: (props, children) => children,
clientRender: () => null,
}
// --- renderToString (SSR) ---
async function renderToString(vnode, isServer = true) {
if (vnode == null || vnode === false) return ''
if (typeof vnode === 'string' || typeof vnode === 'number') {
return String(vnode)
}
const { type, props, children } = vnode
// Функциональный компонент
if (typeof type === 'function') {
const rendered = await type(props, children)
return renderToString(rendered, isServer)
}
// Объект-компонент с render/setup
if (typeof type === 'object' && type !== null) {
// onServerPrefetch — только на сервере
if (isServer && type.serverPrefetch) {
await type.serverPrefetch(props)
}
if (isServer && type.serverRender) {
const rendered = type.serverRender(props, children)
return renderToString(rendered, isServer)
}
if (!isServer && type.clientRender) {
const rendered = type.clientRender(props, children)
if (rendered == null) return ''
return renderToString(rendered, isServer)
}
if (type.render) {
const rendered = type.render(props, children)
return renderToString(rendered, isServer)
}
return ''
}
// HTML-элемент
const selfClosing = ['br', 'hr', 'img', 'input', 'link', 'meta']
const attrs = Object.entries(props)
.filter(([k]) => !k.startsWith('on'))
.map(([k, v]) => {
if (k === 'className') k = 'class'
if (typeof v === 'boolean') return v ? k : ''
return `${k}="${v}"`
})
.filter(Boolean)
.join(' ')
const attrsStr = attrs ? ' ' + attrs : ''
if (selfClosing.includes(type)) {
return `<${type}${attrsStr}>`
}
const innerParts = await Promise.all(
children.map(c => renderToString(c, isServer))
)
const inner = innerParts.join('')
return `<${type}${attrsStr}>${inner}</${type}>`
}
// --- Компоненты с серверной логикой ---
// Компонент с onServerPrefetch
const ProductList = {
name: 'ProductList',
_data: null,
async serverPrefetch(props) {
// Симуляция загрузки данных на сервере
await new Promise(r => setTimeout(r, 10))
ProductList._data = [
{ id: 1, name: 'Товар А', price: 100 },
{ id: 2, name: 'Товар Б', price: 200 },
]
console.log('[SSR] onServerPrefetch: загружены продукты')
},
render(props) {
const items = ProductList._data || []
return h('ul', { class: 'products' },
...items.map(item =>
h('li', { key: item.id, class: 'product' },
h('span', { class: 'name' }, item.name),
' — ',
h('span', { class: 'price' }, item.price + '₽')
)
)
)
}
}
// Компонент только для клиента (интерактивная часть)
const ClientCounter = {
name: 'ClientCounter',
serverRender: () => h('div', { class: 'counter-placeholder' }, 'Загрузка...'),
clientRender: (props) => h('div', { class: 'counter' }, `Счётчик: ${props.initialCount || 0}`),
}
// --- Рендер ---
async function ssrRender() {
const app = h('div', { id: 'app' },
h('h1', {}, 'Интернет-магазин'),
h(ProductList, {}),
h(ClientCounter, { initialCount: 0 }),
h('footer', { class: 'footer' }, '© 2024')
)
console.log('\n=== SSR (сервер) ===')
const serverHtml = await renderToString(app, true)
console.log(serverHtml)
console.log('\n=== CSR (клиент) ===')
const clientHtml = await renderToString(app, false)
console.log(clientHtml)
}
ssrRender()
Реализуй функцию `createSSRRenderer()`, которая возвращает объект с методами: `renderToString(vnode)` — рекурсивно рендерит VNode в HTML-строку (строки/числа напрямую, объект с type/props/children — в теги, массивы — конкатенируются). `addContext(key, value)` — добавляет данные в серверный контекст. `getContext()` — возвращает весь контекст. Используй serveHTML-компоненты: объект с полем `ssr: (props, context) => vnode` — вызывай ssr-метод вместо обычного рендера.
В renderToString: if (vnode == null || vnode === false) return "". if (typeof vnode === "string" || typeof vnode === "number") return String(vnode). if (Array.isArray(vnode)) return vnode.map(renderToString).join(""). Для объекта: const { type, props, children } = vnode. Если type.ssr — return renderToString(type.ssr(props, context)). Иначе генерируй HTML-тег как в предыдущих задачах.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке