SSR. Рендеринг на стороне сервера

Нужен ли вам SSR?

Перед тем как углубляться в SSR, давайте разберёмся, что это за технология, и когда она может вам понадобиться.

SEO

Google и Bing прекрасно индексируют синхронные JavaScript-приложения. Синхронные здесь — ключевое слово. Если ваше приложение начинается с индикатора загрузки, подгружая данные с помощью ajax-запросов, поисковый робот ждать окончания загрузки не станет.

Это означает, что если данные загружаются асинхронно на страницах, для которых важна оптимальная индексация поисковиками, SSR может стать необходимостью.

Клиенты с медленным соединением

Пользователи могут приходить на ваш сайт из отдалённых районов с медленным соединением — или же просто через медленный мобильный интернет. В таких случаях лучше минимизировать количество и объём запросов, требующихся для отображения основного контента.

Во избежания загрузки целого приложения можно использовать инструменты Webpack для разделения кода, но даже этот подход не будет столь же эффективен, как скачивание уже готового отрендеренного на сервере файла.

Клиенты с устаревшим JavaScript (или вовсе без такового)

В некоторых демографических группах и отдельных регионах единственной опцией для доступа в интернет может оказаться компьютер 1998 года. Несмотря на то, что Vue работает только с IE9+, возможно вам всё же понадобится возможность доставить содержимое страниц в устаревшие браузеры — или хакерам-хипстерам, использующим Lynx в терминале.

Пререндеринг

Если вы интересуетесь SSR только для того, чтобы улучшить SEO на нескольких маркетинговых страницах (напр. /, /about, /contact и т.д.), вам скорее всего будет достаточно пререндеринга. Вместо того чтобы заставлять веб-сервер компилировать HTML на лету, пререндеринг просто однократно строит статические HTML-файлы для указанных путей на этапе сборки. Преимущество пререндеринга — простота, кроме того этот подход позволяет вам оставить фронтенд полностью статичным.

Если вы используете Webpack, пререндеринг несложно добавить при помощи плагина prerender-spa-plugin. Плагин был серьёзнейшим образом протестирован с Vue (вообще-то, его создатель — член основной команды разработки Vue).

Hello World

Если вы дочитали до этого места, пора посмотреть на SSR в действии. Звучит, конечно, сложновато, но для простой демонстрации нужно всего 3 шага:

// Шаг 1: Создадим экземпляр Vue
var Vue = require('vue')
var app = new Vue({
render: function (h) {
return h('p', 'hello world')
}
})
// Шаг 2: Создадим рендерер
var renderer = require('vue-server-renderer').createRenderer()
// Шаг 3: Рендеринг экземпляра в HTML
renderer.renderToString(app, function (error, html) {
if (error) throw error
console.log(html)
// => <p server-rendered="true">hello world</p>
})

Не так уж страшно, правда? Конечно, этот пример намного проще чем большинство приложений. Нам не пришлось волноваться о:

В оставшейся части данного руководства мы пробежимся по некоторым из этих вещей. А когда вы поймёте основы, мы сможем направить вас к более детальной документации и более продвинутым примерам, включая рассмотрение краевых случаев.

Простая реализация SSR на базе веб-сервера Express

Использование термина “рендеринг на сервере”, в отсутствии самого веб-сервера звучит натянуто — давайте это исправим. Мы создадим простейшее SSR-приложение, применяя только ES5 и не используя ни сборщиков, ни плагинов.

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

new Vue({
template: '<div>Вы были здесь {{ counter }} секунд.</div>',
data: {
counter: 0
},
created: function () {
var vm = this
setInterval(function () {
vm.counter += 1
}, 1000)
}
})

Чтобы приложение работало и на клиенте и на сервере, придётся внести несколько модификаций:

Для этого потребуется немного дополнительного кода:

// assets/app.js
(function () { 'use strict'
var createApp = function () {
// ----------------------
// Начало кода приложения
// ----------------------
// Необходимо вернуть основной экземпляр Vue, а у корневого
// элемента должен быть id "app", чтобы клиентская версия
// смогла подхватить работу после загрузки
return new Vue({
template: '<div id="app">Вы были здесь {{ counter }} секунд.</div>',
data: {
counter: 0
},
created: function () {
var vm = this
setInterval(function () {
vm.counter += 1
}, 1000)
}
})
// ---------------------
// Конец кода приложения
// ---------------------
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = createApp
} else {
this.app = createApp()
}
}).call(this)

Теперь, когда у нас есть код приложения, давайте соберём файл index.html:

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Моё приложение на Vue</title>
<script src="/assets/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script src="/assets/app.js"></script>
<script>app.$mount('#app')</script>
</body>
</html>

Если упомянутая папка assets содержит файл нашего приложения app.js, и файл самого Vue, vue.js, должно получиться вполне рабочее одностраничное приложение!

Чтобы включить рендеринг на стороне сервера, остаётся добавить только собственно веб-сервер:

