概要
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を挟んでいます。
コードに落とすと以下のようになります。
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かの判定を提供してもらえると嬉しんですけどね。