arsro.net

[Nuxt.js] アンカーを設定した際のスクロールの挙動について


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. 固定ヘッダーがある場合、ヘッダーの高さ分表示位置がズレてしまう問題

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

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

1,2はうまく回避😆

3はぎりぎり回避…😔

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

scrollBehaviorは、以下のうちの1つのスクロールポジションオブジェクトを返します。

  1. { x: number, y: number }

  2. { selector: string }

  3. { 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.jsscrollBehaviorを上書きして終了。

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);
      });
    });
  }
}