// server.js
'use strict'
var fs = require('fs')
var path = require('path')
// Получаем доступ к Vue глобально для серверной версии app.js
global.Vue = require('vue')
// Получаем HTML-шаблон
var layout = fs.readFileSync('./index.html', 'utf8')
// Создаём рендерер
var renderer = require('vue-server-renderer').createRenderer()
// Создаём Express-сервер
var express = require('express')
var server = express()
// Включаем отдачу статических файлов из директории assets
server.use('/assets', express.static(
path.resolve(__dirname, 'assets')
))
// Обрабатываем все GET-запросы
server.get('*', function (request, response) {
// Рендерим наше приложение в строку
renderer.renderToString(
// Создаём экземпляр приложения
require('./assets/app')(),
// Обрабатываем результат рендеринга
function (error, html) {
// Если при рендеринге произошла ошибка...
if (error) {
// Логируем её в консоль
console.error(error)
// И говорим клиенту, что что-то пошло не так
return response
.status(500)
.send('Server Error')
}
// Отсылаем HTML-шаблон, в который вставлен результат рендеринга приложения
response.send(layout.replace('<div id="app"></div>', html))
}
)
})
// Слушаем 5000-й порт
server.listen(5000, function (error) {
if (error) throw error
console.log('Server is running at localhost:5000')
})

Всё готово! Вот код приложения целиком, на случай если вы захотите клонировать репозиторий и продолжить эксперименты. Когда вы запустите этот код локально, проверить что пререндеринг на стороне сервера действительно работает можно будет кликнув правой кнопкой мышки на странице и выбрав Просмотр исходного кода (или подобный пункт). В body страницы вы увидите вот это:

<div id="app" server-rendered="true">Вы были здесь 0 секунд.</div>

вместо:

<div id="app"></div>

Стриминг

Vue также поддерживает потоковый рендеринг, являющийся предпочтительным для поддерживающих его веб-серверов. HTML в таком случае пишется в ответ сервера в процессе генерации, а не единым куском в конце. В результате запросы обслуживаются быстрее, а негативных побочных эффектов у этого подхода — просто нет.

Для того чтобы включить стриминг в нашем приложении, мы просто заменим блок server.get('*', ...) таким образом:

// Разделим HTML-шаблон на две части
var layoutSections = layout.split('<div id="app"></div>')
var preAppHTML = layoutSections[0]
var postAppHTML = layoutSections[1]
// Обрабатываем все GET-запросы
server.get('*', function (request, response) {
// Рендерим наше приложение в поток
var stream = renderer.renderToStream(require('./assets/app')())
// Пишем в ответ сервера кусочек HTML, идущий до самого приложения
response.write(preAppHTML)
// По мере того, как рендерятся новые кусочки...
stream.on('data', function (chunk) {
// Пишем их в ответ сервера
response.write(chunk)
})
// Когда рендеринг приложения завершён
stream.on('end', function () {
// Дописываем остаток HTML-шаблона и завершаем запись в поток
response.end(postAppHTML)
})
// Если при рендеринге произошла ошибка...
stream.on('error', function (error) {
// Логируем её в консоль
console.error(error)
// И говорим клиенту, что что-то пошло не так
return response
.status(500)
.send('Server Error')
})
})

Как вы видите, по сравнению с предыдущей версией особо ничего не усложнилось, даже если вы раньше не сталкивались с потоками. Мы просто:

  1. Создаём поток
  2. Записываем в поток “открывающий” HTML
  3. По мере готовности записываем в поток части отрендеренного приложения
  4. Записываем в поток “закрывающий” HTML и завершаем поток
  5. Обрабатываем ошибки

Кеширование компонентов

SSR во Vue работает быстро, но можно ещё больше улучшить быстродействие, кешируя результаты рендеринга компонентов. Впрочем, важно понимать, что это — продвинутый механизм, поскольку кеширование неправильных компонентов (или правильных компонентов с неправильными ключами) может привести к ошибкам рендеринга приложения. В частном случае:

Не следует кешировать компоненты, содержащие потомков, чьё состояние зависит от глобального (напр. от состояния хранилища Vuex). Если вы это сделаете, дочерние компоненты (а по сути — всё под-дерево целиком) тоже закешируются. Будьте особенно аккуратны с компонентами, принимающими слоты и/или дочерние компоненты.

Настройка

Не забывая о вышесказанном, вот как работает кеширование компонентов.

Во-первых, в перендерер нужно передать объект кеша. Вот простой пример, использующий lru-cache:

var createRenderer = require('vue-server-renderer').createRenderer
var lru = require('lru-cache')
var renderer = createRenderer({
cache: lru(1000)
})

В этом примере мы позволяем кешировать до 1000 уникальных рендеров. Для других конфигураций, более тонко оперирующих использованием оперативной памяти, см. опции lru-cache.

Далее, для компонентов, которые вы хотели бы кешировать, нужно указать:

Например:

Vue.component({
name: 'list-item',
template: '<li>{{ item.name }}</li>',
props: ['item'],
serverCacheKey: function (props) {
return props.item.type + '::' + props.item.id
}
})

Идеальные кешируемые компоненты

Любой “чистый” компонент можно безопасно кешировать. Чистыми считаются компоненты, гарантирующие рендеринг одинакового HTML для одних и тех же входных параметров. Типичными примерами являются:

Процесс сборки, роутинг и гидрация состояния Vuex

К этому моменту вы должны понимать основы рендеринга на сервере. Однако, при добавлении процесса сборки, роутинга и Vuex в картине появятся новые детали.

Если вы хотите действительно хорошо разобраться в SSR сложных приложений, мы советуем ознакомиться со следующими ресурсами:

Nuxt.js

Правильно настроить все аспекты приложения с рендерингом на стороне сервера и готовое к развёртыванию на production может быть сложной задачей. К счастью, есть отличный проект сообщества, который стремится сделать всё проще: Nuxt.js.

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