Мы начали использовать подход, при котором отдается пустой темплейт страницы, а все данные уже постепенно подгружались Аяксом. Все были счастливы, странички показывались. Пока мы не поняли, что наделали себе за шиворот, так как 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 <template-name> <project-name>
По умолчанию предлагается started-template, его и возьмем для нашего примера. Хотя в реальном приложении мы выбрали express-template. Назовем проект незамысловато:
$ vue init nuxt-community/starter-template habr-nuxt-example
$ cd habr-nuxt-example
$ yarn # или npm install, как будет угодно
$ yarn dev
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 секунды на то, чтобы сгенерировать на ней контент.
Conclusion
Что в итоге? Мы разобрались, как получить минимально сконфигурированное запускаемое SSR-приложение. Добавили Linting для сохранения стиля кода с самого начала жизни проекта, а также обозначили общую архитектуру. Можно писать свой гугол.
Комментарии (13)
witka
02.10.2018 00:31ребята, в что делать если бекенд на php?
iit
02.10.2018 06:48Можно например не использовать ssr, можно копнуть в сторону react-php и сделать vue php
А можно написать сервис на js которая будет тупо прослойкой которая стучится на api того-же laravel или yii2
anton_umbrellait
02.10.2018 12:02+1СтрадатьНа самом деле зависит от многих факторов и от того, чего вы хотите добиться. В простейшем случае, можно оставить серверный рендеринг на PHP, а также выставить дублирующие API ендпоинты. Страница отрендерилась, клиентский фреймворк дальше работает как SPA, обращаясь к API. Но в этом случае придется дублировать как контроллеры на сервере, ведь нужно возвращать как сырые данные, так и полностью отрендеренную страницу, а также поддерживать клиентское приложение.
В целом, если нет понимания что делать, лучше оставить как есть.
Zero-Tolerance
03.10.2018 14:25Доброго дня, как вы бы реализовали получение данных для всех компонентов на странице во время рендера на стороне сервера?
Допустим у нас есть layout-default который принимает в себя страницу с постами, имеет в себе меню, какой-то компонент в сайдбаре и что-то еще, что требует обращение к api. Собирать все asyncData в массив промисов и ждать исполнения всех запрос на мой взгляд нерационально, возможно есть хороший способ собрать один запрос к апи, вместо 10?
Я реализовал это, только не уверен в правильности решения.anton_umbrellait
03.10.2018 15:06Доброго.
Если я правильно понял вопрос, то есть возможность использовать nuxtServerInit action. В документации, как раз похожий пример есть:
actions: { async nuxtServerInit({ dispatch }) { await dispatch('core/load') } // ... }
Действие nuxtServerInit вызывается только на сервере. В свою очередь в нем мы можем
вызвать другие необходимые нам события, содержащие обращения к API, и вызывающие мутация нашего store.
Данные, соответственно будут лежать в store, а не локально в компоненте.
DreaMinder
А как варниш узнает что пора инвалидировать кэш?
У меня на одном проекте работает nuxt-пререндер как кэш… Да, велосипед, но задачу выполняет и можно гибко кодить нужную логику в отличии от прокси-кэшей.
anton_umbrellait
В данном случае мы использовали суррогатные ключи, по типу контента, персональный ключ на основе id, а также еще пару специфичных.
Админка, в которой происходит редактирование контента, на основе этих ключей производила сброс кеша.
Например, пост мог быть помечен `post/425053 posts channel/vuejs tag/ssr tag/vue`. Разные ключи необходимы, потому что один и тот же пост может быть показан как на детальной странице, так и в списке постов на главной, в списке канала или по тегу.