Vue.js と Vue Router でアニメーション付きのページ遷移

最終更新日
2018.03.05

Vue Router を使った SPA のウェブページ構築と遷移アニメーションを作ります。

前回 Vue CLI で作った学習用のプロジェクトを引き続き使って Vue Router をもっとちゃんと使っていきます。 全体のコードはここのリポジトリあります。

Vue Router とは

Vue Router は SPA サイトを作るためのルーティング処理をサポートする Vue.js の拡張ライブラリです。 特定の URL とコンポーネントを紐付けます。

出来上がったデモ

公式のカートのサンプルを真似して JavaScript のモック API にしたので追加や削除も機能するようになりました。 リロードしなければデータを維持します。

ルートの定義とルータービュー

それぞれのページはコンポーネントとして定義します。 単一ファイルコンポーネントにする場合、ルートと直接紐付けるコンポーネントは「views」というディレクトリにまとめることが多いようです。

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

const Home = { template: '<div>ホーム</div>' }
const Page1 = { template: '<div>ページ1</div>' }
const Page2 = { template: '<div>ページ2</div>' }

export default new Router({
  routes: [
    { path: '/', component: Home },
    { path: '/page1', component: Page1 },
    { path: '/page2', component: Page2 }
  ]
})

もはやこれだけで、簡単な SPA のでき上がりです。

ルートとマッチしたコンポーネントを表示させたい場所に<router-view/>を記述します。

<div id="app">
  <!-- ここにコンテンツを表示 -->
  <router-view></router-view>
</div>

ハッシュを付けない設定

デフォルトでは自動的にURLに「#」が付きますが、ルーターのオプションでヒストリモードにすると、URL にハッシュが付かなくなります。 しかし、リロードや URL に直接アクセスしたときに Not Found になってしまうので .htaccess でリライトモジュールの設定をします。 このプロジェクトの場合はサブディレクトリがあるのでこんな感じです。

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /vue-test/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

ネストされたルート

1層目のルータービューにホームとメンバーページを表示させ、メンバーページの中にネストされたルータービューでメンバーリストとメンバー詳細ページを表示するようにします。

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

export default new Router({
  // ヒストリモードはハッシュがつかなくなる ※mod_rewrite等が必要
  // mode: 'history',
  // サブディレクトリがある場合ベースに設定する
  base: '/vue-test/',
  routes: [
    {
      path: '/',
      component: require('@/views/Home'),
    },
    {
      path: '/member',
      component: require('@/views/Member'),
      // ネストされたルート
      children: [
        {
          path: '',
          name: 'member-list',
          component: require('@/views/Member/List')
        },
        {
          path: ':id',
          name: 'member-detail',
          component: require('@/views/Member/Detail'),
          // パラメータをpropsとして渡すことでRouterに依存しなくなる
          props: route => ({ id: Number(route.params.id) })
        }
      ]
    }
  ]
})
  • http://localhost/vue-test/member メンバーの一覧を表示
  • http://localhost/vue-test/member/1 メンバー1の詳細を表示

ルートをネストすると、親となるコンポーネントのテンプレートのでも<router-view/>を記述できます。 たとえば、この定義の場合はviews/Memberコンポーネントに記述することで、その場所に子ルートのコンテンツを描画します。 別のトランジションエフェクトを付けたりカテゴリのサイドバーを残して一部だけを遷移させるといったこともできるようになります。 ツイッターのポップアップモーダル的な遷移も多分これでできそうです。

ルーターリンクと現在の位置をハイライト

Vue Router の組み込みコンポーネントの<router-link/>でルーター用のリンクを作成することができます。

<router-link to="/">Home</router-link>
<router-link to="/page1">Page1</router-link>
<router-link to="/page2">Page2</router-link>

<router-link/>を使っていれば自分と親のパスに自動的に.router-link-activeというクラス名が付きます。 ホームのパス「/」は他のすべてのパスにも含まれているため、他のページを見ているときも常にホームがアクティブになってしまいます。 この場合は exact を付けると良いようです。

<router-link to="/" exact>Home</router-link>

IDなどのパラメータを付けたい場合は以下のように書きます。 単純ならテンプレートリテラルとか使って書いても良い。

<router-link :to="{ name: 'member-detail', params: { id }}">{{ name }}</router-link>

ページ遷移エフェクトを作る

お試しだったので結構長めのエフェクトなんですがルートの遷移中にオーバーレイで SVG を表示させるようにしました。

単純に<router-view/>を囲むだけだと遷移先の読み込みが終わるまでトランジションが始まらないので、ViewLoading というコンポーネントを1つ作り、ストアにある状態を使って表示・非表示させるようにしました。

router-view 用のモジュール

export default {
  namespaced: true,
  state: {
    loading: false
  },
  mutations: {
    start(state) { state.loading = true },
    end(state) { state.loading = false }
  }
}

コンポーネントにすると複雑なアニメーションも作りやすい(๑'ᴗ'๑)

ViewLoading コンポーネント

<template>
  <transition name="router">
    <div class="overlay" v-if="loading">ローディング用の SVG とか</div>
  </transition>
</template>
<script>
export default {
  computed: {
    loading() { return this.$store.state.view.loading }
  }
}
</script>

ルーターのグローバルフックの beforeEachafterEachloading の状態を更新していますが、ネストがあると子ルートを区別するのは少し複雑になりますね。 メタフィールドを設定するといいのかもしれません。

src/router/index.js

router.beforeEach((to, from, next) => {
  store.commit('view/start')
  next()
})
router.afterEach((to, from) => {
  store.commit('view/end')
})

メンバー一覧のところだけわざと遅延をいれてますが、実際はロードが長いページだけ表示されます。 2層目のメンバーページのルータービューにはまた別の比較的シンプルなエフェクトを付けました。

初期データの読み込み

最初から表示させるデータを読み込んでから遷移させたい場合はナビゲーションガードbeforeRouteEnter を使用します。

export default {
  beforeRouteEnter(to, from, next) {
    store.dispatch('member/get', to.params.id).then(() => {
      next() // 読み込み終わったら next() で遷移させる
    })
  }
}

Vue Router を使うと以下のようなナビゲーションガードが使用できるようになり、コンポーネント側から遷移をコントロール出来ます。

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

開発中ホットリロードで beforeRouteEnter が実行されずにデータが読み込まれない事があるので、その場合は以下のように対策してます。 何かいい方法ないだろうか。

export default {
  created() {
    if (process.env.NODE_ENV !== 'production') {
      store.dispatch('member/get', this.id)
    }
  }
}

<keep-alive>

ルータービューに keep-alive を設定しておくと状態をキャッシュするので関連したページを分ける時に凄く便利でした。

mounted などのライフサイクルは更新が必要な場合にだけ呼び出されるようになります。 例えば関係ないページに移動したときだけ beforeDestroy が発生してデータを破棄させたり出来ます。

おわりに

SPA はフロント作業を Vue.js だけに集中出来るので、デザインを作るのに PHP とかと行ったり来たりするよりも凄く楽です! また後でサーバーサイドレンダリングも試してみたいです(ずっと言ってる)。 今のままだと誰でも編集できちゃうよみたいな感じなので次回は認証関係をやる予定です多分(๑'ᴗ'๑)