Nuxt.js - быстрый старт

Продолжаем цикл заметок про Vue + Nuxt.

После вводного рассказа о положении дел на рынке, пришла пора попробовать нашего монстрика в деле. Сделать это совсем несложно — нам нужен только установленный Node.js. Дальше идём на nuxtjs.org/guide/installation и читаем, что нужно сделать.
Внимание, сайт хостится на Digital Ocean, который переодически блокируется нашим грозным РКН, так что вам может потребоваться VPN. Лично я читаю документацию в Opera — VPN там встроенный.

Дальше делаем npx create-nuxt-app и отвечаем на вопросы. Лично я выбираю пакетный менеджер Yarn и UI фреймворк Bootstrap-Vue. Из дополнений тащим Axios для сетевых запросов и поддержку PWA. Линтер и тесты нам пока ни к чему, режим работы — Universal (про это позже).



Сразу после установки всех пакетов приложение уже можно запустить в режиме разработки: yarn dev. Поднимается локальный веб-сервер, слушающий порт 3000, нам нужно просто открыть его в браузере.


Собственно, приложение уже работает, дальше просто открываем любимый редактор и редактируем файлы. Так как мы в 2019 году, то изменения сразу применяются на странице, ничего для этого делать не нужно.

Структура проекта


Одна из самых классных вещей в Nuxt, по крайне мере, для меня, это структуризация проекта. Когда я писал на чистом Vue, то пытался придумать что-то такое же, только более корявое. А тут — всё уже готово.

Итак, у нас есть следующие директории:
  • assets — директория с картинками и стилями. Отсюда они будут импортироваться и включаться в проект.
  • components — тут будут лежать переиспользуемые компоненты, в терминологии MODX это сниппеты и чанки
  • layouts — шаблоны страниц
  • middleware — скрипты, которые запускаются перед рендером страниц, можно сказать, что это плагины в MODX
  • pages — сами страницы сайта (ресурсы), по которым автоматически строятся маршруты
  • plugins — js плагины с любым фнукционалом, расширяющие проект. Например, подключение сторонних компонентов из npmjs.org
  • static — статичные файлы, которые не включаются в проект, а будут доступны по прямым ссылкам, например robots.txt
  • store — хранилище Vuex, по умолчанию отключенное и нам пока не нужное
Как видите, перед нами уже готовый сайт, который вполне себе работает на вашей локальной машине. Большинство директорий пока пусты, и хранят только readme.

Давайте попробуем что-нибудь написать!

Однофайловые компоненты


Vue.js, а соответственно, и Nuxt.js предлагают использовать очень классную штуку — однофайловые компоненты, в которых у вас есть 3 блока:
  • template — html шаблон, который использует переменные из js
  • script — сам js код компонента, экспортиующий переменные для шаблона
  • style — стили оформления css (можно настроить и компиляцию sass\scss)
Как видите, в отличие от MODX, здесь чанки, сниппеты и стили находятся в одном месте, что позволяет удобно разделять функциональность проекта по независимым модулям.

Давайте поменяем уже наконец файл pages/index.vue:
<template>
  <div class="container">
    <div class="alert alert-info mt-5 p-5 text-center">
      Привет, друг! Сейчас {{date.getHours()}} и {{date.getMinutes()}} минут.
    </div>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                date: new Date(),
            }
        }
    }
</script>
и на странице вы увидите приветствие с текущим временем.

Что здесь произошло? Мы просто вернули переменную date через функцию data() компонента Vue. Подробнее (и гораздо познавательнее!) можно прочитать в официальном руководстве.

Давайте теперь усложним пример — сделаем запрос на удалённый сервер и выведем результат:
<template>
  <div class="container">
    <div class="mt-5 p-5 text-center">
      {{posts}}
    </div>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                posts: [],
            }
        },
        created() {
            this.$axios.get('https://jsonplaceholder.typicode.com/posts')
                .then(res => {
                    this.posts = res.data
                })
        }
    }
</script>

Мы выполнили запрос на фейковый REST API и вернули данные в функции created(), которая срабатывает при создании компонента.

При этом объявили пустую переменную posts заранее, так как ссылаемся на неё в шаблоне.

Компонент создаётся, страница отрисовывается с пустым posts, затем идёт запрос на удалённый сервер, переменная posts заполняется данными и страница их отображает автоматически. Ничего делать не нужно, просто код шаблона связан с переменной и когда она меняется — меняется и готовый HTML. Это называется реактивностью.

