在 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 兼容 app 和 pages 两种路由模式,现在 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.pushState 和 history.replaceState 方法,在 pushState 和 replaceState 方法中,调用 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,在 useRouter 的 push 和 replace 方法中,调用 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 的页面中,就可以使用二次封装的 useRouter 的 push 和 replace 方法来跳转页面,并且会显示进度条。