そうだ、教祖になろう。出エジプト記 第4章5節 Veu.jsでクライアントサイドを実装する
Freude, schöner Götterfunken,(歓喜よ、美しき光よ)
仕事納めも済ませ、すっかり年末感が強くなりました。
第4章4節 ESLint設定を整えるでリント環境を整えたので、今回は歓喜のうちにクライアントサイドをコーディングしていきます。
軽くコンポーネントを整理しました。
AboutとFaqは後々やるとして、メインコンテンツを実装していきます。
ソースツリーはこちら。
デフォルトのソースはさっぱり消しました。
今年の汚れは今年のうちに。
$ tree src src ├── App.vue ├── components │ └── Life.vue ├── main.js ├── router │ └── index.js ├── store │ └── index.js └── views └── Rebirth.vue $ tree public public ├── api │ └── rebirth ├── favicon.ico └── index.html
Tochter aus Elysium,(楽園エリジウムの娘よ)
まず、サーバサイドがないのでダミーのレスポンスを作ります。
public/api/rebirth
です。
[ "あなたはアジアの小国の王様に生まれ変わりました。", "アジアの小国の王様は今から1000年前に生まれました。", "安定した治世で民に敬われながら生き", "40歳で死にました。" ]
これを読んで表示するsrc/components/Life.vue
です。
複数行をv-for
で繰り返し表示しています。
<template> <div id="life"> <ul> <li v-for="msg in list" :key="msg"> {{ msg }} </li> </ul> </div> </template> <script> import axios from 'axios' export default { name: 'Life', data() { return { list: [], } }, mounted() { axios.get('/api/rebirth').then(response => (this.list = response.data)) }, } </script> <style lang="stylus"> #life padding 10% li list-style-type none </style>
その外側のビューsrc/views/Rebirth.vue
です。
<template> <div class="rebirth"> <Life /> </div> </template> <script> import Life from '@/components/Life.vue' export default { name: 'Rebirth', components: { Life, }, } </script>
src/router/index.js
はさっぱり他のリンクを削除。
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/', name: 'rebirth', component: () => import('@/views/Rebirth.vue'), }, ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, }) export default router
src/App.vue
で統合。
<template> <div id="app"> <router-view /> </div> </template> <style lang="stylus"> #app font-family 'Avenir', Helvetica, Arial, sans-serif -webkit-font-smoothing antialiased -moz-osx-font-smoothing grayscale text-align center color #2c3e50 margin-top 60px </style>
他は変えていません。
プレビューしてみます。
表示されました。
Wir betreten feuertrunken,(我らは炎に酔って踏み入る)
これでもいいのですが、データが一つのコンポーネントに閉じられているので、他コンポーネントで使うときに不便です。
クライアントサイドのデータストアを一元管理して各コンポーネントで利用できるようにしたいと思います。
Vuexで実現します。
Vuexでは最低限、StateとMutationを定義します。
Stateはデータです。
各コンポーネントのdata
と同じと思っていいでしょう。
動的に<template>
配下に反映されます。
Mutationはデータの変更処理です。
変更処理をVuexモジュール内に閉じ込めてカプセル化します。
まず、Mutationの種別を定義します。
新規にsrc/store/mutation-types.js
を作成します。
export const STORE_LIST = 'STORE_LIST'
次にsrc/store/index.js
で実際のStateとMutationを定義します。
state
は単純なリストであるlist
のみ。
mutation
は変数を受けてstate.list
に代入する関数がひとつ。
import Vue from 'vue' import Vuex from 'vuex' import * as types from '@/store/mutation-types' Vue.use(Vuex) export default new Vuex.Store({ state: { list: [], }, mutations: { [types.STORE_LIST](state, payload) { state.list = payload.list }, }, actions: {}, modules: {}, })
最後にデータストアを参照するsrc/components/Life.vue
です。
直にdata
で定義していたlist
を、データストアのlist
からmapState
で取得するよう変更しています。
また、レスポンス受信時の処理はthis.$store.commit
でMutationを起動しています。
<template> <div id="life"> <ul> <li v-for="msg in list" :key="msg"> {{ msg }} </li> </ul> </div> </template> <script> import axios from 'axios' import {mapState} from 'vuex' import * as types from '@/store/mutation-types' export default { name: 'Life', computed: { ...mapState(['list']), }, mounted() { axios.get('/api/rebirth').then(response => { this.$store.commit(types.STORE_LIST, { list: response.data, }) }) }, } </script> <style lang="stylus"> #life padding 10% li list-style-type none </style>
これでさきほどと同じ結果が得られるのですが、試しに他のコンポーネントでもデータストアを参照してみます。
src/views/Rebirth.vue
でlist
の件数を表示してみましょう。
<template> <div class="rebirth"> <Life /> line: {{ list.length }} </div> </template> <script> import {mapState} from 'vuex' import Life from '@/components/Life.vue' export default { name: 'Rebirth', components: { Life, }, computed: { ...mapState(['list']), }, } </script>
Vuexをインポートして、mapState
でlist
を取得し、list.length
で件数を表示しています。
プレビューすると複数コンポーネントでlist
を参照できていることを確認できます。
Himmlische, dein Heiligtum!(天なるもの、汝の聖所へ)
これでは一度読み込んで終わりなので、メッセージがゆっくり現れて消えるようアニメーションをつけたいと思います。
Vue.jsのトランジションを使います。
トランジションでは現れるアニメーションをEnterで、消えるアニメーションをLeaveで定義します。
アニメーション開始時点のEnter/Leaveから、終了時点のtoに至る過程がアニメーションを行う期間activeです。
試しにスライドで現れるEnterだけ定義してみます。
<ul>
全体の表示を制御するshow
をMutationコミット後にtrue
にします。
アニメーション開始時点は透明度100%で右に10pxずれた状態、0.8秒で正規の透明度と位置に遷移します。
<template> <div id="life"> <transition name="slide-fade"> <ul v-if="show"> <li v-for="msg in list" :key="msg"> {{ msg }} </li> </ul> </transition> </div> </template> <script> import axios from 'axios' import {mapState} from 'vuex' import * as types from '@/store/mutation-types' export default { name: 'Life', data() { return { show: false, } }, computed: { ...mapState(['list']), }, mounted() { axios.get('/api/rebirth').then(response => { this.$store.commit(types.STORE_LIST, { list: response.data, }) this.show = true }) }, } </script> <style lang="stylus"> #life padding 10% li list-style-type none .slide-fade-enter-active transition all .8s ease .slide-fade-enter transform translateX(10px) opacity 0 </style>
次にEnterのアニメーションが終わってから一定時間経過後にLeaveのアニメーションを開始したいと思います。
<transition>
タグに@after-enter
属性をつけました。
Enterのアニメーションが終わったら、methods
に追加したafterEnter
を呼び出されてshow
をfalse
にするとLeaveアニメーションが始まります。
同じくLeaveのアニメーションが終わったら、methods
のrebirth
に移動したAjax処理を呼び出します。
これで延々と表示・非表示を繰り返すようになりました。
transform translateX(10px)
は消しました。
<template> <div id="life"> <transition name="fade" @after-enter="afterEnter" @after-leave="afterLeave" > <ul v-if="show"> <li v-for="msg in list" :key="msg"> {{ msg }} </li> </ul> </transition> </div> </template> <script> import axios from 'axios' import {mapState} from 'vuex' import * as types from '@/store/mutation-types' export default { name: 'Life', data() { return { show: false, } }, computed: { ...mapState(['list']), }, mounted() { this.rebirth() }, methods: { rebirth() { axios.get('/api/rebirth').then(response => { this.$store.commit(types.STORE_LIST, { list: response.data, }) this.show = true }) }, afterEnter() { var _this = this setTimeout(() => (_this.show = false), 3000) }, afterLeave() { var _this = this setTimeout(() => _this.rebirth(), 2000) }, }, } </script> <style lang="stylus"> #life padding 10% li list-style-type none .fade-enter-active, .fade-leave-active transition all .8s ease .fade-enter, .fade-leave-to opacity 0 </style>
基本の動きはできました。
まだ画像や凝ったアニメーションはありませんが、やり出すと時間ばかりかかるので、一旦この辺で。
次回はクライアントサイドの自動テストを考えたいと思います。