[Nuxt.js] アンカーを設定した際のスクロールの挙動について
Date
Sep 13, 2020
Nuxt.jsでページ遷移する際に、アンカーを設定するとscrollBehavior が働いて指定の位置までスクロール位置を移動させてくれます。
デフォルトだと👇の設定になってるようです。
const scrollBehavior = function (to, from, savedPosition) {
  let position = false

  if (to.matched.length < 2) {
    position = { x: 0, y: 0 }
  } else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
    position = { x: 0, y: 0 }
  }

  if (savedPosition) {
    position = savedPosition
  }

  return new Promise(resolve => {
    window.$nuxt.$once('triggerScroll', () => {
      if (to.hash && document.querySelector(to.hash)) {
        position = { selector: to.hash }
      }
      resolve(position)
    })
  })
}
ただデフォルトのままで、アンカーリンクでページ遷移しようとすると3点問題がありました。
  1. 固定ヘッダーがある場合、ヘッダーの高さ分表示位置がズレてしまう問題
  1. ブラウザバックすると、アンカーの位置までスクロールが戻ってしまう問題
  1. ブラウザバックすると、triggerScrollの発火が早すぎて、ページ遷移前にスクロールが発生してしまう問題
 
1,2はうまく回避😆
3はぎりぎり回避...😔

1. 固定ヘッダーがある場合、ヘッダーの高さ分表示位置がズレてしまう問題

scrollBehaviorは、以下のうちの1つのスクロールポジションオブジェクトを返します。
  1. { x: number, y: number }
  1. { selector: string }
  1. { selector: string, offset? : { x: number, y: number }}
とのことです。
 
アンカーの位置を取得しているのはここ👇
if (to.hash && document.querySelector(to.hash)) {
    position = { selector: to.hash }
}
3番目の形式を使って、offsetを設定して回避できます。
if (to.hash && document.querySelector(to.hash)) {
    position = { selector: to.hash, offset: { x: 0, y: (ヘッダーの高さ) } }
}

2.ブラウザバックすると、アンカーの位置までスクロールが戻ってしまう問題

ブラウザバックした際に、scrollBehaviorの第3引数「savedPosition」は、前のページのスクロールした位置をオブジェクトで返します。
前ページのスクロール位置を設定しているのここ👇
if (savedPosition) {
    position = savedPosition
}
savedPositionをセットしたどこまでいいのだが、ブラウザバックした時のURLには、アンカーが設定されているため、先程のアンカーの位置の取得する箇所で positionが上書きされてしまいます...😱
そのため、ブラウザバックしても前回のスクロール位置にスクロールせずに、アンカーの位置にスクロールが戻ってしまっています。
アンカーの位置を取得する条件文を変えて回避しました。
if (to.hash && document.querySelector(to.hash)) {
    ...
}
↓
if (to.hash && document.querySelector(to.hash) && !savedPosition) {
    ...
}

3. ブラウザバックすると、triggerScrollの発火が早すぎて、ページ遷移前にスクロールが発生してしまう問題

「絶対同じようにハマってるやついるだろ😡」と思ってたら、それっぽいIssuesを見つけました!
💡
I figured out that the triggerScroll seems to be fired too early, the page/component isn't loaded yet. I solved it for now with a timeout but not sure if thats a good way.
 
タイムアウトを設定して、発火を遅らせることで回避しました。
無理やり感ある...。
window.$nuxt.$once('triggerScroll', () => {
    .
    .
    .
    resolve(position)
})
↓
window.$nuxt.$once('triggerScroll', () => {
    .
    .
    .
    setTimeout(() => { resolve(position) }, 200);
})
他のいい方法あったら共有して欲しいです...
 

最終的に

nuxt.config.jsでscrollBehaviorを上書きして終了。
router: {
  scrollBehavior(to, from, savedPosition) {
    let position = false

    if (to.matched.length < 2) {
      position = { x: 0, y: 0 }
    } else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
      position = { x: 0, y: 0 }
    }

    if (savedPosition) {
      position = savedPosition
    }

    return new Promise(resolve => {
      window.$nuxt.$once('triggerScroll', () => {
        if (to.hash && document.querySelector(to.hash) && !savedPosition) {
          position = { selector: to.hash, offset: { x: 0, y: (ヘッダーの高さ) } }
        }
        setTimeout(() => { resolve(position) }, 200);
      });
    });
  }
}