vue-cli
designed to work with projects Vue.js, thanks to him, we, without wasting time on settings, can quickly create a project and get to work.vue-cli
following command: npm install -g vue-cli
vue init webpack samplevue cd samplevue npm install
npm run dev
touch ./src/components/Movie.vue touch ./src/components/Reviews.vue
<!-- ./src/components/Movie.vue --> <template> <div class="container"> <div class="row"> <form @submit.prevent="fetchMovie()"> <div class="columns large-8"> <input type="text" v-model="title"> </div> <div class="columns large-4"> <button type="submit" :disabled="!title" class="button expanded"> Search titles </button> </div> </form> </div> <!-- /search form row --> </div> <!-- /container --> </template>
fetchMovie()
.@submit
directive is short for v-on:submit
. It is used to listen for DOM events and execute actions or handlers when these events occur. The modifier .prevent
helps to create event.preventDefault()
in the handler.title
we use the v-model
directive. And finally, we can bind the attribute of the disabled
button so that it will be set to true
if the title
empty and vice versa. Also note that :disabled β
a shortcut for v-bind:disabled
. <!-- ./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>
data
: sets the properties that may be needed in the component.methods
: sets the methods of the component. Now there is only one method used to load movie data - 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:
R eview
, , , .
v-for
, . :
<!-- ./src/components/Review.vue --> <template> <div class="container"> <h4 class="uppercase">reviews</h4> <div class="review" v-for="review in reviews"> <p>{{ review.content }}</p> <div class="row"> <div class="columns medium-7"> <h5>{{ review.reviewer }}</h5> </div> <div class="columns medium-5"> <h5 class="pull-right">{{ review.time }}</h5> </div> </div> </div> </div> </template> <script> const MOCK_REVIEWS = [ { movie_id: 7128, content: 'Great show! I loved every single scene. Defintiely a must watch!', reviewer: 'Jane Doe', time: new Date().toLocaleDateString() } ] export default { name: 'reviews', data () { return { mockReviews: MOCK_REVIEWS, movie: null, review: { content: '', reviewer: '' } } }, computed: { reviews () { return this.mockReviews.filter(review => { return review.movie_id === this.movie }) } } } </script>
MOCK_REVIEWS . , , .
:
<!-- ./src/components/Review.vue --> <template> <div class="container"> <!-- //... --> <div class="review-form" v-if="movie"> <h5>add new review.</h5> <form @submit.prevent="addReview"> <label> Review <textarea v-model="review.content" cols="30" rows="5"></textarea> </label> <label> Name <input v-model="review.reviewer" type="text"> </label> <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> </form> </div> <!-- //... --> </div> </template> <script> export default { // .. methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } this.mockReviews.unshift(review) } }, //... } </script>
, , , , :
<!-- ./src/components/Review.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { padding: 0 20px; } .review { border:1px solid #ddd; font-size: 0.95em; padding: 10px; margin: 15px 0 5px 0; } .review h5 { text-transform: uppercase; font-weight: bolder; font-size: 0.7em } .pull-right { float: right; } .review-form { margin-top: 30px; border-top: 1px solid #ddd; padding: 15px 0 0 0; } </style>
movie
Movie
, .
βB3:
, , Vue . β , , . :
touch ./src/bus.js // ./src/bus.js import Vue from 'vue' const bus = new Vue() export default bus
fetchMovies()
:
<!-- ./src/components/Movie.vue --> import bus from '../bus' export default { // ... methods: { fetchMovie (title) { this.loading = true fetch(buildUrl(title)) .then(response => response.json()) .then(data => { this.loading = false this.error_message = '' bus.$emit('new_movie', data.unit) // emit `new_movie` event if (data.errorcode) { this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` return } this.movie = data }).catch(e => { console.log(e) }) } } }
created
Review
:
<!-- ./src/components/Review.vue --> <script> import bus from '../bus' export default { // ... created () { bus.$on('new_movie', movieId => { this.movie = movieId }) }, // ... } </script>
, new_movie
, movie
movieId
, .
, , App.vue
:
<!-- ./src/App.vue --> <template> <div id="app"> <div class="container"> <div class="heading"> <h2><font color="#3AC1EF">samplevue.</font></h2> <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> </div> <div class="row"> <div class="columns small-7"> <movie></movie> </div> <div class="columns small-5"> <reviews></reviews> </div> </div> </div> </div> </template> <script> import Movie from './components/Movie' import Reviews from './components/Reviews' export default { name: 'app', components: { Movie, Reviews } } </script> <style> #app .heading { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin: 60px 0 30px; border-bottom: 1px solid #eee; } </style>
:
npm run dev
, API Netflix .
C: Pusher
, . , , .
, post- , pusher
, .
βC1: Pusher
Pusher . .
βC2:
Node.js. , , package.json
, :
npm install -S express body-parser pusher
server.js
, Express:
// ./server.js /* * Express */ const express = require('express'); const path = require('path'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); /* * Pusher */ const Pusher = require('pusher'); const pusher = new Pusher({ appId:'YOUR_PUSHER_APP_ID', key:'YOUR_PUSHER_APP_KEY', secret:'YOUR_PUSHER_SECRET', cluster:'YOUR_CLUSTER' }); /* * post */ app.post('/review', (req, res) => { pusher.trigger('reviews', 'review_added', {review: req.body}); res.status(200).send(); }); /* * */ const port = 5000; app.listen(port, () => { console.log(`App listening on port ${port}!`)});
Express, Pusher, . YOUR_PUSHER_APP_ID
, YOUR_PUSHER_APP_KEY
, YOUR_PUSHER_SECRET
YOUR_CLUSTER
Pusher.
: /review
. Pusher review_added
reviews
. . trigger
:
pusher.trigger(channels, event, data, socketId, callback);
βC3: API-
config/index.js
, , API -, Vue Webpack. API .
// config/index.js module.exports = { // ... dev: { // ... proxyTable: { '/api': { target: 'http://localhost:5000', // , changeOrigin: true, pathRewrite: { '^/api': '' } } }, // ... } }
addReview
API /src/components/Reviews.vue
:
<!-- ./src/components/Review.vue --> <script> // ... export default { // ... methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { alert('please make sure all fields are not empty') return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } fetch('/api/review', { method: 'post', body: JSON.stringify(review) }).then(() => { this.review.content = this.review.reviewer = '' }) } // ... }, // ... } </script>
βC4:
, , Pusher, . pusher-js:
npm install -S pusher-js
Review.vue
:
<!-- ./src/components/Review.vue --> <script> import Pusher from 'pusher-js' // Pusher export default { // ... created () { // ... this.subscribe() }, methods: { // ... subscribe () { let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' }) pusher.subscribe('reviews') pusher.bind('review_added', data => { this.mockReviews.unshift(data.review) }) } }, // ... } </script>
, Pusher
pusher-js
. subscribe
, :
reviews
pusher.subscribe('reviews')
.
review_added
pusher.bind
. , , . .
D.
server.js
Node- dev- , , API , webpack
:
{ // ... "scripts": { "dev": "node server.js & node build/dev-server.js", "start": "node server.js & node build/dev-server.js", // ... } }
, :
run dev
Vue.js β , . , Pusher.
! -, ?
, :
<!-- ./src/components/Movie.vue --> <template> <!-- // ... --> <div v-if="loading" class="loader"> <img align="center" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader"> </div> <div v-else-if="error_message"> <h3><font color="#3AC1EF">β{{ error_message }}</font></h3> </div> <div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie"> <div class="columns large-7"> <h4> {{ movie.show_title }}</h4> <img :src="movie.poster" :alt="movie.show_title"> </div> <div class="columns large-5"> <p>{{ movie.summary }}</p> <small><strong>Cast:</strong> {{ movie.show_cast }}</small> </div> </div> </template>
, , , :
<!-- ./src/components/Movie.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #movie { margin: 30px 0; } .loader { text-align: center; } </style>
βB2:
R eview
, , , .
v-for
, . :
<!-- ./src/components/Review.vue --> <template> <div class="container"> <h4 class="uppercase">reviews</h4> <div class="review" v-for="review in reviews"> <p>{{ review.content }}</p> <div class="row"> <div class="columns medium-7"> <h5>{{ review.reviewer }}</h5> </div> <div class="columns medium-5"> <h5 class="pull-right">{{ review.time }}</h5> </div> </div> </div> </div> </template> <script> const MOCK_REVIEWS = [ { movie_id: 7128, content: 'Great show! I loved every single scene. Defintiely a must watch!', reviewer: 'Jane Doe', time: new Date().toLocaleDateString() } ] export default { name: 'reviews', data () { return { mockReviews: MOCK_REVIEWS, movie: null, review: { content: '', reviewer: '' } } }, computed: { reviews () { return this.mockReviews.filter(review => { return review.movie_id === this.movie }) } } } </script>
MOCK_REVIEWS . , , .
:
<!-- ./src/components/Review.vue --> <template> <div class="container"> <!-- //... --> <div class="review-form" v-if="movie"> <h5>add new review.</h5> <form @submit.prevent="addReview"> <label> Review <textarea v-model="review.content" cols="30" rows="5"></textarea> </label> <label> Name <input v-model="review.reviewer" type="text"> </label> <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> </form> </div> <!-- //... --> </div> </template> <script> export default { // .. methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } this.mockReviews.unshift(review) } }, //... } </script>
, , , , :
<!-- ./src/components/Review.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { padding: 0 20px; } .review { border:1px solid #ddd; font-size: 0.95em; padding: 10px; margin: 15px 0 5px 0; } .review h5 { text-transform: uppercase; font-weight: bolder; font-size: 0.7em } .pull-right { float: right; } .review-form { margin-top: 30px; border-top: 1px solid #ddd; padding: 15px 0 0 0; } </style>
movie
Movie
, .
βB3:
, , Vue . β , , . :
touch ./src/bus.js // ./src/bus.js import Vue from 'vue' const bus = new Vue() export default bus
fetchMovies()
:
<!-- ./src/components/Movie.vue --> import bus from '../bus' export default { // ... methods: { fetchMovie (title) { this.loading = true fetch(buildUrl(title)) .then(response => response.json()) .then(data => { this.loading = false this.error_message = '' bus.$emit('new_movie', data.unit) // emit `new_movie` event if (data.errorcode) { this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` return } this.movie = data }).catch(e => { console.log(e) }) } } }
created
Review
:
<!-- ./src/components/Review.vue --> <script> import bus from '../bus' export default { // ... created () { bus.$on('new_movie', movieId => { this.movie = movieId }) }, // ... } </script>
, new_movie
, movie
movieId
, .
, , App.vue
:
<!-- ./src/App.vue --> <template> <div id="app"> <div class="container"> <div class="heading"> <h2><font color="#3AC1EF">samplevue.</font></h2> <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> </div> <div class="row"> <div class="columns small-7"> <movie></movie> </div> <div class="columns small-5"> <reviews></reviews> </div> </div> </div> </div> </template> <script> import Movie from './components/Movie' import Reviews from './components/Reviews' export default { name: 'app', components: { Movie, Reviews } } </script> <style> #app .heading { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin: 60px 0 30px; border-bottom: 1px solid #eee; } </style>
:
npm run dev
, API Netflix .
C: Pusher
, . , , .
, post- , pusher
, .
βC1: Pusher
Pusher . .
βC2:
Node.js. , , package.json
, :
npm install -S express body-parser pusher
server.js
, Express:
// ./server.js /* * Express */ const express = require('express'); const path = require('path'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); /* * Pusher */ const Pusher = require('pusher'); const pusher = new Pusher({ appId:'YOUR_PUSHER_APP_ID', key:'YOUR_PUSHER_APP_KEY', secret:'YOUR_PUSHER_SECRET', cluster:'YOUR_CLUSTER' }); /* * post */ app.post('/review', (req, res) => { pusher.trigger('reviews', 'review_added', {review: req.body}); res.status(200).send(); }); /* * */ const port = 5000; app.listen(port, () => { console.log(`App listening on port ${port}!`)});
Express, Pusher, . YOUR_PUSHER_APP_ID
, YOUR_PUSHER_APP_KEY
, YOUR_PUSHER_SECRET
YOUR_CLUSTER
Pusher.
: /review
. Pusher review_added
reviews
. . trigger
:
pusher.trigger(channels, event, data, socketId, callback);
βC3: API-
config/index.js
, , API -, Vue Webpack. API .
// config/index.js module.exports = { // ... dev: { // ... proxyTable: { '/api': { target: 'http://localhost:5000', // , changeOrigin: true, pathRewrite: { '^/api': '' } } }, // ... } }
addReview
API /src/components/Reviews.vue
:
<!-- ./src/components/Review.vue --> <script> // ... export default { // ... methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { alert('please make sure all fields are not empty') return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } fetch('/api/review', { method: 'post', body: JSON.stringify(review) }).then(() => { this.review.content = this.review.reviewer = '' }) } // ... }, // ... } </script>
βC4:
, , Pusher, . pusher-js:
npm install -S pusher-js
Review.vue
:
<!-- ./src/components/Review.vue --> <script> import Pusher from 'pusher-js' // Pusher export default { // ... created () { // ... this.subscribe() }, methods: { // ... subscribe () { let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' }) pusher.subscribe('reviews') pusher.bind('review_added', data => { this.mockReviews.unshift(data.review) }) } }, // ... } </script>
, Pusher
pusher-js
. subscribe
, :
reviews
pusher.subscribe('reviews')
.
review_added
pusher.bind
. , , . .
D.
server.js
Node- dev- , , API , webpack
:
{ // ... "scripts": { "dev": "node server.js & node build/dev-server.js", "start": "node server.js & node build/dev-server.js", // ... } }
, :
run dev
Vue.js β , . , Pusher.
! -, ?
, :
<!-- ./src/components/Movie.vue --> <template> <!-- // ... --> <div v-if="loading" class="loader"> <img align="center" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader"> </div> <div v-else-if="error_message"> <h3><font color="#3AC1EF">β{{ error_message }}</font></h3> </div> <div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie"> <div class="columns large-7"> <h4> {{ movie.show_title }}</h4> <img :src="movie.poster" :alt="movie.show_title"> </div> <div class="columns large-5"> <p>{{ movie.summary }}</p> <small><strong>Cast:</strong> {{ movie.show_cast }}</small> </div> </div> </template>
, , , :
<!-- ./src/components/Movie.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #movie { margin: 30px 0; } .loader { text-align: center; } </style>
βB2:
R eview
, , , .
v-for
, . :
<!-- ./src/components/Review.vue --> <template> <div class="container"> <h4 class="uppercase">reviews</h4> <div class="review" v-for="review in reviews"> <p>{{ review.content }}</p> <div class="row"> <div class="columns medium-7"> <h5>{{ review.reviewer }}</h5> </div> <div class="columns medium-5"> <h5 class="pull-right">{{ review.time }}</h5> </div> </div> </div> </div> </template> <script> const MOCK_REVIEWS = [ { movie_id: 7128, content: 'Great show! I loved every single scene. Defintiely a must watch!', reviewer: 'Jane Doe', time: new Date().toLocaleDateString() } ] export default { name: 'reviews', data () { return { mockReviews: MOCK_REVIEWS, movie: null, review: { content: '', reviewer: '' } } }, computed: { reviews () { return this.mockReviews.filter(review => { return review.movie_id === this.movie }) } } } </script>
MOCK_REVIEWS . , , .
:
<!-- ./src/components/Review.vue --> <template> <div class="container"> <!-- //... --> <div class="review-form" v-if="movie"> <h5>add new review.</h5> <form @submit.prevent="addReview"> <label> Review <textarea v-model="review.content" cols="30" rows="5"></textarea> </label> <label> Name <input v-model="review.reviewer" type="text"> </label> <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> </form> </div> <!-- //... --> </div> </template> <script> export default { // .. methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } this.mockReviews.unshift(review) } }, //... } </script>
, , , , :
<!-- ./src/components/Review.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { padding: 0 20px; } .review { border:1px solid #ddd; font-size: 0.95em; padding: 10px; margin: 15px 0 5px 0; } .review h5 { text-transform: uppercase; font-weight: bolder; font-size: 0.7em } .pull-right { float: right; } .review-form { margin-top: 30px; border-top: 1px solid #ddd; padding: 15px 0 0 0; } </style>
movie
Movie
, .
βB3:
, , Vue . β , , . :
touch ./src/bus.js // ./src/bus.js import Vue from 'vue' const bus = new Vue() export default bus
fetchMovies()
:
<!-- ./src/components/Movie.vue --> import bus from '../bus' export default { // ... methods: { fetchMovie (title) { this.loading = true fetch(buildUrl(title)) .then(response => response.json()) .then(data => { this.loading = false this.error_message = '' bus.$emit('new_movie', data.unit) // emit `new_movie` event if (data.errorcode) { this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` return } this.movie = data }).catch(e => { console.log(e) }) } } }
created
Review
:
<!-- ./src/components/Review.vue --> <script> import bus from '../bus' export default { // ... created () { bus.$on('new_movie', movieId => { this.movie = movieId }) }, // ... } </script>
, new_movie
, movie
movieId
, .
, , App.vue
:
<!-- ./src/App.vue --> <template> <div id="app"> <div class="container"> <div class="heading"> <h2><font color="#3AC1EF">samplevue.</font></h2> <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> </div> <div class="row"> <div class="columns small-7"> <movie></movie> </div> <div class="columns small-5"> <reviews></reviews> </div> </div> </div> </div> </template> <script> import Movie from './components/Movie' import Reviews from './components/Reviews' export default { name: 'app', components: { Movie, Reviews } } </script> <style> #app .heading { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin: 60px 0 30px; border-bottom: 1px solid #eee; } </style>
:
npm run dev
, API Netflix .
C: Pusher
, . , , .
, post- , pusher
, .
βC1: Pusher
Pusher . .
βC2:
Node.js. , , package.json
, :
npm install -S express body-parser pusher
server.js
, Express:
// ./server.js /* * Express */ const express = require('express'); const path = require('path'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); /* * Pusher */ const Pusher = require('pusher'); const pusher = new Pusher({ appId:'YOUR_PUSHER_APP_ID', key:'YOUR_PUSHER_APP_KEY', secret:'YOUR_PUSHER_SECRET', cluster:'YOUR_CLUSTER' }); /* * post */ app.post('/review', (req, res) => { pusher.trigger('reviews', 'review_added', {review: req.body}); res.status(200).send(); }); /* * */ const port = 5000; app.listen(port, () => { console.log(`App listening on port ${port}!`)});
Express, Pusher, . YOUR_PUSHER_APP_ID
, YOUR_PUSHER_APP_KEY
, YOUR_PUSHER_SECRET
YOUR_CLUSTER
Pusher.
: /review
. Pusher review_added
reviews
. . trigger
:
pusher.trigger(channels, event, data, socketId, callback);
βC3: API-
config/index.js
, , API -, Vue Webpack. API .
// config/index.js module.exports = { // ... dev: { // ... proxyTable: { '/api': { target: 'http://localhost:5000', // , changeOrigin: true, pathRewrite: { '^/api': '' } } }, // ... } }
addReview
API /src/components/Reviews.vue
:
<!-- ./src/components/Review.vue --> <script> // ... export default { // ... methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { alert('please make sure all fields are not empty') return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } fetch('/api/review', { method: 'post', body: JSON.stringify(review) }).then(() => { this.review.content = this.review.reviewer = '' }) } // ... }, // ... } </script>
βC4:
, , Pusher, . pusher-js:
npm install -S pusher-js
Review.vue
:
<!-- ./src/components/Review.vue --> <script> import Pusher from 'pusher-js' // Pusher export default { // ... created () { // ... this.subscribe() }, methods: { // ... subscribe () { let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' }) pusher.subscribe('reviews') pusher.bind('review_added', data => { this.mockReviews.unshift(data.review) }) } }, // ... } </script>
, Pusher
pusher-js
. subscribe
, :
reviews
pusher.subscribe('reviews')
.
review_added
pusher.bind
. , , . .
D.
server.js
Node- dev- , , API , webpack
:
{ // ... "scripts": { "dev": "node server.js & node build/dev-server.js", "start": "node server.js & node build/dev-server.js", // ... } }
, :
run dev
Vue.js β , . , Pusher.
! -, ?
, :
<!-- ./src/components/Movie.vue --> <template> <!-- // ... --> <div v-if="loading" class="loader"> <img align="center" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/0.16.1/images/loader-large.gif" alt="loader"> </div> <div v-else-if="error_message"> <h3><font color="#3AC1EF">β{{ error_message }}</font></h3> </div> <div class="row" v-else-if="Object.keys(movie).length !== 0" id="movie"> <div class="columns large-7"> <h4> {{ movie.show_title }}</h4> <img :src="movie.poster" :alt="movie.show_title"> </div> <div class="columns large-5"> <p>{{ movie.summary }}</p> <small><strong>Cast:</strong> {{ movie.show_cast }}</small> </div> </div> </template>
, , , :
<!-- ./src/components/Movie.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> #movie { margin: 30px 0; } .loader { text-align: center; } </style>
βB2:
R eview
, , , .
v-for
, . :
<!-- ./src/components/Review.vue --> <template> <div class="container"> <h4 class="uppercase">reviews</h4> <div class="review" v-for="review in reviews"> <p>{{ review.content }}</p> <div class="row"> <div class="columns medium-7"> <h5>{{ review.reviewer }}</h5> </div> <div class="columns medium-5"> <h5 class="pull-right">{{ review.time }}</h5> </div> </div> </div> </div> </template> <script> const MOCK_REVIEWS = [ { movie_id: 7128, content: 'Great show! I loved every single scene. Defintiely a must watch!', reviewer: 'Jane Doe', time: new Date().toLocaleDateString() } ] export default { name: 'reviews', data () { return { mockReviews: MOCK_REVIEWS, movie: null, review: { content: '', reviewer: '' } } }, computed: { reviews () { return this.mockReviews.filter(review => { return review.movie_id === this.movie }) } } } </script>
MOCK_REVIEWS . , , .
:
<!-- ./src/components/Review.vue --> <template> <div class="container"> <!-- //... --> <div class="review-form" v-if="movie"> <h5>add new review.</h5> <form @submit.prevent="addReview"> <label> Review <textarea v-model="review.content" cols="30" rows="5"></textarea> </label> <label> Name <input v-model="review.reviewer" type="text"> </label> <button :disabled="!review.reviewer || !review.content" type="submit" class="button expanded">Submit</button> </form> </div> <!-- //... --> </div> </template> <script> export default { // .. methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } this.mockReviews.unshift(review) } }, //... } </script>
, , , , :
<!-- ./src/components/Review.vue --> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .container { padding: 0 20px; } .review { border:1px solid #ddd; font-size: 0.95em; padding: 10px; margin: 15px 0 5px 0; } .review h5 { text-transform: uppercase; font-weight: bolder; font-size: 0.7em } .pull-right { float: right; } .review-form { margin-top: 30px; border-top: 1px solid #ddd; padding: 15px 0 0 0; } </style>
movie
Movie
, .
βB3:
, , Vue . β , , . :
touch ./src/bus.js // ./src/bus.js import Vue from 'vue' const bus = new Vue() export default bus
fetchMovies()
:
<!-- ./src/components/Movie.vue --> import bus from '../bus' export default { // ... methods: { fetchMovie (title) { this.loading = true fetch(buildUrl(title)) .then(response => response.json()) .then(data => { this.loading = false this.error_message = '' bus.$emit('new_movie', data.unit) // emit `new_movie` event if (data.errorcode) { this.error_message = `Sorry, movie with title '${title}' not found. Try searching for "Fairy tail" or "The boondocks" instead.` return } this.movie = data }).catch(e => { console.log(e) }) } } }
created
Review
:
<!-- ./src/components/Review.vue --> <script> import bus from '../bus' export default { // ... created () { bus.$on('new_movie', movieId => { this.movie = movieId }) }, // ... } </script>
, new_movie
, movie
movieId
, .
, , App.vue
:
<!-- ./src/App.vue --> <template> <div id="app"> <div class="container"> <div class="heading"> <h2><font color="#3AC1EF">samplevue.</font></h2> <h6 class="subheader">realtime movie reviews with Vue.js and Pusher.</h6> </div> <div class="row"> <div class="columns small-7"> <movie></movie> </div> <div class="columns small-5"> <reviews></reviews> </div> </div> </div> </div> </template> <script> import Movie from './components/Movie' import Reviews from './components/Reviews' export default { name: 'app', components: { Movie, Reviews } } </script> <style> #app .heading { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin: 60px 0 30px; border-bottom: 1px solid #eee; } </style>
:
npm run dev
, API Netflix .
C: Pusher
, . , , .
, post- , pusher
, .
βC1: Pusher
Pusher . .
βC2:
Node.js. , , package.json
, :
npm install -S express body-parser pusher
server.js
, Express:
// ./server.js /* * Express */ const express = require('express'); const path = require('path'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); /* * Pusher */ const Pusher = require('pusher'); const pusher = new Pusher({ appId:'YOUR_PUSHER_APP_ID', key:'YOUR_PUSHER_APP_KEY', secret:'YOUR_PUSHER_SECRET', cluster:'YOUR_CLUSTER' }); /* * post */ app.post('/review', (req, res) => { pusher.trigger('reviews', 'review_added', {review: req.body}); res.status(200).send(); }); /* * */ const port = 5000; app.listen(port, () => { console.log(`App listening on port ${port}!`)});
Express, Pusher, . YOUR_PUSHER_APP_ID
, YOUR_PUSHER_APP_KEY
, YOUR_PUSHER_SECRET
YOUR_CLUSTER
Pusher.
: /review
. Pusher review_added
reviews
. . trigger
:
pusher.trigger(channels, event, data, socketId, callback);
βC3: API-
config/index.js
, , API -, Vue Webpack. API .
// config/index.js module.exports = { // ... dev: { // ... proxyTable: { '/api': { target: 'http://localhost:5000', // , changeOrigin: true, pathRewrite: { '^/api': '' } } }, // ... } }
addReview
API /src/components/Reviews.vue
:
<!-- ./src/components/Review.vue --> <script> // ... export default { // ... methods: { addReview () { if (!this.movie || !this.review.reviewer || !this.review.content) { alert('please make sure all fields are not empty') return } let review = { movie_id: this.movie, content: this.review.content, reviewer: this.review.reviewer, time: new Date().toLocaleDateString() } fetch('/api/review', { method: 'post', body: JSON.stringify(review) }).then(() => { this.review.content = this.review.reviewer = '' }) } // ... }, // ... } </script>
βC4:
, , Pusher, . pusher-js:
npm install -S pusher-js
Review.vue
:
<!-- ./src/components/Review.vue --> <script> import Pusher from 'pusher-js' // Pusher export default { // ... created () { // ... this.subscribe() }, methods: { // ... subscribe () { let pusher = new Pusher('YOUR_PUSHER_APP_KEY', { cluster: 'YOUR_CLUSTER' }) pusher.subscribe('reviews') pusher.bind('review_added', data => { this.mockReviews.unshift(data.review) }) } }, // ... } </script>
, Pusher
pusher-js
. subscribe
, :
reviews
pusher.subscribe('reviews')
.
review_added
pusher.bind
. , , . .
D.
server.js
Node- dev- , , API , webpack
:
{ // ... "scripts": { "dev": "node server.js & node build/dev-server.js", "start": "node server.js & node build/dev-server.js", // ... } }
, :
run dev
Vue.js β , . , Pusher.
! -, ?
Source: https://habr.com/ru/post/343374/
All Articles