Продолжаем усложнять:
<template>
  <div class="container">
    <div class="mt-5 p-5">
      <div class="mt-2" v-for="post in posts">
        <p>{{post.id}}. <strong>{{post.title}}</strong></p>
        <pre>{{post.body}}</pre>
      </div>
    </div>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                posts: [],
            }
        },
        created() {
            this.$axios.get('https://jsonplaceholder.typicode.com/posts')
                .then(res => {
                    this.posts = res.data
                })
        }
    }
</script>
На этот раз нижняя часть вообще не изменилась, потому что она как получала данные, так и получает. Всем оформлением занимается шаблон (ну, примерно как Fenom в MODX)

Мы используем проход по массиву v-for, и обращение к его элементам, чтобы отрисовать данные.
Это всё, конечно же, есть в документации Vue.

Реактивность, как вы понимаете, работает в обе стороны. Мы можем менять переменную posts из шаблона:
<template>
  <div class="container">
    <div class="mt-5 p-5">
      <button class="btn btn-secondary" @click="posts = []">Очистить posts</button>
      <button class="btn btn-secondary ml-2"  @click="loadPosts">Загрузить заново</button>

      <div class="mt-2" v-for="post in posts">
        <p>{{post.id}}. <strong>{{post.title}}</strong></p>
        <pre>{{post.body}}</pre>
      </div>
    </div>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                posts: [],
            }
        },
        methods: {
          loadPosts() {
              this.$axios.get('https://jsonplaceholder.typicode.com/posts')
                  .then(res => {
                      this.posts = res.data
                  })
          }
        },
        created() {
            this.loadPosts()
        }
    }
</script>

Здесь мы переносим загрузку заметок в объект methods, который содержит функции нашего компонента. Теперь мы можем дергать loadPosts как при создании компонента, так и при нажатии кнопки.

Ну а другая кнопка просто обнуляет массив posts, для этого даже отдельный метод создавать необязательно — можно сразу в шаблоне это делать, ведь там просто присвоение значения, без сложной логики.

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

Ну и напоследок уже продвинутый пример:
<template>
  <div class="container">
    <div class="mt-5 p-5">
      <button class="btn btn-secondary" @click="posts = []">Очистить posts</button>
      <button class="btn btn-secondary ml-2"  @click="loadPosts">Загрузить заново</button>

      <b-table
        class="mt-5"
        empty-text="Нет записей для вывода"
        :show-empty="true"
        :items="posts"
        :fields="fields"
        :current-page="page"
        :per-page="limit">
        <template v-slot:row-details="row">
          <pre>{{row.item.body}}</pre>
        </template>
        <template v-slot:cell(actions)="row">
          <button class="btn btn-secondary btn-sm" @click="row.toggleDetails">
              {{ row.detailsShowing ? 'Скрыть' : 'Показать' }} текст
          </button>
        </template>
      </b-table>
    
      <b-pagination
        v-if="total > 0"
        v-model="page"
        :per-page="limit"
        :total-rows="total"
      />
    </div>
  </div>
</template>

<script>
    export default {
        data() {
            return {
                posts: [],
                fields: [
                    {key: 'id', title: 'Id', sortable: true},
                    {key: 'title', title: 'Название', sortable: true},
                    {key: 'actions', title: 'Действия'},
                ],
                page: 1,
                limit: 15,
            }
        },
        computed: {
            total() {
                return this.posts.length
            }
        },
        methods: {
          loadPosts() {
              this.$axios.get('https://jsonplaceholder.typicode.com/posts')
                  .then(res => {
                      this.posts = res.data
                  })
          }
        },
        created() {
            this.loadPosts()
        }
    }
</script>

Здесь нужно много пояснений.


Во-первых, мы используем готовый компонент b-table из Bootstrap-Vue, у которого просто огромное количество настроек и параметров. Ближайший аналог — это гриды в ExtJS.

При вызове компонента эти самые параметры указываются ему в теге. Причём, можно указывать как обычные строки, так и переменные через двоеточие, что является сокращением от v-bind. То есть, если мы указали <component param="true"/>, то это строка "true", а если <component :param="true"/> то это уже булево true.

В моём примере мы так указываем и булево значение, и переменные из функции data(). Обратите внимание, что на странице вызывается еще и второй компонент — b-pagination. Это во-вторых.

Ему нужно передать общее количество выводимых результатов. Но как мы их узнаем, если данные грузятся асинхронно, и на момент рендера страницы массив posts будет пуст? Тут нас выручает еще одна способность Vue — массив переменных computed. Это, по сути, не совсем переменные, а функции, которые дёргаются каждый раз, а потому выдают вычисляемые значения. А нашем случае переменная total будет зависеть от posts и каждый раз возвращать текущее количество элементов массив.

