シャッフル主任の進捗報告

興味のあるものを作ります。進捗を不定期にご報告します。

そうだ、教祖になろう。出エジプト記 第4章5節 Veu.jsでクライアントサイドを実装する

Freude, schöner Götterfunken,(歓喜よ、美しき光よ)


仕事納めも済ませ、すっかり年末感が強くなりました。

www.youtube.com

第4章4節 ESLint設定を整えるでリント環境を整えたので、今回は歓喜のうちにクライアントサイドをコーディングしていきます。

軽くコンポーネントを整理しました。
AboutとFaqは後々やるとして、メインコンテンツを実装していきます。

f:id:chief-shuffle:20191227063057p:plain

ソースツリーはこちら。
デフォルトのソースはさっぱり消しました。
今年の汚れは今年のうちに。

$ 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>

他は変えていません。
プレビューしてみます。

f:id:chief-shuffle:20191227065305j:plain

表示されました。

Wir betreten feuertrunken,(我らは炎に酔って踏み入る)


これでもいいのですが、データが一つのコンポーネントに閉じられているので、他コンポーネントで使うときに不便です。
クライアントサイドのデータストアを一元管理して各コンポーネントで利用できるようにしたいと思います。
Vuexで実現します。

f:id:chief-shuffle:20191227065343p:plain

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.vuelistの件数を表示してみましょう。

<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をインポートして、mapStatelistを取得し、list.lengthで件数を表示しています。
プレビューすると複数コンポーネントlistを参照できていることを確認できます。

f:id:chief-shuffle:20191227193644j:plain

Himmlische, dein Heiligtum!(天なるもの、汝の聖所へ)


これでは一度読み込んで終わりなので、メッセージがゆっくり現れて消えるようアニメーションをつけたいと思います。
Vue.jsのトランジションを使います。
トランジションでは現れるアニメーションをEnterで、消えるアニメーションをLeaveで定義します。
アニメーション開始時点のEnter/Leaveから、終了時点のtoに至る過程がアニメーションを行う期間activeです。

https://jp.vuejs.org/images/transition.png

試しにスライドで現れる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を呼び出されてshowfalseにするとLeaveアニメーションが始まります。
同じくLeaveのアニメーションが終わったら、methodsrebirthに移動した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>

基本の動きはできました。
まだ画像や凝ったアニメーションはありませんが、やり出すと時間ばかりかかるので、一旦この辺で。

次回はクライアントサイドの自動テストを考えたいと思います。