Как написать одностраничное приложение (SPA) с использованием Vue.js

Одностраничные приложения

Одностраничные приложения (SPA) имеют мнжество преимуществ, таких как скорость, по-настоящему хороший UX, и полный контроль HTML-разметки. Становится всё больше и больше сайтов SPA; всё больше инструментов, которые упрощают процесс разработки SPA. Вы, вероятно уже читали о молодом и перспективном фреймворке Vue.js. Предлагаю вам глубже погрузиться в Vue и на конкретном примере разобраться с простым SPA.

Мы напишем клиент-серверное приложение простейшего блога. Приложение будет отображать список записей а также полный текст каждой отдельной записи. И само собой, всё это будет происходить без перезагрузки страницы.

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

Бэкенд


В этом руководстве мы в основном сосредоточимся на фронтенде на Vue. Размышлять о написании REST бэкенда мы не будем. Для примера будет использоваться сервис jsonplaceholder.typicode.com предостовляющий заглушку в виде REST API.

Фронтенд


Инструменты


Начать работу с Vue очень просто. С использованием правильных инструментов это ещё проще. Рекомендую взглянуть на проект vue-awesome, который содержит список инструментов, компонентов, библиотек и плагинов на все случаи жизни.

Vue-cli


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

Итак, для начала установим vue-cli в качестве глобального пакета:

$ npm install -g vue-cli

Затем проинициализируем проект с выбранным шаблоном; для нашего примера более чем достаточно использовать webpack-simple.

$ vue init webpack-simple vue-spa

Далее перейдём в папку vue-spa и запустим npm install в терминале. После установки всех пакетов, мы можем запустить наше приложение в режиме разработки.

$ npm run dev

Эта команда автоматически запустит наш проект на локальном dev-сервере webpack. В браузере появится наше простейшее приложение Vue. Конечно оно выглядит совсем не так, как бы нам хотелось, и годится лишь в качестве отправной точки для начала чего-то большего. Чтобы продолжить работу, предлагаю сначала ознакомиться со структурой нашего шаблона.

Шаблон webpack-simple


Внутри шаблон webpack-simple имеет следующую структуру:

Шаблон webpack-simple

Файл index.html содержит простую разметку HTML с единственным элементом “app” в body. Он будет заменён на DOM, сгенерированный vue. По этой причине тэг body не рекомендуется использовать в качестве корневого элемента.

В папке src лежит файл main.js, который содержит точку входа webpack. Компоненты Vue импортируются там же. Ещё там описан корневой экземпляр Vue, который пока что имеет два свойства. Свойство ‘el’ обеспечивает экземпляру Vue связь с указанным DOM элементом. Ещё одно — функция отрисовки, генерирующая DOM из App.vue. В общем, это все, что нам нужно знать о структуре шаблона webpack-simple, не так много, не так ли? Основная часть нашего приложения будет запрограммирована в App.vue. Расширение .vue определяет файл как однофайловый компонент vue. Это одна из особенностей Vue с которой мы сейчас познакомимся поближе.

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


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

Каждый файл *.vue состоит из блоков трёх типов: <template>, <script> и опционально <style>. В результате, мы можем разделить проект на связанные компоненты. Внутри компонента его шаблон, логика и стили неотъемлемо связаны, и их совмещение фактически делает компонент более целостным и легко поддерживаемым. Теперь мы готовы приступить к созданию блога на Vue.

Пишем приложение


Давайте посмотрим, что собственно мы собираемся реализовать. У нас будет заголовок с названием нашего блога в верхней части страницы. С левой стороны у нас будет фиксированная боковая панель, в которой мы будем отображать заголовки наших записей, это будет что-то вроде оглавления. Остальная часть страницы будет занята динамическим блоком, в котором будет отображаться сам текст записи.

Пишем приложение

Шаг 1


Прежде всего, удалим все лишние строки из App.vue. И перепишем шаблон в соответствии с нашими требованиями.

<template>
  <div id="app">
    <header>
      <h1>Vue.js SPA</h1>
    </header>
    <main>
      <aside class="sidebar">
      </aside>
      <div class="content">
      </div>
    </main>
  </div>
</template>

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

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

<script>
  export default {
    data () {
      return {
        posts: []
      }
    }
  }
</script>

Кроме того, можно добавить немного стилей, чтобы приложение выглядело лучше.
Код приложения хостится на github.com. Достаточно клонировать репозиторий и переключать ветку по номеру шага чтобы проследить разработку приложения шаг за шагом например:

$ git checkout step-1

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

Шаг 2


Установим Axios

$ npm install --save-dev axios

