これは、モバイルファクトリー Advent Calendar 2018 の16日目の記事です。
こんにちは、エンジニアのid:tenmihiです。
今日の記事は、vue-routerのナビゲーションガードを利用してページ遷移時のローディング処理を実装する方法のご紹介です。
概要
業務で次のような要件を満たすフォームをvueを用いて実装しました。
- フォームが複数のページで構成されている(いわゆるウィザード、ステップ形式)
- 各ページに遷移するタイミングでフォームの構成に必要なデータをfetchしてきて、結果が返るまではフォームの操作をローディング画面を挟んで制限する
ローディング処理についてcreatedフック内のfetch処理の前後でloadingフラグを操作する方法を考えましたが、毎ページでフラグ操作を記述するのが冗長に感じました。
<template> <div> <form> ... </form> </div> </template> <script> export default { ... created () { // loadingフラグを元に上位のcomponentでローディング画面を表示させる commit('setIsLoading', true) fetch() commit('setIsLoading', false) } } </script>
理想としてはfetchだけをcreatedフックあるいは相当の処理内で行い、loadingフラグは別でよしなに操作されてほしいです。
そこで今回は、vue-routerのナビゲーションガードを利用してページ遷移時のローディング処理をまとめてみました。
ナビゲーションガード
ナビゲーションガードとはvue-routerの機能の1つです。
// 例 async beforeRouteEnter(to, from, next) { await fetchHoge(); next() }
ガードはページの遷移前や後にフックされ、そのタイミングで任意の処理が可能です。
また、同期的な処理も可能でapiの結果によって遷移をキャンセルするようなことも可能となっています。
vue-routerに対するグローバルガードだけでなく、各コンポーネントに対して個別にガード処理を実装することができます。
利用したモジュール
- vue@2.5.21
- vue-router@3.0.2
- vuex@3.0.1
実装
注意
- vue-cli(3.2.1)で作成したプロジェクトで実装しています
- 単一ファイルコンポーネント(.vue)で書くことを想定しています
- vue, vue-router, vuex の利用方法については触れません
App.vue
<template> <div id="app"> <div id="nav"> <router-link to="/">Hoge</router-link> | <router-link to="/piyo">Piyo</router-link> </div> <span v-show="is_loading">loading...</span> <router-view v-show="!is_loading"/> </div> </template> <script> export default { computed: { is_loading () { return this.$store.state.is_loading } } } </script>
Appコンポーネントはrouter-viewを持っていて、パスによって表示する中身を変えます。
ただし、state.is_loadingが立っている場合はrouter-viewではなく"loading..."を表示します。
router.js
import Vue from 'vue' import Router from 'vue-router' import store from './store' import Hoge from './views/Hoge.vue' import Piyo from './views/Piyo.vue' Vue.use(Router) const router = new Router({ routes: [ { path: '/', name: 'hoge', component: Hoge }, { path: '/piyo', name: 'piyo', component: Piyo, } ] }) router.beforeEach((to, from, next) => { store.commit('setIsLoading', true) next() }) router.afterEach(() => { store.commit('setIsLoading', false) }) export default router
パスが/
のときHogeコンポーネントが、/piyo
のときPiyoコンポーネントが表示されるようなルーティングとなっています。
router.beforeEachとrouter.afterEachはグローバルガードと呼ばれ、どのページでも呼ばれます。
ここではページ遷移前(beforeEach)にis_loadingフラグを立てて、ナビゲーションガードが解決されて遷移する直前(afterEach)にis_loadingフラグを下ろしています。
store.js
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const sleep = msec => new Promise(resolve => setTimeout(resolve, msec)) const store = new Vuex.Store({ state: { message: '' }, mutations: { setMessage(state, message) { state.message = message } }, actions: { async fetch({ commit }) { await sleep(1000) store.commit('setMessage', 'piyo') } } }) export default store
messageとis_loadingをstateとして持ち、1秒待って'piyo'をmessageにsetするようなfetch actionが実装されています。
今回はこのfetch actionをページ遷移前に叩くapiに例えて利用します。
views/Hoge.vue
<template> <div> <h1>hoge</h1> </div> </template>
Hogeコンポーネントは単にhogeを表示するだけのシンプルなコンポーネントです。
views/Piyo.vue
<template> <div> <h1>{{ message }}</h1> </div> </template> <script> import store from '../store' export default { data () { return { message: this.$store.state.message } }, async beforeRouteEnter(to, from, next) { await store.dispatch('fetch') next() } } </script>
Piyoコンポーネントはdata初期化時にstate.messageと同値になるmessageをバインドしています。
ナビゲーションガードであるbeforeRouteEnterメソッドを実装しており、ここで先程のfetch actionを同期的にdispatchします。
ここではfetch actionのdispatchが終わった後にnextメソッドを呼んでページを遷移させるようにしています。
動作
/hoge
から/piyo
に遷移してみます。
リンクをクリックしてからloadingが表示されて1秒経った後にpiyoが表示されています。
パスを見てもfetchが終わった後にpiyoページへ遷移していることが分かりますね。
注意点
当然ですが、同期的なデータ取得となりapiを叩いてレスポンスが帰ってくる時間だけページ遷移に時間がかかるようになるので、結果UXとしては悪くなります。
今回のように遷移が遅いと実感できるような処理を挟む場合は、非同期的なデータ取得で実現できないかを考えたほうが良いです。
まとめ
ナビゲーションガードを利用することで、ページ遷移時のローディング処理をまとめることが出来ました。
今回例に上げた以外にも、ページ遷移前に認証済みかどうかをチェックしてリダイレクトさせたりとナビゲーションガードは様々な用途で利用できそうです。
明日は mizuki_r さんです!