При создании страницы computed переменные смешиваются с обычными из data(), так что их имена не должны повторяться!

На эту же переменную мы завязываем и само отображение компонента через v-if. Как только posts очищается нажатием кнопки, пагинация сразу пропадает со страницы.

Ну и в-третьих, теперь нам нужно как-то связать эти 2 независимых компонента на странице. Как будет нажатие кнопки в пагинации менять вывод таблицы? А вот еще через одну переменную — page. Она указана как :current-page для b-table, и как v-model у b-pagination. Разница в том, что поле v-model может быть у компонента только одно, и оно изменяемо внутри этого компонента.

При переключении страницы пагинации page меняется, это видит b-table и показывает другие данные. Реактивность во всей красе.

Более того, мы можем добавить третью кнопку на страницу и менять page при нажатии на неё — это будет влиять и на таблицу, и на пагинацию. Потому что тут всё связано, не развяжешь.

Ну и последнее, что нужно пояснить — это слоты внутри таблицы. При создании компонента вы можете предусмотреть в нём изменяемые куски оформления, что-то типа плейсхолдеров, чтобы родительский компонент мог их подставить. Таким образом мы можем оформить каждую колонку b-table, даже несуществующую actions
<template v-slot:cell(actions)="row">
    <button class="btn btn-secondary btn-sm" @click="row.toggleDetails">
        {{ row.detailsShowing ? 'Скрыть' : 'Показать' }} текст
    </button>
</template>
При нажатии она меняет статус специального внутреннего шаблона row-details — это тоже фишка b-table.

И вот результат (gif, 2.5 Mb)


Заключение


Вот так мы и написали простенький проект на Nuxt.js на 60 строк с приличным функционалом.

Теперь можно выключить dev сервер и сгенерировать статическую версию командой yarn generate — и после выполнения команды вы получите всё нужное в директории dist:

Помните, в начале мы выбирали режим Universal? Вот это оно и есть — серверный рендеринг.

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

Итак, сборщик компилирует ваш сайт в статический HTML, и вы можете его выгрузить куда-нибудь типа Github Pages, где он будет спокойно работать без PHP и Node.js. Вот, что получилось у меня — test-nuxt.bezumkin.ru

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

Текущий счёт: 1400руб.
Василий Наумкин
24 сентября 2019, 16:34
modx.pro
12
584
+32
Поблагодарить автора Отправить деньги

Комментарии: 14

Павел Бигель
24 сентября 2019, 16:47
+1
Если сесть и вменяемо дописать мою статью про .NET, то в принципе на основании наших статей можно собрать сайт «фром скрэтч» как говорится :)
    Руслан Сафин
    24 сентября 2019, 19:53
    0
    Если речь идет о nuxt (ssr из коробки), то лучше получать данные методом asyncData(). Таким образом они будут рендерится на сервере. В методах created() и mounted() данные получаются клиентом (в браузере)
      srs
      srs
      24 сентября 2019, 20:59
      0
      Уверен, что этот довольно очевидный момент специально был опущен. Возможно в одном из следующих статей будет сказано и о asyncData, и о fetch, и вообще, много чего интересного. Если человек заинтересовался nuxt, то в документации все подробно описано. Лично я надеюсь однажды посмотреть как Василий готовит бэк и женит его с nuxt.
        Руслан Сафин
        24 сентября 2019, 23:28
        0
        Комментарий оставил не для Василия, уверен что он это знает))) Комментарий для новичков, которые захотят освоить nuxtjs. Когда я начал его изучать, не сразу дошло где какие методы использовать)))
          srs
          srs
          25 сентября 2019, 01:04
          0
          Прошу прощенья, я не верно вас понял -)
      Raimei
      25 сентября 2019, 00:25
      +1
      Шикарная статья. Закинул пару сотен
      Сергей
      26 сентября 2019, 09:59
      +1
      Спасибо за продолжение, поблагодарил)
        Володя
        26 сентября 2019, 10:46
        +2
        интересно, спасибо!
          Сергей
          26 сентября 2019, 10:49
          +2
          буду и дальше стараться)))
            Володя
            26 сентября 2019, 10:51
            +2
            эт я промахнулся)
        Михаил
        28 сентября 2019, 16:21
        0
        А как можно пофиксить подсветку ошибки:
          Василий Наумкин
          28 сентября 2019, 18:04
          0
          Поищи плагин Vue для PhpStorm
            Михаил
            28 сентября 2019, 19:49
            0
            ага, да вроде стоит. Пойду гулить, что не так
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          14