在 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
方法来跳转页面,并且会显示进度条。