SSR: когда, зачем и для чего. На примере Vue
Once upon a time Несколько лет назад, когда я только начинал работать с вебом на Java, мы работали с JSP. Вся страница генерировалась на сервере и отправлялась клиенту. Но потом встал вопрос о том, что ответ приходит слишком долго…
Мы начали использовать подход, при котором отдается пустой темплейт страницы, а все данные уже постепенно подгружались Аяксом. Все были счастливы, странички показывались. Пока мы не поняли, что наделали себе за шиворот, так как CSR отрицательно сказывается на поисковой оптимизации и производительности на мобильных устройствах. Но потом я снова услышал про поддержку SSR JS-фреймворками.
И что же получается, история повторяется?
Какие есть принципы работы SSR?
1. Prerendering. В простейшем случае генерируется N HTML-файлов, которые кладутся на сервер и возвращаются как есть — то есть возвращается статика, во время запроса мы ничего не генерируем.
2. Как и в случае с JSP, на сервере генерируется полный HTML со всем контентом и возвращается клиенту. Но, чтобы не генерировать страницу на каждый запрос (коих может быть миллион и наш сервер загнется), давайте добавим кэш прокси. Например, варниш.
Когда может быть применим каждый из этих способов:
1. Когда имеет смысл генерировать пачку HTML-файлов? Очевидно, в том случае, когда данные на сайте меняются чуть реже чем никогда. Например, корпоративный сайт ларька по ремонту обуви, что за углом (да-да, тот дяденька, который меняет набойки в ларьке 2×2 метра, тоже захотел сайт фирмы — и, конечно же, со страницей миссии компании). Для такого сайта вообще не надо заморачиваться на предмет фреймворков, SSR и прочих свистелок, но это сферический пример. Что делать, если у нас блог, в котором 1к постов? Иногда мы их актуализируем, иногда добавляем новые. Сгенерировать 1к+ статичных файлов… Что-то не то. А если мы изменяем пост, то надо перегенерировать определенный файлик. Хм…
2. И вот тут нам подходит второй способ. Где мы генерируем первый раз на лету, а потом кэшируем ответ в проксирующем сервисе. Время кэширования может быть час/два/день — как угодно. Если у нас 10 000 заходов в час на пост (невероятно, правда?), то только первый запрос дойдет до сервера. Остальные получат в ответ кэшированную копию, и наш сервер с большей вероятностью будет жить. В случае обновления какого-то поста нам просто нужно сбросить закэшированную запись, чтобы по следующему реквесту сгенерировалась уже актуальная страница.
Hello world repo.
0) generate hello world
Для быстрого старта сообщество Nuxt подготовило базовые темплейты, установить любой из них можно командой:
$ vue init
По умолчанию предлагается started-template, его и возьмем для нашего примера. Хотя в реальном приложении мы выбрали express-template. Назовем проект незамысловато:
$ vue init nuxt-community/starter-template habr-nuxt-example
$ cd habr-nuxt-example
$ yarn # или npm install, как будет угодно
$ yarn dev
Вжух, мы сгенерировали hello world. Перейдя по урлу, можно увидеть сгенерированную страницу:
1) Webpack и Linting
Nuxt из коробки имеет настроенные вебпак с поддержкой ES6 (babel-loader), Vue однофайловые компоненты (vue-loader), а также SCSS, JSX и прочее.
Если этих возможностей недостаточно, конфиг вебпака можно расширить. Идем в nuxt.config.js, и в build.extend мы имеем возможность модифицировать конфиг.
Для примера добавим линтинг стилей по аналогии с линтингом кода — важный, на наш взгляд, пункт, для поддержания единообразной кодовой базы. Это хорошая привычка, которая поможет избежать многих подводных камней.
Пример расширения конфига (подключение конфиг-файла для дева на основе переменной окружения):
config.plugins.push(
new StylelintPlugin({
files: [
'**/*.vue',
'assets/scss/**/*.scss'
],
configFile: './.stylelintrc.dev.js'
})
)
Остальные изменения можно посмотреть в репо по тегу, эти изменения помогут нам держать стили в порядке.
И пример конфиг-файла линтера: используем Standard JS, как общепринятое в Vue/Nuxt решение:
...
extends: [
- 'plugin:vue/essential'
+ 'standard',
+ 'plugin:vue/recommended'
],
…
2) Для примера работы с данными будем использовать вот это API:
Подключим Axios как плагин, создаем новый файл в директории plugins:
import * as axios from 'axios'
let options = {}
// The server-side needs a full url to works
if (process.server) {
options.baseURL = `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}`
}
export default axios.create(options)
И пример использования:
import axios from '~/plugins/axios'
export default {
async asyncData ({ params }) {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts')
return { data }
}
}
Остальное в репе по тегу.
Цифры загрузки:
1) SSR + Varnish
Первый запрос:
Второй:
2) No-ssr
Второй реквест с фастли
Пустая страница пришла быстро, но потребовалось 2 секунды на то, чтобы сгенерировать на ней контент.
Что в итоге? Мы разобрались, как получить минимально сконфигурированное запускаемое SSR-приложение. Добавили Linting для сохранения стиля кода с самого начала жизни проекта, а также обозначили общую архитектуру. Можно писать свой гугол.