Linuxを日常的に使う実験ブログ

Vue3+vue-routerで動的に生成したアンカーリンクへ遷移する方法

 2024-02-29

 2024-02-29

 プログラミング

今回はVue3+vue-routerを使用した環境で非同期通信で動的に生成したHTMLにアンカーリンク(ページ内リンク)を付与し、そこへジャンプする方法について考えます。ネット上でもいくつか見つかりますが情報が古かったり、自分が使うケースに該当する情報は見当たらなかったのでメモとして残しておきます。

想定するケース

まずは今回検討するケースについて説明します。まず、vue-routerを使用しページ遷移を管理している前提です。次に、最初の描画では外枠のHTMLのみを生成しておいてAjax通信でAPIから内容を取得しHTMLを生成し、その新たに生成されたHTMLにアンカーリンクを貼った場合です。以下にシーケンス図を示します。

sequence-chart

Ajax通信後に初めて目次や見出しのHTMLが生成されるため素直にVueで作るとハッシュ付きのURLでアンカーに飛ぼうと思ってもジャンプしません。また、ハッシュ付きのURLで直にアクセスされた場合に遷移するにもひと工夫が必要です。

動的に描画した後にページ内でアンカーリンクにジャンプする場合

まず、動的に生成されたアンカーリンクでページ内で遷移する方法です。これはcreateRouter関数内でscrollBehaviorオプションを適切に設定すれば可能です。

vue-router 4.xの場合

const router = createRouter({
  routes: {
    ...
  },
  scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth' // 動きをなめらかにする場合
    }

    retrun {x: 0,y: 0} // デフォルト処理
  }

})

注意点としてはvue-router 3.xと4.xでは書き方が変わるので注意が必要です。ネット上ではまだ3.x系の情報が多く出てきますが、4.xではそのコードは動きません。

もし3.x系を利用している場合は以下のコードとなります。

vue-router 3.xの場合

const router = createRouter({
  routes: {
    ...
  },
  scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
      return {
        selector: to.hash,
        behavior: 'smooth' // 動きをなめらかにする場合
    }

    retrun {x: 0,y: 0} // デフォルト処理
  }

})

同一ページ内での遷移に関してはすで画面の再構築が行われた後にユーザーがリンクをクリックするため、Vueのライフサイクルを意識する必要はありません。しかし問題となるのは他のページからのアクセスやハッシュ付きのURLでアクセスされた場合です。

ハッシュ付きURLでダイレクトにアクセスされた場合

ハッシュ付きのURLでダイレクトにアクセスされた場合にアンカーリンクまでスクロールさせるのは上記の方法では対応できません。上記の方法はあくまで再描画で生成されたアンカーリンクにページ内からジャンプする場合です。今回は2つの手段を提示してみます。方針1と方針2はそれぞれ独立ですので、ご注意ください。

方針1:scrollBehaviorの設定を変更する

vue-routerの設定と同じ部分を以下のように修正し、レンダリング処理を待つ設定とすることで直接URLでハッシュを指定された場合にも対応する作戦です。この手段はお手軽なのですが、後述する欠点もあるので注意が必要です。

const router = createRouter({
  routes: {
    ...
  },
  scrollBehavior (to, from , savedPosition) {
    if (to.hash) {
      console.log("url is hash")
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve({el: to.hash, behavior: 'smooth'})
        }, 300)
    }

    return {x: 0, y: 0}
  }

})

この方針の弱点はページが確実に読み込んだかどうかを確認することができないことです。今回の設定例では300msecを指定してますが、もし300msec以内に通信とレンダリングが完了しないとアンカーリンクへはジャンプできません。試しにこの数値を300 -> 0へと変更するとジャンプに失敗します。vue-router 4.xの公式ページにも非同期処理を待つ手法として記載がありコードの修正もお手軽なのですが、今回は採用を見送りました。

方針2:画面更新後にスクロールする処理を追加する

方針2は再レンダリングを行うコンポーネントそのものでレンダリング終了を待ち、URLからハッシュを取得し、スクロール処理をするという正統派な方針です。画面を更新するコンポーネントに以下のようにメソッドを追加します。

Option API

<script lang="ts">
import { defineComponent} from 'vue'
... // 他のインポート省略

export default defineComponent({
  data: return {
  ... // 省略
  },
  methods: {
    sumoFunction(): void {
      ... // 更新処理など
    },
    scrollToAnchor(): void { // アンカー付きURLでアクセスされた場合の処理
      if (this.$route.hash) {
        let el = document.getElementById(this.$route.hash.substring(1));
        if (el) {
          el.scrollIntoView({behavior: 'smooth'});
        }
      }
    },
    ... // 他の処理 省略
  },
  updated() { // 画面処理が終わってから実行するためupdatedサイクル内で処理
    this.scrollToAnchor()
  }
})
</script>

Composition API

<script setup lang="ts">
import { onUpdated } from 'vue'
import { useRoute } from 'vue-router'
... 他のインポートは省略

const route = useRoute()
function scrollToAnchor(): void {
  if (route.hash) {
    let el = document.getElementById(route.hash.substring(1));
    if (el) {
      el.scrollIntoView({behavior: 'smooth'});
    }
  }
}

... // データ更新処理など省略

// 画面更新処理が終わってからDOMにアクセスするためupdatedサイクル内で処理する
onUpdated(() => {
  scrollToAnchor()
})
</script>

まとめ

動的にHTMLを生成し、そのHTMLにアンカーリンクを貼った場合はVueのライフサイクルを意識しないと遷移が上手く行かないという話でした。これはtemplateに直にアンカーリンクをつける場合には起きない問題なので、templateの中身だけを非同期通信で変える場合には関係ない話です。もし今回のケースと同様の問題で行き詰まっている方の役に立てれば幸いです。

参考資料: