
VueQuest第3回の講義では、下記の内容について学んでいきましょう。
- サーバと通信して動画情報を取得
- 通信エラーメッセージ・ロード中の表示
- サーバと通信して動画投稿機能を実装
今回は、いよいよ本講義の目玉でもあるサーバとの通信の部分をメインで実装していきます。フロントエンド開発でもサーバとの通信は非常に重要な要素になってきますので、是非とも学習に励んで下さい。
この記事の目次
axios のインストール
サーバと通信するに当たって、必要なライブラリをインストールしていきましょう。
axios(アクシオス)というHTTP通信を行うためのライブラリをインストールしていきます。ターミナル上で、自分のvue-questディレクトリに移動してから、下記コマンドを実行して下さい。
$ npm install axios
正常にインストールされたか確認するため、package.jsonの12行目辺りが内容が下記のように書き換わったことを確認して下さい。
皆さんの環境によって、axiosのバージョンや記述の行数が異なることがありますが問題ありませんのでご安心ください。
"axios": "^0.21.0",
動画取得機能の実装
Top.vueの変更
下記を参考に、コードを書き換えて下さい。
<template>
<v-container>
<input-form
formRounded="xl"
:formElevation="formElevation"
@storeMovie="storeMovie"
/>
<movies
ref="movies"
:movie-items="movieItems"
/>
</v-container>
</template>
<script>
import InputForm from './InputForm.vue'
import Movies from './Movies.vue'
import axios from 'axios'
export default {
components: {
InputForm,
Movies,
},
data () {
return {
formElevation: "10",
movieItems: [{}],
}
},
created () {
this.getMovies()
},
methods: {
getMovies () {
axios.get('https://youtube-curation.herokuapp.com/rest/1'
).then((response) => {
this.movieItems = response.data.user.movies
}).catch((error) => {
console.log(error)
}).finally(() => {
this.$refs.movies.init()
})
},
storeMovie (movieUrl, comment) {
console.log(movieUrl, comment)
}
},
}
</script>
まず、axiosをこちらのコンポーネントで利用できるように、importすることで呼び出しています。
そして、今回はcreated()という関数の中で動画取得機能getMovies()を呼び出しています。created()という関数はページがロードされた初期のタイミングで実行される関数です。つまり、created()の中身に処理内容を書いておけば、ページをロードすれば自動的にその中身の処理が実行されるということになります。
ちなみに、created()はライフサイクルフックという関数の1つです。ライフサイクルフックとは、Vueインスタンスが生成されてから破棄されるまでの間に使える関数のことを指します。様々な関数がありますので、ここでは詳しくは解説しませんが、ご興味がある方は下記の公式ページなどを見てみて下さい。
https://jp.vuejs.org/v2/guide/instance.html
getMovies()の中身を確認していきましょう。
axios.getでアクシオスを使ってgetメソッドでサーバから情報を取得するという意味を表します。getメソッドの引数に書いているのは、通信先サーバのURLです。
今回書いているURLは、LaravelQuestという別講座で作ったアプリのURLで、このURLが提供してくれる動画のURL・コメント・IDを利用して、情報をVue.jsに表示させていきます。こちらのサーバサイドの講座LaravelQuestに興味のある方は、別講座LaravelQuestをご覧ください。
次に、thenでaxiosの通信が成功した場合の処理を書いています。response に通信の結果サーバから返ってきた値が入ってきますので、その値の中身をmovieItemsというデータの中に入れて、更にそのデータを次で作成するMoviesという子コンポーネントにpropsを使って渡しています。つまり、サーバから受け取った値を、Moviesコンポーネントに渡す処理をしているということです。
catchは、通信の際にエラーが発生したら、コンソールにそのエラー内容を表示するという処理を書いています。finallyは、通信に成功した場合(then)でも失敗した場合(catch)でも必ず通る処理のことで、今回は次で記述するMoviesコンポーネントの初期化処理(initメソッド)を行うという内容を記載しています。
Movies.vueの作成
下記を参考に、新規ファイルを追加してください。
<template>
<v-row>
<v-col
v-for="item in movieInternalItems" :key="item.id"
cols="12"
sm="6"
md="4"
style="text-align: center"
>
<div>
<iframe
:src="item.url"
width="290"
height="163.125"
frameborder="0"
/>
<p>{{ item.comment }}</p>
</div>
</v-col>
</v-row>
</template>
<script>
export default {
props: {
movieItems: {
type: Array,
default: null,
},
},
data () {
return {
movieInternalItems: [],
}
},
methods: {
// 動画の初期表示処理
init () {
this.movieInternalItems = []
this.movieItems.forEach(item => {
console.log(item)
const newItem = {
id: item.id,
url: 'https://www.youtube.com/embed/' + item.url + '?controls=1&loop=1&playlist=' + this.movieItems[i].url,
comment: item.comment,
}
this.movieInternalItems.push(newItem)
})
},
},
}
</script>
v-forという属性(ディレクティブともいいます)について解説します。v-forディレクティブは、v-forのイコールの後に「繰り返し処理」を書くことで、v-colというタグを繰り返しによって何個も複製することができます。この場合は、繰り返し処理を使うことで、サーバから受け取った動画情報の数だけ動画情報を表示する処理を行っています。
また、v-row, v-colでvuetifyのグリッドシステムを利用しています。完成したアプリを見て頂けると分かりますが、cols, sm, md で画面幅によって1つの動画の占領する幅をレスポンシブに決めています。
v-colの中の要素には、iframeを使います。iframeは自身のサイトの中に他のページをはめ込む際に便利なタグです。iframeのsrc属性の中には、サーバから取得した動画のURLを入れてYouTube動画を表示させるようにします。iframeの次には、同じくサーバから取得した動画のコメント情報を入れて、動画に関するコメントを表示させるようにしましょう。
次に、<script>タグの中を記述していきましょう。
まず、propsで親から受け取った動画情報movieItemsの制約を記述しています。次にdataの中でmovieInternalItemsという新しいデータを定義しています。
なぜ新しいデータを定義しているかというと、親から受け取ったmovieItemsを加工して新しいデータmovieInternalItemsの中に移し替えるためです(親から受け取った値をそのまま利用することも可能なのですが、そのまま利用すると若干扱いづらい部分があるため)。その移し替える処理を、次のinit()メソッドで行っています。
initの中では、親から受け取った動画情報movieItemsをforEachでループさせて1つ1つの動画情報を取り出すことで移し替えを実行しており、動画の表示に必要な id, url, comment というプロパティをそれぞれ定義しています。urlでは、サーバから取得したURLを含めて、YouTubeの動画再生可能なURLとして記述し直しています。最後に、取り出した動画情報をpushによってmovieInternalItemsの中身として詰め込み直せば完了です。
上記の内容で、サーバから取得した動画情報が上手く表示されればOKです。
通信エラーメッセージ
サーバとの通信が上手くいかず、動画情報を取得できなかったりする場合もあるかと思います。その際のエラーメッセージを表示できるようにしていきましょう。
Top.vueの変更
下記を参考に、コードを書き換えて下さい。
<template>
<v-container>
<input-form
formRounded="xl"
:formElevation="formElevation"
@storeMovie="storeMovie"
/>
<v-messages
:value="responseError"
color="red"
class="response-error my-5 text-center"
/>
<movies
ref="movies"
:movie-items="movieItems"
/>
</v-container>
</template>
data () {
return {
formElevation: "10",
movieItems: [{}],
responseError: [],
}
},
methods: {
getMovies () {
axios.get('https://youtube-curation.herokuapp.com/rest/1'
).then((response) => {
this.movieItems = response.data.user.movies
}).catch((error) => {
console.log(error)
this.responseError = ['動画の取得に失敗しました']
}).finally(() => {
this.$refs.movies.init()
})
},
ここでは、v-messagesというvuetifyのコンポーネントに、通信エラーが起きた場合にエラーメッセージを代入することで、メッセージを表示させています。
<style>
.response-error .v-messages__message {
font-size: 18px
}
</style>
最後に、CSSでメッセージの大きさなどを調整します。.v-messages__messageというのは、vuetifyのv-messagesコンポーネントに割り振られているクラス名です。Chromeのディベロッパーツールなどで見ていただくと、そのようなクラス名が付いているのが分かるかと思います。
なお、今回は使いませんが、styleタグにscopedと記述すると、CSSの適用範囲をそのコンポーネント内だけに限定することができます。scopedはよく使われるので是非覚えておいて下さい。
<style scoped>
ちなみに、今回は、v-messagesという外部コンポーネントに存在するクラスを操作したいので、scopedは利用していません。
ロード中の表示
現状だと、画面をロードした場合、タイムラグがあってから動画情報が表示されるようになっているかと思います。画面ロード直後、動画情報がロード中だと分かるような表示を実装していきましょう。
Top.vueの変更
下記を参考に、コードを書き換えて下さい。
<movies
ref="movies"
:movie-items="movieItems"
:loading="loading"
/>
data () {
return {
formElevation: "10",
movieItems: [{}],
responseError: [],
loading: true,
}
},
methods: {
getMovies () {
axios.get('https://youtube-curation.herokuapp.com/rest/1'
).then((response) => {
this.movieItems = response.data.user.movies
}).catch((error) => {
console.log(error)
this.responseError = ['動画の取得に失敗しました']
}).finally(() => {
setTimeout(() => {
this.loading = false
}, 1000)
this.$refs.movies.init()
})
},
ここでは、loadingという新しい値を作って、loadingが初期状態でtrueなら「ロード中」、動画取得が終了してfalseになったら「ロード終了」という場合分けをしていきたいと思います。
setTimeoutという関数を使うことで、少なくとも1秒間は「ロード中」の表示になるようにしています。
Movie.vueの変更
下記を参考に、コードを書き換えて下さい。
<template>
<v-row>
<template
v-if="loading"
>
<v-container
class="px-10 py-10"
style="text-align:center"
>
<v-progress-circular
size="70"
color="blue"
indeterminate
/>
</v-container>
</template>
<template
v-else
>
<v-col
v-for="item in movieInternalItems" :key="item.id"
cols="12"
sm="6"
md="4"
style="text-align: center"
>
<div>
<iframe
:src="item.url"
width="290"
height="163.125"
frameborder="0"
/>
<p>{{ item.comment }}</p>
</div>
</v-col>
</template>
</v-row>
</template>
<script>
export default {
props: {
movieItems: {
type: Array,
default: null,
},
loading: {
type: Boolean,
required: true,
default: true,
},
},
propsでloadingという値を親コンポーネントから受け取っています。
ここで、v-ifディレクティブ・v-elseディレクティブという新しい属性が登場しています。これは、上記の場合分けを実現させるために記述していまして、loadingが初期状態でtrueなら「ロード中の表示(vuetifyのv-progress-circular)を行う」、動画取得が終了してloadingがfalseになったら「動画情報を表示する」という意味合いになります。
つまり、v-if/v-else の条件分岐をloadingという値がtrueかfalseかによって判定しているということです。
v-if/v-else で templateタグを利用しているのは、templateはHTMLタグに現れてこないタグだからです。今回のように条件分岐だけさせればOKで、タグとして表示させたくない場合はtemplateタグを使って下さい。
ここまでで動作確認をしてみて、上手くロード中・ロード終了の表示が出来ればOKです。
動画投稿機能(+順序並び替え)の実装
Top.vueの変更
下記を参考に、コードを書き加えて下さい。
storeMovie (movieUrl, comment) {
console.log(movieUrl, comment)
this.loading = true
this.responseError = []
axios.post('https://youtube-curation.herokuapp.com/rest', {
url: movieUrl,
comment: comment,
}).then((response) => {
this.movieItems = response.data.movies
}).catch((error) => {
console.log(error)
this.responseError = ['動画の投稿に失敗しました']
}).finally(() => {
setTimeout(() => {
this.loading = false
}, 1000)
this.$refs.movies.init()
})
},
動画取得ではaxios.getメソッドで行いましたが、今回は投稿処理なのでaxios.postメソッドを使っていきます。postの第一引数には前回と同じくURLを取り、第二引数には第一引数のURLに送りたいパラメータを指定していきます。今回は、投稿動画のURLとコメントを送ります。
動画投稿に成功した場合、失敗した場合の処理、最終の処理は動画取得機能とほぼ同じです。
Movie.vueの変更
新しく投稿した動画が、動画群の先頭に来た方が分かりやすいと思います。なので、新しい動画情報が先頭に来るように、順番の並び替えを行いたいと思います。
下記を参考に、コードを書き換えて下さい。
methods: {
// 動画の初期表示処理
init () {
this.movieInternalItems = []
Object.keys(this.movieItems).forEach(i => {
const newItem = {
id: this.movieItems[i].id,
url: 'https://www.youtube.com/embed/' + this.movieItems[i].url + '?controls=1&loop=1&playlist=' + this.movieItems[i].url,
comment: this.movieItems[i].comment,
}
this.movieInternalItems.push(newItem)
})
this.movieInternalItems.sort(this.descending)
},
// IDの降順に並び換え
descending (a, b) {
let comparison = 0
if (a.id > b.id) {
comparison = -1
} else if (b.id > a.id) {
comparison = 1
}
return comparison
},
},
initメソッドの最後で、ソートを掛けてdescending関数を通すことで、順番をIDの降順(新しいもの順)で並び替えています。
descending関数の中身は少し複雑ですが、下記ような意味を表しています。
- aという動画情報のIDが、bという動画情報のIDよりも大きい(新しい)場合、並び替えをしない(-1はfalseと同義なので、falseを返す)
- bという動画情報のIDが、aという動画情報のIDよりも大きい(新しい)場合、並び替えをする(1はtrueと同義なので、trueを返す)
ここまでできたら、動画を実際に投稿してみましょう。好きなYouTube動画のURLの「v=」の後の11桁の文字列を動画IDに入力して投稿してみて下さい。
無事投稿できて、投稿した動画が動画群の先頭に来ていれば実装は完了です。
次回#4はこちら
今回の講義は以上となります。
次は、「ドラッグ&ドロップによる動画削除機能」を実装した後、アプリを世界に公開していきます。