Vue.jsのリアルタイムアプリケーション

この記事の著者であるAlgoworks SolutionsのバイスプレジデントであるDavis Kerbyによると、Vue.jsフレームワークはシンプルで使いやすいため、 JavaScript開発者の間で人気が高まっています。 Vueのほんの数行のコードで非常に深刻なことができます。 Vueは最も有名なフレームワークの1つであり、Web開発の主要なプラットフォームの1つです。




現代のWebユーザーは待つことを好みません。 Vueが特定のデータをリアルタイムで処理するためのアプリケーションを作成する必要がある場合はどうなりますか? Davisは、Vue.js 2.0アプリケーションに統合することでこの質問に答えています。 プッシャーサービス機能。 この記事では、最初から、Movie Reviewと呼ばれるこのようなアプリケーションの開発を分析します。









A:vue-cliのインストール



vue-cli



コマンドラインvue-cli



Vue.jsプロジェクトで動作するように設計されているため、セットアップの時間を無駄にすることなく、プロジェクトをすばやく作成して作業を開始できます。



次のコマンドvue-cli



インストールします。



 npm install -g vue-cli
      
      





webpackテンプレートに基づいてプロジェクトを作成し、次の一連のコマンドを使用して依存関係をインストールします。



 vue init webpack samplevue cd samplevue npm install
      
      





webpackは非常に有用なものであるという事実に注意してください。 そのため、ES6標準のコードをES5標準のコードに変換し、Vueコンポーネントファイルを処理するのに役立ちます。これにより、さまざまなブラウザーで作成されたアプリケーションの互換性について心配する必要がなくなります。



アプリケーションを起動するには、次のコマンドを使用します。



 npm run dev
      
      





B:Movie Reviewアプリケーションの作成を開始する



次に、いくつかのファイルを準備して、アプリケーションコンポーネントの作成を始めましょう。



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





Vue.jsのパワーはコンポーネントにあることに留意してください。 これにより、最新のJSフレームワークに似たものになります。 コンポーネントは、アプリケーションのさまざまな部分を再利用するのに役立ちます。



1:B1:映画情報の検索とダウンロード



映画レビューを作成するには、 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>
      
      





このコードでは、フォームを作成し、フォームfetchMovie()



イベントfetchMovie()



独自のハンドラーを設定します。



@submit



ディレクティブはv-on:submit



略です。 DOMイベントをリッスンし、これらのイベントが発生したときにアクションまたはハンドラーを実行するために使用されます。 .prevent



修飾子は、ハンドラーでevent.preventDefault()



を作成するのに役立ちます。



入力テキストボックスの値をtitle



バインドするには、 v-model



ディレクティブを使用します。 最後に、 disabled



ボタン属性をバインドして、 title



空の場合にtrue



に設定され、その逆の場合も同様に設定できtrue



。 また、 :disabled —



v-bind:disabled



省略形であることに注意してください。



次に、コンポーネントのメソッドとデータ値を定義します。



 <!-- ./src/components/Movie.vue --> <script> //  URL  API const API_URL = 'https://netflixroulette.net/api/api.php' //    URL        function buildUrl (title) { return `${API_URL}?title=${title}` } export default { name: 'movie', //   data () {   return {     title: '',     error_message: '',     loading: false, // ,         movie: {}   } }, methods: {   fetchMovie () {     let title = this.title     if (!title) {       alert('please enter a title to search for')       return     }     this.loading = true     fetch(buildUrl(title))     .then(response => response.json())     .then(data => {       this.loading = false       this.error_message = ''       if (data.errorcode) {         this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.`         return       }       this.movie = data     }).catch((e) => {       console.log(e)     })   } } } </script>
      
      





ムービーデータをダウンロードするためにアクセスする外部URLを設定したら、コンポーネントの構成に必要となる可能性がある最も重要なVueパラメーターを設定する必要があります。





その後に追加する必要があります
            ,     : 
      



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



! -, ?







All Articles