LoginSignup
21
17

More than 5 years have passed since last update.

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

Posted at

概要

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

簡単なページ遷移アニメーションの設定
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の時にこの変数を切り替えておくとアニメーションを変えることができます。

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を挟んでいます。

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

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);
    });
  }
});

デモページ、サンプルコード

デモページとサンプルコードのリンクは以下に貼りましたので、気になる方は参照してみてください。
デモページ
サンプルコード

まとめ

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

21
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
17