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 <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

Вжух, мы сгенерировали 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 секунды на то, чтобы сгенерировать на ней контент.

Conclusion


Что в итоге? Мы разобрались, как получить минимально сконфигурированное запускаемое SSR-приложение. Добавили Linting для сохранения стиля кода с самого начала жизни проекта, а также обозначили общую архитектуру. Можно писать свой гугол.

Комментарии (13)


  1. DreaMinder
    01.10.2018 21:01

    А как варниш узнает что пора инвалидировать кэш?
    У меня на одном проекте работает nuxt-пререндер как кэш… Да, велосипед, но задачу выполняет и можно гибко кодить нужную логику в отличии от прокси-кэшей.


    1. anton_umbrellait
      01.10.2018 21:18

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

      Админка, в которой происходит редактирование контента, на основе этих ключей производила сброс кеша.

      Например, пост мог быть помечен `post/425053 posts channel/vuejs tag/ssr tag/vue`. Разные ключи необходимы, потому что один и тот же пост может быть показан как на детальной странице, так и в списке постов на главной, в списке канала или по тегу.


  1. Aldarund
    01.10.2018 23:22

    Для vue есть nuxt.js. Там ssr из коробки, как и много чего еще


  1. witka
    02.10.2018 00:31

    ребята, в что делать если бекенд на php?


    1. iit
      02.10.2018 06:48

      Можно например не использовать ssr, можно копнуть в сторону react-php и сделать vue php


      А можно написать сервис на js которая будет тупо прослойкой которая стучится на api того-же laravel или yii2


      1. Chris_Griffin
        02.10.2018 11:50

        И это будет CSR, нет?


    1. TheRat
      02.10.2018 10:48

      1. Chris_Griffin
        02.10.2018 11:49

        Вы этим сами пользовались? И как впечатления?


    1. anton_umbrellait
      02.10.2018 12:02
      +1

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


  1. Zero-Tolerance
    03.10.2018 14:25

    Доброго дня, как вы бы реализовали получение данных для всех компонентов на странице во время рендера на стороне сервера?

    Допустим у нас есть layout-default который принимает в себя страницу с постами, имеет в себе меню, какой-то компонент в сайдбаре и что-то еще, что требует обращение к api. Собирать все asyncData в массив промисов и ждать исполнения всех запрос на мой взгляд нерационально, возможно есть хороший способ собрать один запрос к апи, вместо 10?

    Я реализовал это, только не уверен в правильности решения.


    1. DreaMinder
      03.10.2018 14:55

      в nuxt для этого есть action в сторе — nuxtServerInit


    1. anton_umbrellait
      03.10.2018 15:06

      Доброго.

      Если я правильно понял вопрос, то есть возможность использовать nuxtServerInit action. В документации, как раз похожий пример есть:

      actions: {
        async nuxtServerInit({ dispatch }) {
          await dispatch('core/load')
        }
      
        // ...
      }
      


      Действие nuxtServerInit вызывается только на сервере. В свою очередь в нем мы можем
      вызвать другие необходимые нам события, содержащие обращения к API, и вызывающие мутация нашего store.

      Данные, соответственно будут лежать в store, а не локально в компоненте.


      1. Zero-Tolerance
        03.10.2018 15:12

        спасибо