πŸ“œ ⬆️ ⬇️

Real-time application on Vue.js

According to Davis Kirby, vice-president of Algoworks Solutions , the author of this article, the Vue.js framework is gaining popularity among JavaScript developers due to its simplicity and the ease with which you can start working with it. Literally a few lines of code on Vue allow you to do very serious things. Vue is one of the most famous frameworks, it is among the leading platforms for web development.
Modern Web user does not like to wait. What if on Vue you need to create an application to work with certain data in real time? Davis answers this question by integrating into the application Vue.js 2.0. features of the Pusher service. In this article, he will, from the very beginning, deal with the development of such an application, called Movie Review.


A: install vue-cli


The command-line tool vue-cli designed to work with projects Vue.js, thanks to him, we, without wasting time on settings, can quickly create a project and get to work.

Install vue-cli following command:

 npm install -g vue-cli 

Create a project based on the webpack template and install dependencies using the following set of commands:
')
 vue init webpack samplevue cd samplevue npm install 

Pay attention that webpack is a very useful thing. So, it helps to convert the code in the ES6 standard to the code in the ES5 standard and process the Vue component files, which allows you not to worry about the compatibility of applications created with its help with different browsers.

In order to start the application, use the following command:

 npm run dev 

B: start creating Movie Review application


Next, let's start creating application components by preparing a couple of files:

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

Here it is worth considering that the power of Vue.js lies in the components. This makes it related to modern JS frameworks. Components are what helps to reuse different parts of the application.

▍B1: search and download movie information


To write movie reviews, create a simple form that we will use to download movie information using the public Netflix Roulette API:

 <!-- ./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> 

In this code, we create a form and set our own event handler for the form's fetchMovie() .

The @submit directive is short for v-on:submit . It is used to listen for DOM events and execute actions or handlers when these events occur. The modifier .prevent helps to create event.preventDefault() in the handler.

To bind the value of the input text field to the title we use the v-model directive. And finally, we can bind the attribute of the disabled button so that it will be set to true if the title empty and vice versa. Also note that :disabled β€” a shortcut for v-bind:disabled .

Now we define the methods and data values ​​for the component:

 <!-- ./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> 

As soon as we set the external URL to which we want to apply to download movie data, we need to set the most important Vue parameters that may be needed to configure the components:


After that we need to add in
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?
            ,     : 

<!-- ./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:
R eview , , , .

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.

! -, ?
, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

, :

<!-- ./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:
R eview , , , .

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.

! -, ?

Source: https://habr.com/ru/post/343374/


All Articles