Nextjs 添加顶部加载进度条及其原理

2 阅读4分钟

在 Nextjs 中进行路由跳转的时候,会加载跳转页的数据,如果网络状态不是特别好的话,给用户的感觉就像是点击了跳转但是没有任何反应,因为还在加载下一页的数据。

这时候如果有一个加载条,提示用户的话,体验会好很多。

在 Nextjs 中我一般使用 nextjs-toploader 来快速加入进度条的功能。

使用 nextjs-toploader

安装

npm install nextjs-toploader
# or
yarn add nextjs-toploader
# or
pnpm add nextjs-toploader

使用

首先引入 nextjs-toploader 的组件

import NextTopLoader from 'nextjs-toploader'

nextjs-toploader 兼容 apppages 两种路由模式,现在 Nextjs 默认使用 app 路由模式,所以这里只说明 app 路由模式的使用。

app 路由模式中,nextjs-toploader 的组件需要放在 Layout 组件中,这样在每次路由跳转的时候,进度条都会显示。

import NextTopLoader from 'nextjs-toploader'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <NextTopLoader />
        {children}
      </body>
    </html>
  );
}

在一些使用 useRouter 的页面中,可以使用 nextjs-toploader 封装的 useRouter 来显示进度条。

import { useRouter } from 'nextjs-toploader/app'

const router = useRouter()
router.push('/some-page')

配置

nextjs-toploader 提供了一些配置项,可以传入 props 进行配置。

  • color: 修改 TopLoader 的默认颜色
  • initialPosition: 修改 TopLoader 的初始位置百分比,例如:0.08 = 8%
  • crawlSpeed: 增量延迟速度,单位为毫秒
  • speed: TopLoader 的动画速度,单位为毫秒
  • easing: 使用缓动函数的动画设置(CSS 缓动字符串)
  • height: TopLoader 的高度,单位为像素
  • crawl: TopLoader 的自动增量行为
  • showSpinner: 是否显示加载动画
  • shadow: TopLoader 的阴影(设置为 false 可禁用)
  • template: 为 TopLoader 包含自定义 HTML 属性
  • zIndex: 定义 TopLoader 的 z-index 值
  • showAtBottom: 在底部显示 TopLoader(增加 TopLoader 的高度以确保在移动设备上可见)
  • showForHashAnchor: 是否为 hash 锚点显示 TopLoader
<NextTopLoader color="#000" crawlSpeed={200} initialPosition={0.08} showSpinner={false} />

原理

nextjs-toploader 使用了 nprogress 库来渲染进度条。

显示进度条的时机为点击 a 标签进行页面跳转,或者使用 useRouter 的方法进行跳转。

所以我们需要监听全局的点击事件,首先找到点击事件最近的 a 标签,如果找不到 a 标签,则不显示进度条。

document.addEventListener('click', handleClick)

找到 a 标签后,会判断是否需要显示进度条

  • 如果 a 标签的 href 属性为空,则不显示进度条
  • 如果 a 标签的 target 属性为 _blank,则不显示进度条
  • 如果 a 标签的 href 属性为特殊的格式,如tel:, mailto:, sms:, blob:, download:,则不显示进度条
  • 如果 a 标签的 href 前后跳转的链接不是同一个页面,即不是同一个 hostname,则不显示进度条
  • 如果用户同时按住了 shift 键,则不显示进度条
  • 根据配置项 showForHashAnchor 的值,决定是为锚点链接显示进度条

除以上情况,使用 nprogress 库的 start 来开启进度条

import * as NProgress from 'nprogress';

NProgress.start()

之后就是路由完成的时候,使用 nprogress 库的 done 方法来结束进度条

这时就要覆写 history.pushStatehistory.replaceState 方法,在 pushStatereplaceState 方法中,调用 nprogress 库的 done 方法来结束进度条

((history: History): void => {
	const pushState = history.pushState;
	history.pushState = (...args) => {
		NProgress.done()
		return pushState.apply(history, args)
	}
})((window as Window).history)

((history: History): void => {
	const replaceState = history.replaceState
	history.replaceState = (...args) => {
		NProgress.done()
		removeNProgressClass()
		return replaceState.apply(history, args)
	}
})((window as Window).history)

之后监听 popstate 事件和 pagehide 事件,这两种事件主要处理浏览器的前进后退,以及页面卸载的情况。

window.addEventListener('popstate', () => {
	NProgress.done()
})

window.addEventListener('pagehide', () => {
	NProgress.done()
})

如果是使用 useRouter 的方法进行跳转,则需要封装一个 useRouter 的 hook,在 useRouterpushreplace 方法中,调用 nprogress 库的 start 方法来开启进度条

'use client'

import { AppRouterInstance, NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useRouter as useNextRouter, usePathname } from 'next/navigation'
import { useCallback, useEffect } from 'react'
import * as NProgress from 'nprogress'

export const useRouter = (): AppRouterInstance => {
  const router = useNextRouter()
  const pathname = usePathname()

	// 监听 pathname 的变化,当 pathname 发生变化时,调用 `nprogress` 库的 `done` 方法来结束进度条
  useEffect(() => {
    NProgress.done()
  }, [pathname])

	// 覆写 replace 方法
  const replace = useCallback(
    (href: string, options?: NavigateOptions) => {
      href !== pathname && NProgress.start()
      router.replace(href, options)
    },
    [router, pathname]
  )

	// 覆写 push 方法
  const push = useCallback(
    (href: string, options?: NavigateOptions) => {
      href !== pathname && NProgress.start()
      router.push(href, options)
    },
    [router, pathname]
  )

  return {
    ...router,
    replace,
    push,
  }
}

这样在使用到 useRouter 的页面中,就可以使用二次封装的 useRouterpushreplace 方法来跳转页面,并且会显示进度条。