vue-routerでbackwardかforwardかを判定する

# 概要
vue-routerではrouter-viewにtransitionでラップすることで簡単にアニメーションは簡単にできますが、履歴をバックしたのかフォワードしたかの判定が難しかったので、それに関する記事です。

```js:簡単なページ遷移アニメーションの設定
new Vue({
  el: '#app',
  router,
  template: `
    <div class="page">
      <!-- .fade-enter-activeとかにCSS Transitionを設定すればアニメーションできる -->
      <transition name="fade">
        <router-view class="view" />
      </transition>
    </div>
  `
});
```

# アニメーションの切り替え方法
transitionコンポーネントのnameに$data変数でバインドしておき、beforeEachの時にこの変数を切り替えておくとアニメーションを変えることができます。

```js
new Vue({
  el: '#app',
  router,
  template: `
    <div class="page">
      <!-- $data.transitionNameに入っている名前でクラスが付与される -->
      <transition :name="$data.transitionName">
        <router-view class="view" />
      </transition>
    </div>
  `,
  data() {
    return {
      transitionName: 'forward'
    };
  },
  created() {
    // beforeEachでアニメーションする名前を決める
    this.$router.beforeEach((to, from, next) => {
      // forwardかbackwardか判定して、どっちかを入れる
      this.$data.transitionName = ( `forwardかbackwardかの判定` ) ? 'backward' : 'forward';
      next();
    }
});
```

ここまでくれば後はforwardかbackwardかの判定ですが、この判定がなかなか大変でした。

# forwardかbackwardかの判定
## やり方
よくブラウザバックかの判定では`window.addEventListener('popstate', () => { ... });`で調べる記事がありますが、これはブラウザバックだけじゃなくて、フォワードする時もこのイベントが発生してしまいます。popstateの中身はbackかforwardかの判定はできなくて、ほぼ詰んでいます（なんでこんな仕様なんでしょうか・・・）。
どうしようもないので、今回は`history.replaceState`でhistoryにページ番号を保存させて、その番号と比較してbackかforwardかを判定します。

## 具体的な方法
* data関数で現在のhistory.stateを取得し、pageNumがない場合は0で初期化させます。
* beforeEachでvueの変数とhistory.stateのpageNumを比較して、forwardかbackwardを判定します。この時、新しくページを作る場合はまだpushStateされていないため、stateは遷移前の状態なので実際は同じページ番号になっているので注意が必要です。
* afterEachではhistory.stateにpageNumがない場合は新しくページを作ったので、ページ番号を一つ増やしてセットします。pageNumがある場合は戻るか進むかをした動作なので、vueのdataを同期だけさせます。afterEach実行時ではまだpushStateはされていない気がしたので、setTimeoutを挟んでいます。

コードに落とすと以下のようになります。

```js:backwardかforwardかの判定
new Vue({
  el: '#app',
  router,
  template: `
    <div class="page">
      <transition :name="$data.transitionName">
        <router-view class="view" />
      </transition>
    </div>
  `,
  data() {
    const pageNum = window.history.state ? window.history.pageNum || 0 : 0;
    // ページ番号が今のhistoryにない場合は0で初期化する（実際は0でも上書きしているが動作上問題はない）
    if (!pageNum) {
      window.history.replaceState({
        ...window.history.state,
        pageNum
      }, '');
    }
    return {
      pageNum,
      transitionName: 'forward'
    }
  },
  created() {
    // ルーティング前の設定
    this.$router.beforeEach((to, from, next) => {
      // historyにあるページ番号の大小でバックか判定する
      // pushStateの場合はまだ更新されていないので注意
      const { pageNum } = window.history.state;
      this.$data.transitionName = (pageNum < this.$data.pageNum) ? 'backward' : 'forward';
      console.log(`${this.$data.pageNum} -> ${pageNum}:`, this.$data.transitionName);
      next();
    });
    // ルーティング後の設定（pushStateはまだされていない？）
    this.$router.afterEach((to, from) => {
      // ワンクッション挟む（historyの更新が終わっていないため）
      window.setTimeout(() => {
        // 現在のページ番号を取得する
        const { pageNum } = window.history.state;
        // ページ番号がある場合は更新して終了
        if (Number.isInteger(pageNum)) {
          this.$data.pageNum = pageNum;
          return;
        }

        // ページ番号がない場合は$dataのpageNumをインクリメントしてhistoryにセットする
        this.$data.pageNum += 1;
        window.history.replaceState({
          ...window.history.state,
          pageNum: this.$data.pageNum
        }, '');
      }, 0);
    });
  }
});
```

# デモページ、サンプルコード
デモページとサンプルコードのリンクは以下に貼りましたので、気になる方は参照してみてください。
[デモページ](https://takanorionuma.github.io/vue-router-slide-animation/)
[サンプルコード](https://github.com/TakanoriOnuma/vue-router-slide-animation)

# まとめ
一応なんとか動きはしました。ただ、pushStateのタイミングが合わなくてsetTimeoutを挟んだり、同じページ番号の時は新しいページが作られる前だからforwardだろうって感じで割と無茶しています。beforeEachでnextを実行しなくて遷移をブロックした時はどうなるのとか、まだまだ懸念がある気がして、判定はなかなか難しいものだなと思いました。できればrouter側でbackかforwardかの判定を提供してもらえると嬉しんですけどね。