JavaScript
vue.js
vue-router
Vue.js #1Day 17

Vue-Routerのナビゲーションガードを使ってみる

概要

Vue.js #1 Advent Calendar 17日目です。
こんにちは、@SatohJohnです。
Vue.js人気ですね。導入して良かったです。(結果論
今回は実際に実務で使った際に覚えたVue-Routerのナビゲーションガード(before)について書きます。
つらつら書いてもわかりにくかったので、今回「許可のないidでは対象のページにアクセスさせず、failページに飛ばす。」という課題を考えました。
idが直打ちなのかよとかのツッコミは入れないでください。

前提

バージョン
Vue 2.5.11
Vue-Router 3.0.1

Vue-Routerのモードはhashを想定しています。

Vue-router

Vue-RouterはVue.jsでSPAを作る際につかいます。

ナビゲーションガード

画面にルーティングされる前にパラメータのバリデーションをして、アクセスのガードできます。

イベントの起きる順番と書き方

主にガードする場所は以下の4箇所です
1. グローバルbeforeEach
2. ルート単位beforeEnter
3. コンポーネント単位beforeRouteEnter
4. コンポーネント単位beforeRouteUpdate

vue-router-test.js
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

const TestComponent = { // アクセスさせない画面を持つコンポーネント
    render: function(h) {
        return h('div', `hello: ${this.id}`)
    },
    beforeRouteEnter (to, from, next) { // [3]
        console.log('component: beforeRouteEnter');
        next();
    },
    beforeRouteUpdate (to, from, next) { // [4]
        console.log('component: beforeRouteUpdate');
        next();
    },
    props: ['id']
};
const FailComponent = {
    render: h => h('div', `you cannot access`)
};

const router = new Router({
    routes: [
        {path: '/', redirect: '/test'},
        {path: '/fail', component: FailComponent},
        {path: '/:id', component: TestComponent, props: true,
        beforeEnter: (to, from, next) => { // [2]
            console.log('router: beforeEnter');
            next();
        }},
    ]
});
router.beforeEach((to, from, next) => { // [1]
    console.log('global: beforeEach');
    next();
});

new Vue({
    router,
    render: h => h('router-view'),
}).$mount((() => {
    const list = document.getElementsByTagName('div');
    if (list.length < 1) {
        const appArea = document.createElement('div');
        document.body.appendChild(appArea);
        return appArea
    }
    return list[0];
})());

初回のページ読み込み時に実行される順番としては1→2→3です。
スクリーンショット 2017-12-16 13.57.12.png

現状何もガードを書けていないので/#/satohでも/#/johnでもTestComponentの画面にたどり着けます。
それではガードを書いてみます。

各場所でガードしてみる

1. グローバルでガードする

nextを呼ぶとtoのコンポーネントに遷移します。
しかし、ここで、idが定義されていないことを考慮しなきゃいけないのは厳しい。。。(この例だと見なければ無限ループになる。
他にコンポーネントが増えたときに。。。とか考えたくないですね。

router.beforeEach((to, from, next) => { // [1]
    console.log('global: beforeEach');
    if (to.params.id === undefined || to.params.id === 'satohjohn') {
        next();
    } else {
        next({path: '/fail'});
    }
});

2. ルート単位でガードする

書き方は1.と変わりません。ただ、ここに来るときのtoはTestComponentに対するrouteオブジェクトになるためidが無いときというのを気にしなくても良いかもしれません。
ただ、propsで指定しているのにparamsを見ているのもどうなんだろうという気がします。
また、routerの定義でロジックを書きたくは無い気がしています。(個人の感想

        {path: '/:id', component: TestComponent, props: true,
        beforeEnter: (to, from, next) => { // [2]
            console.log('router: beforeEnter');
            if (to.params.id === 'satohjohn') {
                next();
            } else {
                next({path: '/fail'});
            }
        }},

3.component単位でガードする

書き方はやはり1と変わりませんが、2と同じようにcomponentが特定されているのでidだけをみてよいと思います。2と同じようにparamsみることになります。

    beforeRouteEnter (to, from, next) { // [3]
        console.log('component: beforeRouteEnter');
        if (to.params.id === 'satohjohn') {
            next();
        } else {
            next({path: '/fail'});
        }
    },

しかし、propを参照できるように変更します。

    beforeRouteEnter (to, from, next) { // [3]
        console.log('component: beforeRouteEnter');
        next(vm => {
            if (vm.id !== 'satohjohn') {
                next({path: '/fail'});
            }
        });
    },

できたかと思いますが、しかし、これはまだガードが完全ではありません。初回アクセス時のみのガードしかできていないのです。
/#/satohjohnにアクセスした時ページが表示されていますが、これを/#/satohに変更してみると、通ってしまいます。
これは、hash eventに対して、2と3が動かないためです。

4.hash changeの際にガードする。

この問題にはコンポーネントに対するbeforeRouteUpdateを利用します。
beforeRouteUpdateはコンポーネントが再利用される際に呼ばれます。逆に言えば初回には呼ばれません。
シグネチャは今までのものと同じではありますが、thisパラメータを利用し、コンポーネントの値を取得することができます。しかし、このタイミングでは、遷移する前のidしか取れないので、to.paramsから取ります。

    beforeRouteUpdate (to, from, next) {
        console.log('component: beforeRouteUpdate');
        if (to.params.id === 'satohjohn') {
            next();
        } else {
            next({path: '/fail'});
        }
    },

これによってbeforeRouteUpdateで弾かれるようになります。

スクリーンショット 2017-12-16 16.02.12.png

これでsatohjohnさんしかTestComponentにアクセスすることができなくなりました!

その他4の解決方法

$routeオブジェクトをwatchするという手があります。
こちらの場合はnextがパラメータに渡ってこないため、$router.pushする必要があります。
ただ、this.idが更新されているのでto.paramsを見なくても良いという利点があります。

    watch: {
        $route(to, from) {
            if (this.id !== 'satohjohn') {
                this.$router.push('/fail');
            }
        }
    },

まとめ

Vue-Routerのガードを使ってみました。
beforeRouteUpdateあたりは知らずにいるとハマるポイントだと思います。

今回関係はないんですが、せっかくなのでparcelで作ってみました。
webpackより導入も簡単で、検証とかする際にとてもいいですね!