7839

雑草魂エンジニアブログ

【Nuxt】nuxt-linkでスムーズにスクロールさせる

発生事象

Nuxt.jsでnuxt-linkを利用して、同じページの特定のセレクターに移動する場合、スムーズではなく、瞬間移動したようになってしまう。

その他、同様の問題で以下のような事象が発生する可能性がある。

  • 遷移した先で、固定ヘッダーがある場合に表示位置がズレる
  • アンカーリンクがある状態でブラウザの「戻る」を実施した際に、トップにいかず、アンカーリンクの位置までスクロールが戻ってしまう

解決策

これを解決するためには、2つの方法がある。

1. vue-scrolltoを導入する
github.com

2. v2.9.0 以降で導入された、scrollBehaviorを上書きして設定を追加する
ja.nuxtjs.org

それぞれについて実装してみたので比較も含めて記載する。

1. vue-scrolltoを導入する

基本的には、README記載どおりに設定することで気軽に使うことができた。

1. vue-scrolltoのインストール

   npm install --save vue-scrollto

2. plugins/vue-scrollto.js作成

import Vue from 'vue'
import VueScrollTo from 'vue-scrollto'

Vue.use(VueScrollTo, {
  offset: -60 // 固定ヘッダーがある場合に、オフセットで調整する
})

3. nuxt.config.jsにpluginを追加

export default {
  plugins: [
    '~plugins/vue-scrollto',
  ]
}

4. component内で使用する
以下では、モーダル内で「お問い合わせ」ボタンを押すことで、同ページのお問い合わせのフォームに遷移するボタンを実装した例を示す。移動する前に、まずはモーダルを閉じる必要があるため、onStartにhideModalのメソッドを設定している。

<nuxt-link
  tag="div"
  class="btn-to-contact"
  to="#"
  v-scroll-to="{
    el: '#contact',  //遷移先のセレクターを設定する
    onStart: hideModal  //遷移前に実施すべきことがあれば、ここに設定する
  }"
>
  Contact
</nuxt-link>

2. scrollBehaviorを上書きして設定を追加する

(余談)上書きと言っているので、デフォルトの設定があり、公式ドキュメントにも以下のリンクが記載されているので確認してみてください。

https://github.com/nuxt/nuxt.js/blob/dev/packages/vue-app/template/router.scrollBehavior.js

1. ~/app/router.scrollBehavior.js を作成する (※)ルートディレクトリに、appフォルダがない場合には作成する必要がある

export default function (to, from, savedPosition) {
  if (savedPosition) {
    // savedPosition is only available for popstate navigations.
    return savedPosition
  }

  if (to.hash) {
    let el = document.querySelector(to.hash)
    if ('scrollBehavior' in document.documentElement.style) {
      const OFFSET = 60
      return window.scrollTo({ top: el.offsetTop - OFFSET, behavior: 'smooth' })
    } else {
      return window.scrollTo(0, el.offsetTop)
    }
  }

  return { x: 0, y: 0 }
}

詳細は以下を参照してみてください。

router.vuejs.org

簡単に処理を説明しておく。 to, fromでは、ルートオブジェクトを受け取ることができる。そのため、 if (to.hash) これでアンカーリンク(ページ内リンク)があるかどうかを判別することができる。

もしアンカーがない場合は、return { x: 0, y: 0 } ページのトップがスクロール位置となる。

アンカーリンクがある場合は、スクロール位置を求めるために、まずアンカーリンクのある要素を取得する。次に、ブラウザの対応を確認するために、scrollBehaviorのstyleがあるかを確認する。あれば、window.scrollTo({ top: el.offsetTop - OFFSET, behavior: 'smooth' }) でwindowをスムーズにスクロールさせることができるが、もしない場合は、window.scrollTo(0, el.offsetTop)でX方向はそのままで、Y方向だけスクロールさせる。

3. component内で使用する
上記と同じ設定で書き換えると、以下のようになる。

<nuxt-link
  tag="div"
  class="btn-to-contact"
  to="#contact"
  @click.native="hideModal"
>
  Contact
</nuxt-link>

(注意)nuxt-linkの中で、@click のみを記載しても動作しないので、 @click.nativeとする必要がある。

比較

vue-scrolltoを使う場合は、to="#"としており、URLに実際のアンカーリンクが記載されず、"http://localhost:3000/#"のままである。しかしながら、nuxt-linkをそのまま使った場合には、"http://localhost:3000/#contact"となる。そのため、二回連続で同じアンカーリンクをクリックしてもscrollBehavior.jsが動作しないという問題がある。

私は今回、SPのナビゲーションバーで本リンクを使用しており、連続で二回同じリンクを押すと、2回目以降URIが変わるまで、スクロールが動作しなくなるため、事象が発生したため、 vue-scrollto を用いることとした。

動作させたい条件に応じて、それぞれ使い分けていきたいと思えた。 モジュールを上書きして使う方法は挙動のテスト含め、しっかりとやっていく必要があるが、別のモジュールを用いずに、Nuxtのみでできるのはメリットがあるように思えた。