Затем импортируем его в компонент App и определим метод getAllPosts() который будет делать запрос к серверу и присваивать его свойству posts. Вызываем метод в хуке created(), который будет вызываться после создания экземпляра Vue и после установки настроек обращения к данным.

import axios from 'axios'
export default {
  data () {
    return {
      posts: null,
      endpoint: 'https://jsonplaceholder.typicode.com/posts/',
    }
  },

  created() {
    this.getAllPosts();
  },

  methods: {
    getAllPosts() {
      axios.get(this.endpoint)
        .then(response => {
          this.posts = response.data;
        })
        .catch(error => {
          console.log('-----error-------');
          console.log(error);
        })
    }
  }
}

А теперь отобразим все заголовки записей в боковой панели.

<aside class="sidebar">
  <div v-for="post in posts">
    {{ post.title }}
  </div>
</aside>

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

Шаг 3


Для этого воспользуемся официальной Vue библиотекой vue-router. Как уже понятно из названия, библиотека позволяет настраивать роутинг для нашего приложения.
Установим библиотеку:

$ npm install --save-dev vue-router

Для настройки роутинга вернёмся к файлу main.js. Здесь мы определим настройки роутинга и добавим их в наш экземпляр Vue.

import Vue from 'vue'
import Router from 'vue-router'
import App from './App.vue'
import Post from './components/Post.vue'
import Hello from './components/Hello.vue'
Vue.use(Router)

const router = new Router({
 routes: [
   {
     path: '/',
     name:'home',
     component: Hello,
   },
   {
     path: '/post/:id',
     name:'post',
     component: Post,
     props: true,
   },
 ]
})

new Vue({
 el: '#app',
 render: h => h(App),
 router
})

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

path: '/post/:id'

Этот путь содержит динамический сегмент :id который указывает на конкретный пост. При этом у нас есть доступ к этому сегменту в компоненте Post через this.$route.params.id. Однако использование $route в нашем компоненте закрепит жесткую связь с роутом, что в свою очередь ограничивает гибкость компонента, поскольку он может использоваться только на определенных URL-адресах. Вместо этого мы можем использовать опцию props и установить её в true. После этого $route.params станет связан с опцией props компонента Post.
Теперь, когда мы создали роутер, мы можем вернуться к нашему приложению и добавить еще несколько строк в шаблон.

<main>
  <aside class="sidebar">
    <router-link
        v-for="post in posts"
        active-class="is-active"
        class="link"
        :to="{ name: 'post', params: { id: post.id } }">
      {{post.id}}. {{post.title}}
    </router-link>
  </aside>
  <div class="content">
    <router-view></router-view>
  </div>
</main>

Здесь мы имеем два компонента vue-router: <router-link> и <router-view>. Первый — это компонент для включения навигации пользователя в приложении с поддержкой роутинга. Второй компонент — это функциональный компонент, который отрисовывает согласованный компонент для данного пути.

Остался заключительный шаг. Нам нужно отобразить содержимое записи поста.

Шаг 4


Перейдём к файлу Post.vue, в котором добавим простой шаблон:
<template lang="html">
  <div class="post" v-if="post">
    <h1 class="post__title">{{ post.title }}</h1>
    <p class="post__body">{{ post.body }}</p>
    <p class="post__id">{{ post.id }}</p>
  </div>
</template>

Затем нам нужно установить параметры экземпляра Vue для этого компонента. Здесь все также как в настройках отображения всех постов. Объявим опцию props с меняющимся id, которая будет получать номер нашего поста. Далее, объявим объект данных, как уже делали в App.vue:

import axios from 'axios';
export default {
  props: ['id'],
  data() {
    return {
      post: null,
      endpoint: 'https://jsonplaceholder.typicode.com/posts/',
    }
  }
}

Затем опишем метод getPost(), который будет получать только одну запись поста по идентификатору и вызовем его в хуке created().

methods: {
  getPost(id) {
    axios(this.endpoint + id)
      .then(response => {
        this.post = response.data
      })
      .catch( error => {
        console.log(error)
      })
  }
},
  
created() {
  this.getPost(this.id);
},

Почти готово. Если мы запустим приложение сейчас, мы увидим, что, хотя URL-адрес меняется, мы видим единственный пост, который был отрисован первым. Дело в том, что для отрисовки разных постов у нас есть один и тот же компонент, и Vue не нужно его пересоздавать из-за лишней траты ресурсов, а это также означает, что хуки жизненного цикла компонента не будут вызваны.
Чтобы это исправить, нам просто нужно установить watcher для объекта $route.

watch: {
  '$route'() {
    this.getPost(this.id);
  }
}

Теперь всё работает так, как и должно. Чтобы получить версию для продакшина достаточно выполнить команду npm run build в консоли.

