目次
四部作です。
- Todoアプリ作成編 / サーバーサイド
- Todoアプリ作成編 / フロントエンド ← 今ここ
- [JWTAuthでログイン編] (http://qiita.com/acro5piano/items/eb29f13b82f386220460)
今回は、Vue.jsを使ってSPAを作っていきます。
これまでに作成したREST APIに問い合わせて、画面を更新します。
JS初心者なので、おかしいところとかベストじゃないところが多々あると思いますので
ご指摘頂けたら嬉しいです!
なお、ユニットテストはまだ作ってません・・・
Vue インスタンスについて
Vue.js はコンポーネント志向なので、
- Template
- Script
- Style
がひとまとまりになった .vue
という拡張子のコンポーネントを作ります。
それを組み合わせて、クライアントサイドでのページ遷移を実現しています。
詳しくは本家サイトをご覧下さい。
https://vuejs.org/v2/guide/components.html
Root Component
まずは大元のインスタンスである app.vue
を作ります。
<template>
<div id="app">
<div class="container">
<router-view></router-view>
</div>
<hr>
<div class="container-fluid">
<a href="https://github.com/acro5piano/laravel-vue-jwtauth-spa-todo-app" target="_blank">
<img src="https://image.flaticon.com/icons/svg/25/25231.svg" width="30" height="20">
</a>
</div>
</div>
</template>
このコンポーネントを読み込む app.js
を作ります。
ブラウザがサーバーからレスポンスを受取った後、最初に実行されるエントリーポイント?という認識です。
import Vue from 'vue'
require('bootstrap-sass')
const app = new Vue({
el: '#app',
render: h => h(require('./app.vue')),
})
Child component + Routing
親コンポーネントができたので、子コンポーネントを作っていきます。
まずは about us
という静的なコンポーネントから作ります。
<template>
<div>
This page describes who we are.
</div>
</template>
このコンポーネントを vue-router
に登録します。
import VueRouter from 'vue-router'
import Vue from 'vue'
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',
routes: [
{ path: '/about', component: require('./components/About.vue') },
],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
})
app.js
に書いても良いのですが、肥大化してしまうので別ファイルにするのが定石のようです。
何となく分かると思いますが、
/about
というURLで About.vue
コンポーネントが、
app.vue
の <router-view></router-view>
にマウントされます。
mode: 'history',
のところで、HTML5の push state
を利用しています。
mode
のデフォルトは hash
です。
scrollBehavior
の部分は、ブラウザバックでスクロール位置を保つためのもので、
おまじないだと思っててOKです。
作った router.js
を app.js
から読み込みます。
import Vue from 'vue'
// 追加
import router from './router'
require('bootstrap-sass')
const app = new Vue({
// 追加
router,
el: '#app',
render: h => h(require('./app.vue')),
})
試す
この時点で、 localhost:8000/about
にアクセスすると、
下記のようにルーティングしてくれるはずです。
Layoutを作る
先に全てのコンポーネントとそれに対するルーティングを作ってしまいましょう。
今回必要なルーティングは
/
/about
/login
の3種類です。 なお、 /
でタスク一覧が表示されるようにします。
なので、必要な子コンポーネントはそれぞれ
components/Tasks.vue
components/About.vue
components/Login.vue
です。
これに加えて、常に表示されるナビゲーションバーとして
components/Navbar.vue
も必要です。
とりあえずずらーっと書いてしまいます。
<template>
<div>
please <router-link to="/login">Login.</router-link>
<div>
<strong>Hello, Kazuya!</strong>
<p>Your tasks here.</p>
<ul>
<li>
Learn Vue.js
</li>
<button class="btn btn-sm btn-success">Done</button>
<button class="btn btn-sm btn-danger">Remove</button>
</ul>
<div class="form-group">
<div class="alert alert-danger" role="alert">
Task name should not be blank.
</div>
<input type="text" class="form-control" placeholder="new task...">
<button class="btn btn-primary">
Add task
</button>
</div>
</div>
</div>
</template>
<template>
<div>
This page describes who we are.
</div>
</template>
<template>
<div>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Login</div>
<div class="panel-body">
<div class="alert alert-danger" role="alert">
Wrong email or password.
</div>
<div class="form-group">
<label for="email" class="col-md-4 control-label">E-Mail Address</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control" required autofocus>
</div>
</div>
<div class="form-group">
<label for="password" class="col-md-4 control-label">Password</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control" required autofocus>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button type="submit" class="btn btn-primary">
Login
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<template>
<nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"
aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<router-link to="/" class="navbar-brand">Vue TODO</router-link>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
<li><router-link to="/about">About</router-link></li>
<li>
<router-link to="/login">Log in</router-link>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
</template>
次に、作成したコンポーネントをRouterに登録します。
import VueRouter from 'vue-router'
import Vue from 'vue'
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: require('./components/Tasks.vue') }, // 追加
{ path: '/about', component: require('./components/About.vue') },
{ path: '/login', component: require('./components/Login.vue') }, // 追加
],
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
},
})
Navbarを配置します。
Routerで変化する部分ではないので、 app.vue
に書きます。
<template>
<div id="app">
<!-- 追加 -->
<navbar></navbar>
<div class="container">
<router-view></router-view>
</div>
<hr>
<div class="container-fluid">
<a href="https://github.com/acro5piano/laravel-vue-jwtauth-spa-todo-app" target="_blank">
<img src="https://image.flaticon.com/icons/svg/25/25231.svg" width="30" height="20">
</a>
</div>
</div>
</template>
<script>
// この部分全部追加
export default {
components: {
navbar: require('./components/Navbar.vue'),
},
}
</script>
この時点で、SPAっぽい画面遷移をするようになっているはずです。
アラートが常に出ていますが、気にしないで下さい。
次から動的なページにしていきます。
axios を使ってリクエストを飛ばす
いよいよ tasks
をAPIサーバーから取得する処理を書いていきます。
axios
を使ってリクエストするのですが、方法はいくつかあります。
Laravelをインストールした直後は、
window.axios = require('axios');
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest'
};
となっており、 window.axios.get(...
という書き方ができるようになっています。
が、これだとどのコンポーネントもリクエストを発行できるので、
アプリケーション全体の見通しが悪くなりがちです。
そこで、 services/http.js
を作成して、リクエストはそこに一任させるような設計にします。
これも koel のパクリです。
import axios from 'axios'
/**
* Responsible for all HTTP requests.
*/
export default {
request (method, url, data, successCb = null, errorCb = null) {
axios.request({
url,
data,
method: method.toLowerCase()
}).then(successCb).catch(errorCb)
},
get (url, successCb = null, errorCb = null) {
return this.request('get', url, {}, successCb, errorCb)
},
post (url, data, successCb = null, errorCb = null) {
return this.request('post', url, data, successCb, errorCb)
},
put (url, data, successCb = null, errorCb = null) {
return this.request('put', url, data, successCb, errorCb)
},
delete (url, data = {}, successCb = null, errorCb = null) {
return this.request('delete', url, data, successCb, errorCb)
},
/**
* Init the service.
*/
init () {
axios.defaults.baseURL = '/api'
// Intercept the request to make sure the token is injected into the header.
axios.interceptors.request.use(config => {
config.headers['X-CSRF-TOKEN'] = window.Laravel.csrfToken
config.headers['X-Requested-With'] = 'XMLHttpRequest'
return config
})
}
}
これの初期化を app.js
でやってあげます。
import Vue from 'vue'
import router from './router'
import http from './services/http.js' // 追加
require('bootstrap-sass')
const app = new Vue({
router,
el: '#app',
// 追加
created () {
http.init()
},
render: h => h(require('./app.vue')),
}).$mount('#app')
Task コンポーネントで、この http
サービスを利用してリクエストを飛ばします。
<template>
<div>
please <router-link to="/login">Login.</router-link>
<div>
<strong>Hello, Kazuya!</strong>
<p>Your tasks here.</p>
<ul v-for="task in tasks">
<li v-if="task.is_done">
<strike> {{ task.name }} </strike>
</li>
<li v-else>
{{ task.name }}
</li>
<button @click="completeTask(task)" class="btn btn-sm btn-success" v-if="task.is_done">Undo</button>
<button @click="completeTask(task)" class="btn btn-sm btn-success" v-else>Done</button>
<button @click="removeTask(task)" class="btn btn-sm btn-danger">Remove</button>
</ul>
<div class="form-group">
<div class="alert alert-danger" role="alert" v-if="showAlert">
{{ alertMessage }}
</div>
<input type="text" class="form-control"
v-model="name" @keyup.enter="addTask" placeholder="new task...">
<button class="btn btn-primary" disabled="disabled" v-if="name === ''">
Add task
</button>
<button class="btn btn-primary" @click='addTask' v-else>
Add task
</button>
</div>
</div>
</div>
</template>
<script>
import http from '../services/http'
export default {
mounted() {
this.fetchTasks()
},
data() {
return {
tasks: [],
name: '',
showAlert: false,
alertMessage: '',
}
},
methods: {
fetchTasks () {
// TODO: not to send request when the user is not authenticated
http.get('tasks', res => {
this.tasks = res.data
})
},
addTask () {
if (this.name === '') {
this.showAlert = true
this.alertMessage = 'Task name should not be blank.'
return false
}
http.post('tasks', {name: this.name}, res => {
this.tasks[res.data.id] = res.data
this.name = ''
this.showAlert = false
this.alertMessage = ''
})
},
completeTask (task) {
http.put('tasks/' + task.id, {is_done: !task.is_done}, res => {
this.tasks[task.id] = res.data
this.$forceUpdate()
})
},
removeTask (task) {
http.delete('tasks/' + task.id, {}, () => {
delete this.tasks[task.id]
this.$forceUpdate()
})
},
}
}
</script>
上手く行けばこんな感じになると思います。
ここまで読んで頂き、ありがとうございました。