Рендерингом на стороні серверу (SSR)
Огляд
Що таке SSR?
Vue.js - це фреймворк для створення клієнтських застосунків. За замовчуванням компоненти Vue створюють і обробляють DOM у браузері як вихідні дані. Однак також можна зробити рендер тих самих компонентів в рядки HTML на сервері, надіслати їх безпосередньо в браузер і, нарешті, "гідратувати" статичну розмітку в повністю інтерактивний застосунок на клієнті.
Застосунок Vue.js, який рендериться на сервері, також можна вважати "ізоморфним" або "універсальним" в тому сенсі, що більшість коду вашого застосунку виконується як на сервері, так і на клієнті.
Чому SSR?
У порівнянні з клієнтським односторінковим застосунком (SPA), в основному є такі переваги SSR:
Швидший час до готового вмісту: це більш помітно при повільному з'єднанні або повільних пристроях. Для розмітки, яка рендериться на стороні серверу, не потрібно чекати, поки весь JavaScript буде завантажено та виконано, тому ваш користувач швидше побачить повністю готову сторінку. Крім того, вибірка даних виконується на стороні сервера для першого відвідування, яке, ймовірно, має швидше з'єднання з вашою базою даних, ніж клієнт. Загалом це призводить до покращення показників Core Web Vitals, покращення взаємодії з користувачем і може бути критичним для застосунків, де час до вмісту безпосередньо пов'язаний із коефіцієнтом конверсії.
Уніфікована ментальна модель: ви можете використовувати ту саму мову та ту саму декларативну, компонентно-орієнтовану ментальну модель для розробки всього застосунку, замість того, щоб стрибати між системою шаблонів серверного фреймворку і клієнтським фреймворком.
Краща SEO: сканери пошукової системи безпосередньо побачать повністю рендерину сторінку.
TIP
Зараз Google і Bing можуть нормально індексувати синхронні застосунки JavaScript. І ключове слово тут — синхронні. Якщо ваш застосунок починає завантажуватися, а потім отримує вміст через Ajax, сканер не чекатиме, поки ви закінчите. Це означає, що якщо у вас є асинхронний вміст на сторінках, де важлива SEO, може знадобитися SSR.
Є також і деякі компроміси, які слід враховувати при використанні SSR:
Обмеження розробки. Специфічний код браузера, можна використовувати лише в межах певних хуків життєвого циклу; деяким зовнішнім бібліотекам може знадобитися спеціальна обробка, щоб їх можна було запускати в застосунку, що рендериться на сервері.
Більш складні вимоги до налаштування збірки та розгортання. На відміну від повністю статичного SPA, який можна розгорнути на будь-якому статичному файловому сервері, застосунок, що рендериться на сервері, потребує середовища, де може працювати сервер Node.js.
Більше навантаження на сервер. Рендеринг повного застосунку в Node.js буде більш інтенсивним для процесора, ніж просто обслуговування статичних файлів, тому, якщо ви очікуєте високий трафік, будьте готові до відповідного навантаження на сервер і мудро використовуйте стратегії кешування.
Перш ніж використовувати SSR у своєму застосунку, перше, що ви повинні запитати - чи дійсно це вам потрібне. Здебільшого це залежить від того, наскільки важливий час для вмісту для вашого застосунку. Наприклад, якщо ви створюєте внутрішню інформаційну панель, де додаткові кілька сотень мілісекунд при початковому завантаженні не мають такого великого значення, SSR буде надлишковим. Однак у випадках, коли час до вмісту є абсолютно критичним, SSR може допомогти вам досягти найкращої продуктивності початкового завантаження.
SSR чи SSG
Генерація статичного сайту (SSG), яка також називається попереднім рендерингом, є ще одним популярним методом створення швидких веб-сайтів. Якщо дані, необхідні для рендерингу сторінки сервером, однакові для кожного користувача, тоді замість того, щоб рендерити сторінку щоразу, коли надходить запит, ми можемо завчасно зробити її рендер лише один раз під час процесу створення. Попередньо відрендерені сторінки генеруються та подаються як статичні файли HTML.
SSG зберігає ті самі характеристики продуктивності застосунків SSR: вона забезпечує чудову продуктивність часу до готового вмісту. У той же час, це дешевше та легше розгортати, ніж застосунки SSR, оскільки результатом є статичний HTML і ресурси. Ключове слово тут статичний: SSG можна застосовувати лише до сторінок, які використовують статичні дані, тобто дані, які відомі під час створення та не змінюються між розгортаннями. Щоразу, коли дані змінюються, потрібне нове розгортання.
Якщо ви досліджуєте SSR лише для покращення SEO кількох маркетингових сторінок (наприклад, /
, /about
, /contact
тощо), тоді вам, мабуть, потрібен SSG замість SSR. SSG також чудово підходить для веб-сайтів із вмістом, таких як сайти документації чи блоги. Фактично, цей веб-сайт, який ви зараз читаєте, статично згенерований за допомогою VitePress, генератора статичних сайтів на основі Vue.
Базовий посібник
Рендеринг застосунку
Давайте подивимося на найпростіший приклад Vue SSR у дії.
- Створіть нову директорію та перейдіть до неї
cd
- Виконайте
npm init -y
- Додайте
"type": "module"
доpackage.json
щоб Node.js працював у модульному режимі ES. - Виконайте
npm install vue
- Створіть файл
example.js
:
js
// це виконується у Node.js на сервері.
import { createSSRApp } from 'vue'
// API серверного рендерингу Vue надається `vue/server-renderer`.
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})
Далі виконайте:
sh
> node example.js
Він повинен надрукувати наступне в командному рядку:
<button>1</button>
renderToString()
приймає екземпляр застосунку Vue та повертає Promise, який надає рендериний HTML застосунку. Також можна транслювати рендеринг за допомогою Node.js Stream API або Web Streams API. Для повної інформації перегляньте SSR API Reference.
Потім ми можемо перемістити код Vue SSR в обробник запитів на сервер, який загортає розмітку застосунку в повний HTML-код сторінки. Ми будемо використовувати express
для наступних кроків:
- Виконайте
npm install express
- Створіть файл
server.js
з наступним вмістом:
js
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const server = express()
server.get('/', (req, res) => {
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Приклад Vue SSR</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
})
server.listen(3000, () => {
console.log('готово')
})
Нарешті, виконайте node server.js
та перейдіть по посиланню http://localhost:3000
. Ви маєте побачити робочу сторінку з кнопкою.
Гідрація на стороні клієнту
Якщо ви натиснете кнопку, ви помітите, що число не зміниться. HTML повністю статичний на клієнті, оскільки ми не завантажуємо Vue у браузері.
Щоб зробити застосунок на стороні клієнта інтерактивним, Vue має виконати гідрацію. Під час гідрації він створює той самий застосунок Vue, який було запущено на сервері, зіставляє кожен компонент із вузлами DOM, якими він має керувати, і приєднує слухачі подій DOM.
Щоб змонтувати застосунок в режимі гідрації, нам потрібно використовувати createSSRApp()
замість createApp()
:
js
// це виконається в браузері.
import { createSSRApp } from 'vue'
const app = createSSRApp({
// ...той самий застосунок, що й на сервері
})
// монтування застосунку SSR на клієнті припускає,
// що рендеринг HTML було виконано попередньо, і
// виконає гідрацію замість монтування нових
// вузлів DOM.
app.mount('#app')
Структура коду
Зверніть увагу, що нам потрібно повторно використовувати ту саму реалізацію застосунку, що й на сервері. Саме тут ми повинні почати думати про структуру коду в застосунку SSR - як нам спільно використовувати той самий код застосунку між сервером і клієнтом?
Тут ми продемонструємо найпростіше налаштування. По-перше, давайте розділимо логіку створення застосунку в окремий файл app.js
:
js
// app.js (розподіляється між сервером і клієнтом)
import { createSSRApp } from 'vue'
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}
Цей файл і його залежності спільно використовуються між сервером і клієнтом - ми називаємо їх універсальним кодом. Під час написання універсального коду вам слід звернути увагу на кілька речей, які ми обговоримо нижче.
Наш клієнт імпортує універсальний код, створює застосунок та виконує монтування:
js
// client.js
import { createApp } from './app.js'
createApp().mount('#app')
Та сервер використовує ту саму логіку створення застосунку в обробнику запитів:
js
// server.js (невідповідний код пропущено)
import { createApp } from './app.js'
server.get('/', (req, res) => {
const app = createApp()
renderToString(app).then(html => {
// ...
})
})
Крім того, щоб завантажити файли клієнта в браузер, нам також потрібно:
- Обслуговувати клієнтські файли шляхом додавання
server.use(express.static('.'))
уserver.js
. - Завантажити введення клієнту, додавши
<script type="module" src="/client.js"></script>
до оболонки HTML. - Підтримувати використання
import * from 'vue'
у браузері, додавши Карти імпорту до оболонки HTML.
Спробуйте завершений приклад у StackBlitz. Кнопка тепер інтерактивна!
Рішення вищого рівня
Перехід від прикладу до готового застосунку SSR до виробництва передбачає набагато більше. Нам потрібно буде:
Підтримка Vue SFC та інших вимог на етапі збірки. Фактично, нам потрібно буде скоординувати дві збірки для одного застосунку: одну для клієнта, а іншу для сервера.
TIP
Компоненти Vue компілюються по-іншому, коли використовуються для SSR – шаблони компілюються в конкатенації рядків замість рендер-функцій Virtual DOM для більш ефективної працездатності рендерингу.
У обробнику запитів сервера рендерінг HTML із правильними посиланнями на ресурси на стороні клієнта та оптимальними підказками ресурсів. Нам також може знадобитися перемикатися між режимами SSR і SSG або навіть змішувати обидва в одному застосунку.
Універсально керувати маршрутизацією, отриманням даних і керуванням станом сховищ.
Повна реалізація буде досить складною і залежить від цілого ланцюжка інструментів збирання, з яким ви обрали працювати. Тому ми дуже рекомендуємо вибрати вищий рівень, перевірене рішення, яке абстрагує складність для вас. Нижче ми представимо кілька рекомендованих рішень SSR в екосистемі Vue.
Nuxt
Nuxt це фреймворк вищого рівня, створений на основі екосистеми Vue, який забезпечує добре налагоджений досвід розробки для написання універсальних застосунків Vue. Більш за це, ви також можете використовувати його як генератор статичного сайту! Ми настійно рекомендуємо його спробувати.
Quasar
Quasar це комплексне рішення на основі Vue, яке дозволяє орієнтуватися на SPA, SSR, PWA, мобільні та настільні застосунки, та розширення браузера, використовуючи одну кодову базу. Воно не лише керує налаштуваннями збірки, але й надає повну колекцію компонентів інтерфейсу користувача, сумісних із Material Design.
Vite SSR
Vite забезпечує вбудовану підтримку рендерингу Vue на стороні сервера, але вона навмисно низькорівнева. Якщо ви маєте намір використовувати безпосередньо Vite, перевірте vite-plugin-ssr, плагін спільноти, який абстрагує для вас багато складних деталей.
Ви також можете знайти приклад проєкту Vue + Vite SSR з ручним налаштуванням тут, який може бути основою для розробки. Зауважте, що це рекомендовано, лише якщо ви маєте досвід роботи з інструментами SSR / збирання та дійсно хочете мати повний контроль над архітектурою вищого рівня.
Написання коду, сприятливого для SSR
Незалежно від ваших налаштувань збірки чи вибору фреймворку вищого рівня, є деякі принципи, які застосовуються до всіх застосунків Vue SSR.
Реактивність на сервері
Під час SSR кожна URL-адреса запиту зіставляється з бажаним станом нашого застосунку. Тут немає взаємодії з користувачем та немає оновлень DOM, тому на сервері немає потреби в реактивності. За замовчуванням реактивність вимкнена під час SSR для підвищення продуктивності.
Хуки життєвого циклу компонентів
Оскільки немає динамічних оновлень, хуки життєвого циклу, такі як onMounted
or onUpdated
НЕ буде викликано під час SSR і буде виконано лише на клієнті.
Слід уникати коду, що робить побічні ефекти, які потребують очищення у setup()
або у кореневій області <script setup>
. Прикладом таких побічних ефектів є встановлення таймерів за допомогою setInterval
. У коді лише на стороні клієнта ми можемо встановити таймер, а потім завершити його в onBeforeUnmount
або onUnmounted
. Однак, оскільки під час SSR хуки відмонтування ніколи не викликатимуться, таймери залишаться назавжди. Щоб уникнути цього, натомість перемістіть свій код з подібним побічним ефектом у onMounted
.
Доступ до специфічних для платформи API
Універсальний код не може передбачити доступ до специфічних для платформи API, тому, якщо ваш код напряму використовує глобальні об'єкти, які доступні лише для браузера, такі як window
або document
, вони викликатимуть помилки під час виконання в Node.js, і навпаки.
Для завдань, які спільно виконуються між сервером і клієнтом, але з різними API платформами, рекомендується загортати специфічні для платформи реалізації в універсальний API або використовувати бібліотеки, які роблять це для вас. Наприклад, ви можете використовувати node-fetch
, щоби використовувати той самий API запиту даних і на сервері, і на клієнті.
Для лише браузерних API загальним підходом є відкладений доступ до них всередині лише клієнтських хуків життєвого циклу, таких як onMounted
.
Зауважте, що якщо стороння бібліотека не написана з урахуванням універсального використання, її може бути складно інтегрувати в серверну програму. Ви можете змусити це працювати, імітуючи деякі глобальні значення, але це буде хакерським і може заважати коду виявлення середовища інших бібліотек.
Забруднення стану перехресного запиту
У розділі "керування станом" ми надали простий шаблон керування станом за допомогою API реактивності. У контексті SSR цей шаблон вимагає деяких додаткових коригувань.
Шаблон оголошує спільний стан у кореневій області модуля JavaScript. Це робить їх синглтонами, тобто існує лише один екземпляр реактивного об'єкта протягом усього життєвого циклу нашого застосунку. Це працює належним чином у застосунку Vue на стороні клієнта, оскільки модулі в нашому застосунку оновлюються для кожного відвідування сторінки браузера.
Однак у контексті SSR модулі додатку зазвичай ініціалізуються лише один раз на сервері під час його ініціалізації. Ті самі екземпляри модуля будуть повторно використовуватися для кількох запитів до сервера, як і наші синглтони об'єктів стану. Якщо ми змінимо спільний синглтон стану з даними, які є специфічними для одного користувача, вони можуть випадково просочитися до запиту від іншого користувача. Ми називаємо це перехресним запитом забруднення стану.
Ми можемо технічно повторно ініціалізувати всі модулі JavaScript за кожним запитом, як це робимо в браузерах. Однак ініціалізація модулів JavaScript може бути дорогою, тому це значно вплине на продуктивність сервера.
Рекомендованим рішенням є створення нового екземпляра всього застосунку, включаючи маршрутизатор і глобальні сховища, за кожним запитом. Тоді, замість того, щоб безпосередньо імпортувати його в наші компоненти, ми надаємо спільний стан за допомогою надавання рівня застосунку і вводимо його в компоненти, яким це потрібно:
js
// app.js (розподіляється між сервером і клієнтом)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'
// викликається за кожним запитом
export function createApp() {
const app = createSSRApp(/* ... */)
// створює новий екземпляр сховища за кожним запитом
const store = createStore(/* ... */)
// вводить сховище на рівні app
app.provide('store', store)
// також виставляє сховище для цілей гідрації
return { app, store }
}
Бібліотеки керування станом, такі як Pinia, розроблені з урахуванням цього. Зверніться до посібника Pinia з SSR, щоб дізнатися більше.
Невідповідність гідрації
Якщо структура DOM HTML з попереднім рендерингом не відповідає очікуваному результату застосунку на стороні клієнта, виникне помилка невідповідності гідрації. Невідповідність гідрації найчастіше виникає через такі причини:
Шаблон містить недійсну вкладену структуру HTML, а рендериний HTML було "виправлено" власною поведінкою браузера при синтаксичному аналізі HTML. Наприклад, загальна помилка така, що
<div>
не може бути розміщений всередині<p>
:html<p><div>привіт</div></p>
Якщо ми створимо це в нашому HTML, який рендериться сервером, браузер завершить перший
<p>
, коли зустрінеться<div>
, і розбере його в таку структуру DOM:html<p></p> <div>привіт</div> <p></p>
Дані, які використовуються під час рендерингу, містять випадково згенеровані значення. Оскільки один і той самий застосунок запускатиметься двічі — один раз на сервері та один раз на клієнті — випадкові значення не можуть бути гарантовано однаковими між двома запусками. Є два способи уникнути розбіжностей, спричинених випадковими значеннями:
Використовувати
v-if
+onMounted
, щоб рендерити ту частину, яка залежить від випадкових значень, лише на клієнті. Ваш фреймворк також може мати вбудовані функції, щоб полегшити це, наприклад, компонент<ClientOnly>
у VitePress.Використовувати бібліотеку генерації випадкових чисел, яка підтримує генерацію з початковими значеннями, і гарантує, що виконання сервера та виконання клієнта використовують одне і те ж початкове значення (наприклад, шляхом включення початкового значення в серіалізований стан і отримавши його на клієнті).
Сервер і клієнт знаходяться в різних часових поясах. Іноді нам може знадобитися перетворити мітку часу на місцевий час користувача. Однак часовий пояс під час запуску сервера та часовий пояс під час запуску клієнта не завжди однакові, і ми можемо не знати точно часовий пояс користувача під час запуску сервера. У таких випадках перетворення місцевого часу також має виконуватися лише для клієнтської сторони.
Коли Vue виявляє невідповідність гідрації, він намагатиметься автоматично відновити та налаштувати попередньо відрендерену DOM відповідно до стану на стороні клієнта. Це призведе до деякої втрати продуктивності рендерингу через відкидання неправильних вузлів і монтування нових вузлів, але в більшості випадків застосунок має продовжувати працювати належним чином. Тим не менш, найкраще усунути невідповідності гідрації під час розробки.
Спеціальні директиви
Оскільки більшість призначених для користувача директив передбачає пряме маніпулювання DOM, вони ігноруються під час SSR. Однак, якщо ви хочете вказати, як користувацька директива повинна бути відрендерена (тобто, які атрибути вона має додати до відрендериного елемента), ви можете використати хук директиви getSSRProps
:
js
const myDirective = {
mounted(el, binding) {
// реалізація на стороні клієнта:
// оновлення DOM безпосередньо
el.id = binding.value
},
getSSRProps(binding) {
// реалізація на стороні сервера:
// повернути реквізити для візуалізації.
// getSSRProps отримує лише прив'язування директиви.
return {
id: binding.value
}
}
}
Телепорти
Телепорти вимагають особливого поводження під час SSR. Якщо рендериний застосунок містить телепорти, телепортований вміст не буде частиною відрендереного рядка. Простішим рішенням є умовне відтворення телепорту під час монтування.
Якщо вам все-таки потрібно гідратувати телепортований вміст, він доступний у властивості teleports
об'єкта контексту SSR:
js
const ctx = {}
const html = await renderToString(app, ctx)
console.log(ctx.teleports) // { '#teleported': 'телепортований вміст' }
Вам потрібно вставити розмітку телепорту в правильне місце в HTML-коді вашої фінальної сторінки, подібно до того, як вам потрібно вставити розмітку основного застосунку.
TIP
Уникайте розміщення у body
, коли використовуєте телепорти та SSR разом - зазвичай <body>
матиме інший вміст, який відрендериться сервером, що унеможливлює телепортам визначити правильне початкове місце для гідрації.
Натомість віддавайте перевагу спеціальному контейнеру, наприклад, <div id="teleported"></div>
, який матиме лише телепортований вміст.