Pull to refresh
2661.37
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Приложение реального времени на Vue.js

Reading time 11 min
Views 23K
Original author: Davis Kerby
По мнению Дэвиса Керби, вице-президента 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.

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

Articles

Information

Website
ruvds.com
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
ruvds