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](https://router.vuejs.org/ja/)
Vue-RouterはVue.jsでSPAを作る際につかいます。

### [ナビゲーションガード](https://router.vuejs.org/ja/advanced/navigation-guards.html)
画面にルーティングされる前にパラメータのバリデーションをして、アクセスのガードできます。

### イベントの起きる順番と書き方
主にガードする場所は以下の4箇所です
1. グローバルbeforeEach
2. ルート単位beforeEnter
3. コンポーネント単位beforeRouteEnter
4. コンポーネント単位beforeRouteUpdate

```javascript: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です。
<img width="642" alt="スクリーンショット 2017-12-16 13.57.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/75299/d183d8f3-7126-02ab-f772-0d210cad87f9.png">

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

## 各場所でガードしてみる

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

```javascript
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の定義でロジックを書きたくは無い気がしています。（個人の感想

```javascript
        {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みることになります。

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

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

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

できたかと思いますが、しかし、これはまだガードが完全ではありません。初回アクセス時のみのガードしかできていないのです。
`/#/satohjohn`にアクセスした時ページが表示されていますが、これを`/#/satoh`に変更してみると、通ってしまいます。
これは、[hash event](http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html#event-hashchange)に対して、2と3が動かないためです。

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

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

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

<img width="632" alt="スクリーンショット 2017-12-16 16.02.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/75299/8c34dcb1-6ce8-4dcb-bafc-a195b87c7864.png">

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

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

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

## まとめ

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

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