router.back で他のサイトに行ってしまうのを止めたい

Nuxt.js
公開日:

自サイト上に配置した戻るボタンを押した時に、一つ前の履歴が別サイトだったら自サイトのフロントページに遷移させたい。

この記事では Vue Router の history モードのみ扱います。

試したこと

Vue Router のグローバルビフォーガードを使う

router.beforeEach() を使うと、router のページ切替時に何か処理を行なうことができます。しかしこれは他サイトに遷移する場合には機能せず、今回の目的には適していません。

何か他の方法で、他サイトに遷移してしまう瞬間を判断する必要があります。

history.length を使う

window.history では、ブラウザの戻る・進むの履歴(以下セッション履歴)の情報を得たり、操作したりすることができます。

history.length には、現在のセッション履歴のアイテム数が記録されているので、アクセスされたときの length を保持しておけば、戻るによって自サイトを離れてしまう瞬間を捉えられるのではないかと考えました。

ところが、そういったコードを書いて動きを見てみると、これは無理であることが分かります。

戻る・進むでは history.length は変化しない

ユーザーが history.length が 2 の状態でページ A に来て、ページ B → C と遷移したあとに 2 回戻ったとします。ページ C に来た時点で history.length は 4 になっていますが、2 回戻ったあとも history.length は 4 のままです。

それもそのはずで、history.length は、飽く迄セッション履歴のアイテム数を指しています。1 つ戻ると、戻る側の履歴が 1 つ減って、進む側の履歴が 1 つ増えます。逆も然りで、何度戻ったり進んだりしようが、履歴の数自体は一定です。これが変化するのは、リンクを踏んだり、スクリプトによってページ遷移したりしたときだけです。

history.state.key を使う

セッション履歴のアイテムには、history.pushState()history.replaceState() を使うことで、state と呼ばれるデータを保存することができます。

Vue Router は、この state に key というデータを保存しています。

そこで、アクセスされたときの key を保存しておけば、他サイトに遷移する瞬間を判断できるのではないかと考えました。ところが、これも割と上手くいくものの対応できないケースがあります。

更新すると key が変わってしまう

ページ A に来て、更新してページ上の戻るボタンを押したとします(そんなことされるかはさておき)。

最初のページ A に来た時点で key を sessionStorage に保存します。更新された時に sessionStorage から key を取り出します。

ところが、更新した時に、Vue Router は現在の履歴の state に key があるかどうかに拘らず、key を上書きしてしまいます。

そのため、保存しておいた key と一致せず、戻るボタンを押すと他サイトに遷移してしまいます。

最終的に実装した仕組み

自分で history.statecanGoBack というキーで真偽値のデータを入れるようにします。

SPA 起動時に現在の state を見て、state 自体または state.canGoBack が設定されていなければ、false に設定します。

ページ遷移時には、state.canGoBack が設定されていなければ、true に設定します。

こうすることで、history.state.canGoBack を見れば、現在のルートから一つ戻っても良いのかが分かります。

Nuxt.js のプラグインにするとこんな感じです。

// plugins/replaceState.client.js
export default ({ app }) => {
  const history = window.history
  const initState = history.state

  switch (initState && initState.canGoBack) {
    case null:
    case undefined:
      // console.log('初回アクセス')
      const stateCopy = initState === null ? {} : Object.assign({}, initState)
      stateCopy.canGoBack = false
      history.replaceState(stateCopy, '')
    //   break
    // default:
    //   console.log('戻る・進むまたは更新によるアクセス')
  }

  app.router.afterEach(() => {
    const state = history.state

    if (state.canGoBack === undefined) {
      // console.log('<nuxt-link>, $router.push() 等による遷移')
      const stateCopy = Object.assign({}, state)
      stateCopy.canGoBack = true
      history.replaceState(stateCopy, '')
    }
    // else {
    //   console.log('$router.go(), 戻る・進む等による遷移')
    // }
  })
}

プラグインはアプリの起動よりも前に実行されるため、Vue Router による history.state.key 設定もまだ行なわれていません。

一度も history.pushState() / .replaceState() によってデータを格納していない場合、history.statenull なので、短絡評価などによる null チェックを行なわねばなりません。

一方、Vue Router のグローバルな After フック(app.router.afterEach())では、Vue Router の key 設定が済んだ状態なので、history.statenull になることはありません。

そのため、state.canGoBackundefined であるかどうかだけ確認します。

nuxt.config.js でプラグインを登録します。.client.js で終わるファイルは自動的にクライアントサイドのみで実行されます。

// nuxt.config.js
export default {
  plugins: [
    '~/plugins/replaceState.client',
  ],
}

戻るボタンが設置されているテンプレートでは、window.history.state.canGoBack を確認して、遷移先を変更します。

$router.back() メソッドもありますが、内部で .go(-1) してるだけなので使う必要はありません。

<template>
  <button type="button" @click="goBack">1つ前のページに戻る</button>
</template>

<script>
export default {
  methods: {
    goBack() {
      window.history.state.canGoBack
        ? this.$router.go(-1)
        : this.$router.push({ name: 'index' })
    },
  },
}