Подведём итоги


Мы написали простое одностраничное приложение с использованием Vue за четыре шага. Мы узнали, как легко начать свой проект с vue-cli. Мы рассмотрели концепцию однофайловых компонентов Vue, которые делают ваш проект более гибким и масштабируемым. Мы узнали, как извлекать данные из внешнего API с помощью Axios. И мы увидели, как настроить роутинг с помощью vue-router. Разумеется, это базовые знания, но я надеюсь, что это поможет вам начать использовать Vue.js используя его расширенные возможности.

Полезные ссылки


Оригинал статьи
Ссылка на проект GitHub с исходным кодом из статьи
Проект vue-awesome
Поделиться публикацией
Комментарии 26
    –2
    Cool!
      0
      Вашу статью бы на пару месяцев раньше. Именно тогда начал изучать Vue, параллельно пишу SPA, стараюсь по максимум все усложнить, чтоб пройтись по подводным камням, использую, vuex, vue-route, vuetify бэкенд на php yii2. Потом планирую сбилдить в мобильное приложение. И Vue мне очень нравится, приложение строятся очень быстро, просто тащусь от реактивности, компонентов которые получаются изолированными и самостоятельными кусочками кода, собираешь все как в конструкторе.
        +1

        А не поделитесь: каким способом Вы билдите проект в мобильное приложение? Изучаю Vue уже год, много раз слышал о такой возможности, но самостоятельно не пробовал пока что...

          0

          Apache cordova в помощь. Либо возьмите quasar framework который уже годный сам по себе и поддерживает это всё из коробки.

      0
      .catch(error => {
            console.log('-----error-------');
            console.log(error);
      })

      По-моему не надо так ошибки обрабатывать, почему в «примерах» не пишут хорошие примеры?
        0

        Напишите в комментах, как надо.

          +1
          ну как минимум
          console.log('-----error-------', error);
            0
            Во-первых стоит наверное что-то показать пользователю, мол сорян, ошибка на сервере, попробуй позже.
            А во-вторых стоит консоль лог спрятать под условие `if (isDevelopment) {}`. Хотя вебпак всё-равно удалит их для прода при правильной настройке.
          0
          axios и vue-router в дев зависимости? Зачем?
          И итерацию v-for надо с атрибутом :key писать, так правильно. Ну и линтер по шее даст.
            0

            Объясните, пожалуйста, как правильно и почему?

              0
              axios и vue-router используются в самом приложении, а не при его разработке. Поэтому эти пакеты надо подключать как обычные зависимости, а не dev. В dev зависимости отправляют всякие бабели, лоадеры и другие пакеты, которые нужны непосредственно только для разработки и сборки итоговых файлов. Сами пакеты в итоговые файлы не попадают
              0
              Я вообще не понимаю, почему не использовать нативный `fetch` и не тянуть тонны зависимостей. Или я упускаю какую-то супер интеграцию axios со vue?
              0
              Почему во всех статьях про Vue используется Axios для запросов, а не тот же vue-resource?
              0

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

                0
                Подскажите, а кто как решает вопрос локализации приложений на vue?
                0
                Мне кажется, содержание этой статьи во многом повторяет кое-то из лекций www.youtube.com/channel/UCzgtMBarT8AvsGc-Y_8Qexw/videos чуть ли не дословно. Например, часть 14 про vue-router.
                  0
                  Внизу статьи есть ссылка на оригинал. Статья — мой вольный перевод.
                  0
                  Для примера будет использоваться сервис jsonplaceholder.typicode.com предостовляющий заглушку в виде REST API.

                  Что-то все норовят в подобных мануалах для вью заглушки юзать. Кто бы с реальной бд такой пример показал...

                    +1
                    А в чём сложность? В примере упор на фронт.
                    REST API по сути не привязан к Vue никак. Бэк можно делать хоть на голых файлах, хоть на Apache+PHP, да хоть на nginx+LUA.
                    Статья совершенно не поменяется.
                      0

                      Согласен :) Просто много мануалов про вью, много про бэкенд, а чтоб и то и другое — маловато… А хочется :)

                        0
                        Мне кажется потому, что специфика разная.
                        Бэкенд — это ведь еще нередко запросы к базе данных либо вообще другой язык программирования.
                    0
                    В просторах Github можно найти достаточно классных примеров как сделать SPA с применением как Vue в отдельности, так и в содружестве с Laravel, к примеру.
                    Laravel Vue SPA, Bulma themed
                    Vue SPA projects using Vue CLI 3
                    Laravel 5.6 + Vue.js
                    dashboard.spatie.be
                    и Boilerplate for [mobile] SPAs

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое