По мнению Дэвиса Керби, вице-президента Algoworks Solutions, автора этой статьи, фреймворк Vue.js набирает популярность в среде JavaScript-разработчиков благодаря своей простоте и той лёгкости, с которой можно начать работу с ним. Буквально несколько строк кода на Vue позволяют делать очень серьёзные вещи. Vue — это один из самых известных фреймворков, он находится в числе ведущих платформ для веб-разработки.
Современный пользователь Сети не любит ждать. Как быть, если на Vue нужно создать приложение для работы с некими данными в реальном времени? Дэвис отвечает на этот вопрос с помощью интеграции в приложение Vue.js 2.0. возможностей сервиса Pusher. В этом материале он, с самого начала, разберёт разработку такого приложения, названного Movie Review.


A: установка vue-cli


Инструмент командной строки vue-cli предназначен для работы с проектами Vue.js, благодаря ему мы, не тратя время на настройки, можем быстро создать проект и приступить к работе.

Установим vue-cli следующей командой:

npm install -g vue-cli

Создадим проект, основанный на шаблоне webpack, и установим зависимости с помощью следующего набора команд:

vue init webpack samplevue
cd samplevue
npm install

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

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

npm run dev

B: начало создания приложения Movie Review


Далее, начнём создавать компоненты приложения, подготовив пару файлов:

touch ./src/components/Movie.vue
touch ./src/components/Reviews.vue

Тут стоит учитывать, что мощь Vue.js заключается в компонентах. Это роднит его с современными JS-фреймворками. Компоненты — это то, что помогает повторно использовать различные части приложения.

?B1: поиск и загрузка информации о фильмах


Для написания отзывов о фильмах создадим простую форму, которую будем использовать для загрузки информации о фильмах с использованием общедоступного API Netflix Roulette:

<!-- ./src/components/Movie.vue -->
<template>
  <div class="container">
    <div class="row">
      <form @submit.prevent="fetchMovie()">
        <div class="columns large-8">
          <input type="text" v-model="title">
        </div>
        <div class="columns large-4">
          <button type="submit" :disabled="!title" class="button expanded">
            Search titles
          </button>
        </div>
      </form>
    </div>
    <!-- /search form row -->
  </div>
  <!-- /container -->
</template>

В этом коде мы создаём форму и задаём собственный обработчик события отправки формы fetchMovie().

Директива @submit — это сокращение для v-on:submit. Она используется для прослушивания событий DOM и выполнения действий или обработчиков при возникновении этих событий. Модификатор .prevent помогает создать event.preventDefault() в обработчике.

Для привязки значения текстового поля ввода к title мы используем директиву v-model. И, наконец, мы можем привязать атрибут кнопки disabled так, что он будет установлен в true, если title пусто, и наоборот. Кроме того, обратите внимание на то, что :disabled — это сокращение для v-bind:disabled.

Теперь определим методы и значения данных для компонента:

<!-- ./src/components/Movie.vue -->
<script>
// зададим URL внешнего API
const API_URL = 'https://netflixroulette.net/api/api.php'
// вспомогательная функция создания URL для загрузки данных о фильме по названию
function buildUrl (title) {
  return `${API_URL}?title=${title}`
}
export default {
  name: 'movie', // имя компонента
  data () {
    return {
      title: '',
      error_message: '',
      loading: false, // устанавливается, когда приложение загружает данные
      movie: {}
    }
  },
  methods: {
    fetchMovie () {
      let title = this.title
      if (!title) {
        alert('please enter a title to search for')
        return
      }
      this.loading = true 
      fetch(buildUrl(title))
      .then(response => response.json())
      .then(data => {
        this.loading = false
        this.error_message = ''
        if (data.errorcode) {
          this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.`
          return
        }
        this.movie = data
      }).catch((e) => {
        console.log(e)
      })
    }
  }
}
</script>

Как только мы задали внешний URL, к которому хотим обратиться для загрузки данных о фильме, нам нужно задать важнейшие параметры Vue, которые могут потребоваться для настройки компонентов:

  • data: задаёт свойства, которые могут понадобится в компоненте.
  • methods: задаёт методы компонента. Сейчас задан лишь один метод, служащий для загрузки данных о фильмах — fetchMovie().

После этого нам нужно добавить в
 код для вывода сведений о фильме или показа сообщения о том, что название фильма не найдено:

<!-- ./src/components/Movie.vue --> <template> <!-- // ... --> <div v-if="loading" class="loader">  <img align="center" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader"> </div> <div v-else-if="error_message">  <h3><font color="#3AC1EF">?{{ error_message }}</font></h3> </div> <div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie">  <div class="columns large-7">    <h4> {{ movie.show_title }}</h4>    <img :src="movie.poster" :alt="movie.show_title">  </div>  <div class="columns large-5">    <p>{{ movie.summary }}</p>    <small><strong>Cast:</strong> {{ movie.show_cast }}</small>  </div> </div> </template>

Кроме того, можно стилизовать компонент, добавив соответствующий код в нижней части файла, хотя это и необязательно:

<!-- ./src/components/Movie.vue -->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#movie {
  margin: 30px 0;
}
.loader {
  text-align: center;
}
</style>

?B2: загрузка и написание обзоров фильмов


Отредактируем компонент Review, который содержит логику и средства для вывода отзывов, с использованием того же самого подхода, когда каждый компонент описывают в его собственном файле.

Используем директиву v-for для того, чтобы перебирать доступные обзоры для фильма. Затем выведем их в шаблоне:

<!-- ./src/components/Review.vue -->
<template>
  <div class="container">
    <h4 class="uppercase">reviews</h4>
    <div class="review" v-for="review in reviews">
      <p>{{ review.content }}</p>
      <div class="row">
        <div class="columns medium-7">
          <h5>{{ review.reviewer }}</h5>
        </div>
        <div class="columns medium-5">
          <h5 class="pull-right">{{ review.time }}</h5>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
const MOCK_REVIEWS = [
  {
    movie_id: 7128,
    content: 'Great show! I loved every single scene. Defintiely a must watch!',
    reviewer: 'Jane Doe',
    time: new Date().toLocaleDateString()
  }
]
export default {
  name: 'reviews',
  data () {
    return {
      mockReviews: MOCK_REVIEWS,
      movie: null,
      review: {
        content: '',
        reviewer: ''
      }
    }
  },
  computed: {
    reviews () {
      return this.mockReviews.filter(review => {
        return review.movie_id === this.movie
      })
    }
  }
}
</script>

MOCK_REVIEWS используется для создания макета обзоров. Свойство, сформированное программно, применяется для фильтрации обзоров конкретного фильма.

Теперь напишем код формы и методов для добавления нового обзора:

<!-- ./src/components/Review.vue -->
<template>
  <div class="container">
    <!-- //... -->
    <div class="review-form" v-if="movie">
      <h5>add new review.</h5>
      <form @submit.prevent="addReview">
        <label>
          Review
          <textarea v-model="review.content" cols="30" rows="5"></textarea>
        </label>
        <label>
          Name
          <input v-model="review.reviewer" type="text">
        </label>
        <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button>
      </form>
    </div>
    <!-- //... -->   
  </div> 
</template>
<script>
export default {
  // ..
  methods: {
    addReview () {
      if (!this.movie || !this.review.reviewer || !this.review.content) {
        return
      }
      let review = {
        movie_id: this.movie, 
        content: this.review.content, 
        reviewer: this.review.reviewer, 
        time: new Date().toLocaleDateString()
      }
      this.mockReviews.unshift(review)
    }
  },
  //...
}
</script>

Опять же, можно, в нижней части кода компонента, добавить стилизацию, но это необязательно:

<!-- ./src/components/Review.vue -->
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .container {
    padding: 0 20px;
  }
  .review {
    border:1px solid #ddd;
    font-size: 0.95em;
    padding: 10px;
    margin: 15px 0 5px 0;
  }
  .review h5 {
    text-transform: uppercase;
    font-weight: bolder;
    font-size: 0.7em
  }
  .pull-right {
    float: right;
  }
  .review-form {
    margin-top: 30px;
    border-top: 1px solid #ddd;
    padding: 15px 0 0 0;
  }
</style>

Используем идентификатор movie из компонента Movie для того, чтобы загружать и отправлять обзоры.

?B3: обмен данными между компонентами


Для того, чтобы наладить обмен данными между компонентами, можно создать новый экземпляр Vue и использовать его как шину для передачи сообщений. Шина передачи сообщения — это объект, в который компоненты могут отправлять события, и с помощью которого могут на события подписываться. Создадим шину передачи сообщений:

touch ./src/bus.js
// ./src/bus.js
import Vue from 'vue'
const bus = new Vue()
export default bus

Для отправки события при обнаружении фильма отредактируем метод fetchMovies():

<!-- ./src/components/Movie.vue -->
import bus from '../bus'
export default {
  // ...
  methods: {
    fetchMovie (title) {
      this.loading = true
      fetch(buildUrl(title))
      .then(response => response.json())
      .then(data => {
        this.loading = false
        this.error_message = ''
        bus.$emit('new_movie', data.unit) // emit `new_movie` event
        if (data.errorcode) {
          this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.`
          return
        }
        this.movie = data
      }).catch(e => { console.log(e) })
    }
  }
}

В хуке created будем прослушивать события в компоненте Review:

<!-- ./src/components/Review.vue -->
<script>
import bus from '../bus'
export default {
  // ...
  created () {
    bus.$on('new_movie', movieId => {
      this.movie = movieId
    })
  },
  // ...
}
</script>

В этом коде мы указали, что когда происходит событие new_movie, надо установить свойство movie в значение movieId, которое и передаётся всем заинтересованным получателям с помощью соответствующего события.

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

<!-- ./src/App.vue -->
<template>
  <div id="app">
    <div class="container">
      <div class="heading">
        <h2><font color="#3AC1EF">samplevue.</font></h2>
        <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6>
      </div>
      <div class="row">
        <div class="columns small-7">
          <movie></movie>
        </div>
        <div class="columns small-5">
          <reviews></reviews>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  import Movie from './components/Movie'
  import Reviews from './components/Reviews'
  export default {
    name: 'app',
    components: {
      Movie, Reviews
    }
  }
</script>
<style>
  #app .heading {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin: 60px 0 30px;
    border-bottom: 1px solid #eee;
  }
</style>

Теперь запустим приложение и проверим его функционал по загрузке информации о фильмах и добавлению обзоров:

npm run dev

Обратите внимание на то, что для успешной загрузки информации о фильмах с использованием API Netflix требуется полное название фильма.

C: добавление обновлений в реальном времени с использованием Pusher


Теперь разберёмся с тем, как добавить нашему приложению функциональность реального времени. Это позволит показывать всем пользователям, просматривающим сведения о некоем фильме, новые обзоры по мере их появления.

Настроим простой бэкенд, где можно обрабатывать post-запросы с новыми обзорами и, используя pusher, будем оформлять эти данные как события.

?C1: настройка Pusher


Зарегистрируйте бесплатную учётную запись на сайте Pusher. Создайте приложение с помощью панели управления и скопируйте его учётные данные.

?C2: настройка бэкенда и широковещательная передача событий


Создадим простой сервер с помощью Node.js. Добавим зависимости, которые нам понадобятся, в package.json, и установим необходимые пакеты:

npm install -S express body-parser pusher

Создадим файл server.js, в котором напишем приложение Express:

// ./server.js
/*
 * Инициализация Express
 */
const express = require('express');
const path = require('path');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname)));
/*
 * Инициализация Pusher
 */
const Pusher = require('pusher');
const pusher = new Pusher({
  appId:'YOUR_PUSHER_APP_ID',
  key:'YOUR_PUSHER_APP_KEY',
  secret:'YOUR_PUSHER_SECRET',
  cluster:'YOUR_CLUSTER'
});
/*
 * Определения маршрута post для создания новых обзоров
 */
app.post('/review', (req, res) => {
  pusher.trigger('reviews', 'review_added', {review: req.body});
  res.status(200).send();
});
/*
 * Запуск приложения
 */
const port = 5000;
app.listen(port, () => { console.log(`App listening on port ${port}!`)});

Инициализируем приложение Express, затем инициализируем Pusher, воспользовавшись полученными ранее учётными данными. Замените YOUR_PUSHER_APP_ID, YOUR_PUSHER_APP_KEY, YOUR_PUSHER_SECRET и YOUR_CLUSTER данными с панели управления Pusher.

Зададим маршрут для создания отзывов: /review. В данной конечной точке используем Pusher для вызова события review_added в канале reviews. Затем передадим все необходимые данные в виде обзора. Вот как выглядит работа с методом trigger:

pusher.trigger(channels, event, data, socketId, callback);

?C3: создание API-прокси


Создадим прокси в config/index.js, для того, чтобы обращаться к нашему серверному API из фронтенд-сервера, созданного Vue Webpack. Затем мы можем запустить сервер разработки и бэкенд API одновременно.

// config/index.js
module.exports = {
  // ...
  dev: {
    // ...
    proxyTable: {
        '/api': {
        target: 'http://localhost:5000', // эту строку надо отредактировать в соответствии с портом, на котором работает ваш сервер
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    },
    // ...
  }
}

Отредактируем метод addReview для обращения к API в /src/components/Reviews.vue:

<!-- ./src/components/Review.vue -->
<script>
// ...
export default {
  // ...
  methods: {
    addReview () {
      if (!this.movie || !this.review.reviewer || !this.review.content) {
        alert('please make sure all fields are not empty')
        return
      }
      let review = {
        movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString()
      }
      fetch('/api/review', {
        method: 'post',
        body: JSON.stringify(review)
      }).then(() => {
        this.review.content = this.review.reviewer = ''
      })
    }
    // ...
  },
  // ...
}
</script>

?C4: прослушивание событий


Когда публикуется новый обзор, будем прослушивать события, отправляемые Pusher, и расширять их с использованием более подробных сведений. Установим библиотеку pusher-js:

npm install -S pusher-js

Отредактируем Review.vue:

<!-- ./src/components/Review.vue -->
<script>
import Pusher from 'pusher-js' // импорт Pusher
export default {
  // ...
  created () {
    // ...
    this.subscribe()
  },
  methods: {
    // ...
    subscribe () {
      let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' })
      pusher.subscribe('reviews')
      pusher.bind('review_added', data => {
        this.mockReviews.unshift(data.review)
      })
    }
  },
  // ...
}
</script>

Как уже было сказано, мы импортируем объект Pusher из библиотеки pusher-js. Теперь создадим метод subscribe, который выполняет следующие действия:

  • Подписка на канал reviews с помощью команды pusher.subscribe('reviews').
  • Прослушивание события review_added с помощью pusher.bind. Тут, в качестве второго аргумента, используется функция обратного вызова. При получении широковещательного сообщения функция обратного вызова вызывается с переданными данными в качестве параметра.

D. Сборка готового проекта


Добавим файл server.js с Node-сервером в dev-зависимости приложения, или выполним скрипт для того, чтобы сервер API запустился вместе с сервером, предоставленным шаблоном webpack:

{
  // ...
  "scripts": {
    "dev": "node server.js & node build/dev-server.js",
    "start": "node server.js & node build/dev-server.js",
    // ...
  }
}

Теперь можно запустить всё, из чего состоит приложение:

run dev

Итоги


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

Уважаемые читатели! Какими технологиями вы пользуетесь для создания веб-приложений, предназначенных для работы с данными в реальном времени?

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


  1. Damaskus
    28.11.2017 13:45

    let, window.fetch используете, а const и await — нет.

    Вот этот кусок вообще не очень, если честно:

     created () {
        bus.$on('new_movie', movieId => {
          this.movie = movieId
        })
      },
    

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

    А точки с запятой тормоза придумали трусы.


    1. Londeren
      28.11.2017 14:18

      да тут вообще какой-то сумасшедший набор худших практик по js/css


    1. mayorovp
      28.11.2017 14:43

      Предлагаете тащить moveId через все компоненты из корня? Все равно ведь кто-то должен слушать событие `new_movie`…


      1. Damaskus
        28.11.2017 15:13

        Да вам вообще это шина не нужна.
        Вы реализовали вместо иерархии плоскую структуру.
        Из инструкции vue.js:
        props down, events up
        image
        1. Ваш компонент, который делает fetchMovie() не должен ничего сам грузить, но даже если грузит, то должен кинуть событие наверх, что есть новый фильм. Это изменение вызовет изменение данных в родителе, который спустит новые данные в виде props в ребенка.

        Соответственно:
        2. Ребенок — компонент, который слушает ваш event сейчас, должен иметь это свойство.

        А с текущим подходом, вы успешно теряете все бенефиты от immutability и всего того, что паттерн mediator приносит вобщем-то.


        1. mayorovp
          28.11.2017 15:54

          Я реализовал?!


          Приведенная вами картинка описывает взаимодействие компонентов и не запрещает компонентам иметь состояние. Точно так же как наличие специальных механизмов для взаимодействия компонентов вида не запрещают виду взаимодействовать с моделью.


          Если что и ставить в вину автору кода — так это отсутствие явно выделенной модели; fetch-запрос с нетривиальной обработкой не должен был делаться из UI-компонента.


          1. Damaskus
            28.11.2017 19:50

            Модель, если уж на то пошло, там есть и, собственно, там все в ней и лежит.
            Вообще все, даже то, что не мутирует и приходит снаружи — movie.

            А картинка эта (иерархия, которая предполагается Vue) и использование props позволит сэкономить на перестроении DOM.

            Bottomline.
            Есть механизмы для того, чтобы написать все это, используя подходы из коробки дающие хорошую изоляцию компонентов(+реюзабилити, + тесты) и быстродействие.
            Вместо этого пишется свой велик и декларируется, что вью — это круто.
            А по факту из всего набора использовался только шаблонизатор.


  1. PaulMaly
    29.11.2017 00:05

    Не понимаю, зачем переводить плохие статьи?


  1. isden
    29.11.2017 19:54

    Как-то вообще немного не очень понятно, там нужно Review.vue и Reviews.vue? Или только Reviews.vue (а по тексту, в т.ч. в оригинале, ошибки)?


  1. SysCat
    29.11.2017 20:44

    Хочу такие часы :)


  1. psFitz
    30.11.2017 10:30

    Какое-то уродство


  1. LiguidCool
    01.12.2017 10:10

    Я не понял зачем для «шины» вызывается отдельный класс vue? Разве vuex не для этого сделан (обмен данными между компонентами)? Поправьте, если не прав…


    1. PaulMaly
      01.12.2017 11:34

      Как я понял, на тот случай, если вы не хотите все данные класть в стор.