
現代の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パラメーターを設定する必要があります。
-
data
:コンポーネントに必要なプロパティを設定します。
-
methods
:コンポーネントのメソッドを設定します。 現在、動画データの読み込みに使用されるメソッドはfetchMovie()
です。
その後に追加する必要があります
, :
<!-- ./src/components/Movie.vue --> <template> <!-- // ... --> <div v-if="loading" class="loader"> <img align="center" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader"> </div> <div v-else-if="error_message"> <h3><font color="#3AC1EF">▍{{ error_message }}</font></h3> </div> <div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie"> <div class="columns large-7"> <h4> {{ movie.show_title }}</h4> <img :src="movie.poster" :alt="movie.show_title"> </div> <div class="columns large-5"> <p>{{ movie.summary }}</p> <small><strong>Cast:</strong> {{ movie.show_cast }}</small> </div> </div> </template>
, , , :
<!-- ./src/components/Movie.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #movie { margin: 30px 0; } .loader { text-align: center; } </style>
▍B2:
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:
Review
, , , .
v-for
, . :
<!-- ./src/components/Review.vue --> <template> <div class="container"> <h4 class="uppercase">reviews</h4> <div class="review" v-for="review in reviews"> <p>{{ review.content }}</p> <div class="row"> <div class="columns medium-7"> <h5>{{ review.reviewer }}</h5> </div> <div class="columns medium-5"> <h5 class="pull-right">{{ review.time }}</h5> </div> </div> </div> </div> </template> <script> const MOCK_REVIEWS = [ { movie_id: 7128, content: 'Great show! I loved every single scene. Defintiely a must watch!', reviewer: 'Jane Doe', time: new Date().toLocaleDateString() } ] export default { name: 'reviews', data () { return { mockReviews: MOCK_REVIEWS, movie: null, review: { content: '', reviewer: '' } } }, computed: { reviews () { return this.mockReviews.filter(review => { return review.movie_id === this.movie }) } } } </script>
MOCK_REVIEWS . , , .
:
<!-- ./src/components/Review.vue --> <template> <div class="container"> <!-- //... --> <div class="review-form" v-if="movie"> <h5>add new review.</h5> <form @submit.prevent="addReview"> <label> Review <textarea v-model="review.content" cols="30" rows="5"></textarea> </label> <label> Name <input v-model="review.reviewer" type="text"> </label> <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> </form> </div> <!-- //... --> </div> </template> <script> export default { // .. methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } this.mockReviews.unshift(review) } }, //... } </script>
, , , , :
<!-- ./src/components/Review.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { padding: 0 20px; } .review { border:1px solid #ddd; font-size: 0.95em; padding: 10px; margin: 15px 0 5px 0; } .review h5 { text-transform: uppercase; font-weight: bolder; font-size: 0.7em } .pull-right { float: right; } .review-form { margin-top: 30px; border-top: 1px solid #ddd; padding: 15px 0 0 0; } </style>
movie
Movie
, .
▍B3:
, , Vue . — , , . :
touch ./src/bus.js // ./src/bus.js import Vue from 'vue' const bus = new Vue() export default bus
fetchMovies()
:
<!-- ./src/components/Movie.vue --> import bus from '../bus' export default { // ... methods: { fetchMovie (title) { this.loading = true fetch(buildUrl(title)) .then(response => response.json()) .then(data => { this.loading = false this.error_message = '' bus.$emit('new_movie', data.unit) // emit `new_movie` event if (data.errorcode) { this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` return } this.movie = data }).catch(e => { console.log(e) }) } } }
created
Review
:
<!-- ./src/components/Review.vue --> <script> import bus from '../bus' export default { // ... created () { bus.$on('new_movie', movieId => { this.movie = movieId }) }, // ... } </script>
,new_movie
,movie
movieId
, .
, ,App.vue
:
<!-- ./src/App.vue --> <template> <div id="app"> <div class="container"> <div class="heading"> <h2><font color="#3AC1EF">samplevue.</font></h2> <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> </div> <div class="row"> <div class="columns small-7"> <movie></movie> </div> <div class="columns small-5"> <reviews></reviews> </div> </div> </div> </div> </template> <script> import Movie from './components/Movie' import Reviews from './components/Reviews' export default { name: 'app', components: { Movie, Reviews } } </script> <style> #app .heading { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin: 60px 0 30px; border-bottom: 1px solid #eee; } </style>
:
npm run dev
, API Netflix .
C: Pusher
, . , , .
, post- ,pusher
, .
▍C1: Pusher
Pusher . .
▍C2:
Node.js. , ,package.json
, :
npm install -S express body-parser pusher
server.js
, Express:
// ./server.js /* * Express */ const express = require('express'); const path = require('path'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); /* * Pusher */ const Pusher = require('pusher'); const pusher = new Pusher({ appId:'YOUR_PUSHER_APP_ID', key:'YOUR_PUSHER_APP_KEY', secret:'YOUR_PUSHER_SECRET', cluster:'YOUR_CLUSTER' }); /* * post */ app.post('/review', (req, res) => { pusher.trigger('reviews', 'review_added', {review: req.body}); res.status(200).send(); }); /* * */ const port = 5000; app.listen(port, () => { console.log(`App listening on port ${port}!`)});
Express, Pusher, .YOUR_PUSHER_APP_ID
,YOUR_PUSHER_APP_KEY
,YOUR_PUSHER_SECRET
YOUR_CLUSTER
Pusher.
:/review
. Pusherreview_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:
Review
, , , .
v-for
, . :
<!-- ./src/components/Review.vue --> <template> <div class="container"> <h4 class="uppercase">reviews</h4> <div class="review" v-for="review in reviews"> <p>{{ review.content }}</p> <div class="row"> <div class="columns medium-7"> <h5>{{ review.reviewer }}</h5> </div> <div class="columns medium-5"> <h5 class="pull-right">{{ review.time }}</h5> </div> </div> </div> </div> </template> <script> const MOCK_REVIEWS = [ { movie_id: 7128, content: 'Great show! I loved every single scene. Defintiely a must watch!', reviewer: 'Jane Doe', time: new Date().toLocaleDateString() } ] export default { name: 'reviews', data () { return { mockReviews: MOCK_REVIEWS, movie: null, review: { content: '', reviewer: '' } } }, computed: { reviews () { return this.mockReviews.filter(review => { return review.movie_id === this.movie }) } } } </script>
MOCK_REVIEWS . , , .
:
<!-- ./src/components/Review.vue --> <template> <div class="container"> <!-- //... --> <div class="review-form" v-if="movie"> <h5>add new review.</h5> <form @submit.prevent="addReview"> <label> Review <textarea v-model="review.content" cols="30" rows="5"></textarea> </label> <label> Name <input v-model="review.reviewer" type="text"> </label> <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> </form> </div> <!-- //... --> </div> </template> <script> export default { // .. methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } this.mockReviews.unshift(review) } }, //... } </script>
, , , , :
<!-- ./src/components/Review.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { padding: 0 20px; } .review { border:1px solid #ddd; font-size: 0.95em; padding: 10px; margin: 15px 0 5px 0; } .review h5 { text-transform: uppercase; font-weight: bolder; font-size: 0.7em } .pull-right { float: right; } .review-form { margin-top: 30px; border-top: 1px solid #ddd; padding: 15px 0 0 0; } </style>
movie
Movie
, .
▍B3:
, , Vue . — , , . :
touch ./src/bus.js // ./src/bus.js import Vue from 'vue' const bus = new Vue() export default bus
fetchMovies()
:
<!-- ./src/components/Movie.vue --> import bus from '../bus' export default { // ... methods: { fetchMovie (title) { this.loading = true fetch(buildUrl(title)) .then(response => response.json()) .then(data => { this.loading = false this.error_message = '' bus.$emit('new_movie', data.unit) // emit `new_movie` event if (data.errorcode) { this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` return } this.movie = data }).catch(e => { console.log(e) }) } } }
created
Review
:
<!-- ./src/components/Review.vue --> <script> import bus from '../bus' export default { // ... created () { bus.$on('new_movie', movieId => { this.movie = movieId }) }, // ... } </script>
,new_movie
,movie
movieId
, .
, ,App.vue
:
<!-- ./src/App.vue --> <template> <div id="app"> <div class="container"> <div class="heading"> <h2><font color="#3AC1EF">samplevue.</font></h2> <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> </div> <div class="row"> <div class="columns small-7"> <movie></movie> </div> <div class="columns small-5"> <reviews></reviews> </div> </div> </div> </div> </template> <script> import Movie from './components/Movie' import Reviews from './components/Reviews' export default { name: 'app', components: { Movie, Reviews } } </script> <style> #app .heading { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin: 60px 0 30px; border-bottom: 1px solid #eee; } </style>
:
npm run dev
, API Netflix .
C: Pusher
, . , , .
, post- ,pusher
, .
▍C1: Pusher
Pusher . .
▍C2:
Node.js. , ,package.json
, :
npm install -S express body-parser pusher
server.js
, Express:
// ./server.js /* * Express */ const express = require('express'); const path = require('path'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); /* * Pusher */ const Pusher = require('pusher'); const pusher = new Pusher({ appId:'YOUR_PUSHER_APP_ID', key:'YOUR_PUSHER_APP_KEY', secret:'YOUR_PUSHER_SECRET', cluster:'YOUR_CLUSTER' }); /* * post */ app.post('/review', (req, res) => { pusher.trigger('reviews', 'review_added', {review: req.body}); res.status(200).send(); }); /* * */ const port = 5000; app.listen(port, () => { console.log(`App listening on port ${port}!`)});
Express, Pusher, .YOUR_PUSHER_APP_ID
,YOUR_PUSHER_APP_KEY
,YOUR_PUSHER_SECRET
YOUR_CLUSTER
Pusher.
:/review
. Pusherreview_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.
! -, ?