🐶

nuxt3のloading indicatorに、自作でカスタマイズしたcomponentを利用する

2023/05/19に公開

nuxt2の場合

ページ遷移中の表示をカスタマイズする場合、
以下のような設定で行うことができた。

nuxt.config.js
export default {
  loading: '~/components/atoms/Loading.vue',

nuxt3の場合

loading中の表示をするNuxtLoadingIndicatorコンポーネントがあるので、
これをカスタマイズする
公式参照↓
https://nuxt.com/docs/api/components/nuxt-loading-indicator

カスタマイズなし

app.vue
<template>
  <div>
    <Nav></Nav>
    <main>
        <NuxtLayout>
          <NuxtLoadingIndicator />
          <NuxtPage :key="route.fullPath" />
        </NuxtLayout>
    </main>
  </div>
</template>

カスタマイズする場合、slotでhtmlを編集する。

app.vue
<template>
  <div>
    <Nav></Nav>
    <main>
        <NuxtLayout>
          <NuxtLoadingIndicator>
            <slot><Loading /></slot>
          </NuxtLoadingIndicator>
          <NuxtPage :key="route.fullPath" />
        </NuxtLayout>
    </main>
  </div>
</template>

独自に作りたい場合は、
NuxtLoadingIndicator componentのオリジナルをもとに、
自分で作成する。
ポイントは、nuxtApp.hook()で、ページローディングstart/finish をhookして、
表示/非表示を切り替える部分。ここを抑えれば、あとは好きに実装できる。

オリジナル(nuxt3.5時点)

https://github.com/nuxt/nuxt/blob/25c150136dc6e589b1fdf0c90c8d37010c3409a0/packages/nuxt/src/app/components/nuxt-loading-indicator.ts#L5

nuxt-loading-indicator.ts

import { computed, defineComponent, h, onBeforeUnmount, ref } from 'vue'
import { useNuxtApp } from '#app/nuxt'

export default defineComponent({
  name: 'NuxtLoadingIndicator',
  props: {
    throttle: {
      type: Number,
      default: 200
    },
    duration: {
      type: Number,
      default: 2000
    },
    height: {
      type: Number,
      default: 3
    },
    color: {
      type: [String, Boolean],
      default: 'repeating-linear-gradient(to right,#00dc82 0%,#34cdfe 50%,#0047e1 100%)'
    }
  },
  setup (props, { slots }) {
    const indicator = useLoadingIndicator({
      duration: props.duration,
      throttle: props.throttle
    })

    // Hook to app lifecycle
    // TODO: Use unified loading API
    const nuxtApp = useNuxtApp()
    nuxtApp.hook('page:start', indicator.start)
    nuxtApp.hook('page:finish', indicator.finish)
    nuxtApp.hook('vue:error', indicator.finish)
    onBeforeUnmount(indicator.clear)

    return () => h('div', {
      class: 'nuxt-loading-indicator',
      style: {
        position: 'fixed',
        top: 0,
        right: 0,
        left: 0,
        pointerEvents: 'none',
        width: 'auto',
        height: `${props.height}px`,
        opacity: indicator.isLoading.value ? 1 : 0,
        background: props.color || undefined,
        backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
        transform: `scaleX(${indicator.progress.value}%)`,
        transformOrigin: 'left',
        transition: 'transform 0.1s, height 0.4s, opacity 0.4s',
        zIndex: 999999
      }
    }, slots)
  }
})

function useLoadingIndicator (opts: {
  duration: number,
  throttle: number
}) {
  const progress = ref(0)
  const isLoading = ref(false)
  const step = computed(() => 10000 / opts.duration)

  let _timer: any = null
  let _throttle: any = null

  function start () {
    clear()
    progress.value = 0
    if (opts.throttle && process.client) {
      _throttle = setTimeout(() => {
        isLoading.value = true
        _startTimer()
      }, opts.throttle)
    } else {
      isLoading.value = true
      _startTimer()
    }
  }
  function finish () {
    progress.value = 100
    _hide()
  }

  function clear () {
    clearInterval(_timer)
    clearTimeout(_throttle)
    _timer = null
    _throttle = null
  }

  function _increase (num: number) {
    progress.value = Math.min(100, progress.value + num)
  }

  function _hide () {
    clear()
    if (process.client) {
      setTimeout(() => {
        isLoading.value = false
        setTimeout(() => { progress.value = 0 }, 400)
      }, 500)
    }
  }

  function _startTimer () {
    if (process.client) {
      _timer = setInterval(() => { _increase(step.value) }, 100)
    }
  }

  return {
    progress,
    isLoading,
    start,
    finish,
    clear
  }
}

自作例 (シンプルにloading中か否かのフラグのみを利用して、loading表示をする)

CustomeLoading.vue
<script setup>
// import { computed, defineComponent, onBeforeUnmount, ref } from 'vue'
// import { useNuxtApp } from '#app/nuxt'

const props = defineProps({
  throttle: {
    type: Number,
    default: 200,
  },
  duration: {
    type: Number,
    default: 2000,
  },
});

const progress = ref(0)
const isLoading = ref(false)
const step = computed(() => 10000 / props.duration)

let _timer = null
let _throttle = null

//methods
const start = () => {
  // console.log('indicator start!!!');
  clear();
  // console.log('clear finish... props.throttle:', props.throttle);
  progress.value = 0
  if (props.throttle && process.client) {
    _throttle = setTimeout(() => {
      isLoading.value = true
      _startTimer()
    }, props.throttle)
  } else {
    isLoading.value = true
    _startTimer()
  }
};
const finish = () => {
  progress.value = 100;
  _hide();
};

const clear = () => {
  clearInterval(_timer)
  clearTimeout(_throttle)
  _timer = null
  _throttle = null
};

const _increase = (num) => {
  // console.log('indicator increase...!!!');
  progress.value = Math.min(100, progress.value + num)
};

const _hide = () => {
  // console.log('indicator hide....!!!');
  
  if (process.client) {
    setTimeout(() => {
      isLoading.value = false
      setTimeout(() => {
        progress.value = 0
      }, 400)
    }, 500)
  }
};

const _startTimer = () => {
  console.log('indicator _startTimer....!!!');
  if (process.client) {
    _timer = setInterval(() => {
      _increase(step.value)
    }, 100)
  }
};


  // Hook to app lifecycle
  const nuxtApp = useNuxtApp()
  nuxtApp.hook('page:start', start)
  nuxtApp.hook('page:finish', finish)
  nuxtApp.hook('vue:error', finish)
  
  onBeforeUnmount(clear);
</script>

<template>
  <div>
  <div
      v-if="isLoading"
      class="loading-page element-animation element-animation-ease-in">
    
      <div class="element-animation__inner">
        <div class="loader">ローディングアニメーション画像を、中央に表示</div>
      </div>
    </div>
  </div>
</template>

Discussion