BLOG

ブログ

Safari環境のNuxtでpopStateが変な動きをする話

先日ブルーベリーガム味という珍しいアイスに出会いました、カリーです。

Nuxt.jsでpopstateを使っている案件を対応したときに、やや挙動が変な感じになってハマったので、その内容を備忘録として残しておこうと思います。

動作環境

  • Nuxt2環境(SPA)
  • Safariバージョン16.2 (18614.3.7.1.5)

どんなことが起きたか

popstateイベントにてブラウザバック直前に何かしらの処理を入れているNuxt.jsアプリ、

例として、要素編集の途中でブラウザバックしたとき「保存されません、よろしいですか?」のポップアップを出すアプリがあるとします。(編集中か判定などの処理は割愛してます)

<template>
  <div>
    <div>
      <div>
        <span>ユーザー名入力</span>
        <input :value="userName" @input="userName = $event.target.value" />
      </div>
      {{ userName }}
    </div>
  </div>
</template>

<script lang="ts">
import Vue from "vue";

export default Vue.extend({
  name: "LinkPage",
  data() {
    return {
      userName: "",
    };
  },
  mounted() {
    window.addEventListener("popstate", this.popEvent);
  },
  destroyed() {
    // Vueコンポーネント破棄時にpopstateイベントも削除
    window.removeEventListener("popstate", this.popEvent);
  },
  methods: {
    async popEvent(event: PopStateEvent): Promise<void> {
      alert("保存されません。よろしいですか?");
    },
  },
});
</script>

Safari環境だと、Nuxtアプリから外部サイトに遷移した後、ブラウザバックでNuxtアプリに戻ってきたタイミングでpopstateイベントが発火してしまいます。

以下の図でいう、③の直後に起こります。

本来ブラウザバック直前に発火するpopstateが、ブラウザバック後のページ内にて発火しています。ナンデ!?

この現象の原因

調べてみたところ、この事象には複数の原因がありました。

①外部遷移時、Nuxtのdestroyedが発火しない

このコードではVueインスタンスが破棄される、destroyed発火のタイミングでpopstateイベントを削除する処理を入れています。

しかし、外部遷移時にはVueインスタンスが破棄されず、destroyedが発火しません。その為、明示的なpopstateイベントの削除を行えていませんでした。

②Safariのbfchach

Safari独自のキャッシュの仕組みであるbfcache(バック/フォワードキャッシュ)という機能があり、ブラウザの戻る or 進むを行ったとき、前のページの情報をキャッシュから呼び出します。

これにより外部遷移後ブラウザバックした際に、popstateイベントが残った状態のNuxtアプリへ戻ることになります。

③Safariではページ読み込み時にもpopstateイベントが発火する

Safari(と古いChromeバージョン)ではページ読み込み時にもpopstateイベントが発火するようです。

https://developer.mozilla.org/ja/docs/Web/API/Window/popstate_event

そのため、ブラウザバック時にbfcacheからpopstateイベントの残ったページが読み込まれ、読み込み時にpopstateイベントが発火していたようです。

対処法

popstateイベントの代わりに、VueRouterのbeforeRouteLeaveを使用します。

beforeRouteLeaveは、ブラウザバックやVueRouterで前のページに戻る際に発火するナビゲーションガードです。beforedestroyより先に発火するので、ブラウザバック直前の確認処理などに利用できます。

<script lang="ts">
import Vue from "vue";

export default Vue.extend({
  name: "LinkPage",
  data() {
    return {
      userName: "",
    };
  },
  // popstateでの処理を無くし、以下のbeforeRouteLeaveを追加
  beforeRouteLeave(to, from, next) {
    const answer = window.confirm("保存されません。よろしいですか?");
    if (answer) {
      next();
    } else {
      next(false);
    }
  },
});
</script>

上記のコードでは戻る時にダイアログでの確認を行い、OKなら処理続行、NGなら戻る処理を中断させます。

これでSafariやChromeなどの環境に左右されず、ブラウザバック時にのみ正常に動作するようになりました。

あとがき

複数の原因が重なっていて、原因解明まで非常に沼にハマってました。

結果的にはbeforeRouteLeaveを使っていれば早かったのですが、諸々の勉強になってよかったです。

popstateが軽くトラウマになった

RELATED ARTICLE