coverPiccoverPic

Next 的 App Router 下,如何加入页面跳转的进度条?

❓加一个进度条

问题起因是我想给自己博客页面切换时加一个进度条,博客前端技术栈是 React,使用 Next 做了 SSR。在这方面有非常成熟的库 NProgress,只需要调用NProgress.start()NProgress.done()即可开启和结束进度条。我们只需要在页面开始、结束切换时调用即可。

🌐Page Router 模式的做法

在 Page Router 模式下,可以使用router.events的 API 去监听路由切换:

ts
  1. import { useEffect } from 'react'
  2. import { useRouter } from 'next/router'
  3. import NProgress from 'nprogress'
  4. import 'nprogress/nprogress.css'
  5. export function useProgressBar() {
  6. const router = useRouter()
  7. NProgress.configure({ showSpinner: false, speed: 400 })
  8. useEffect(() => {
  9. const startHandler = () => {
  10. NProgress.start()
  11. };
  12. const completeHandler = () => {
  13. NProgress.done()
  14. };
  15. router.events.on('routeChangeStart', startHandler)
  16. router.events.on('routeChangeComplete', completeHandler)
  17. }, [])
  18. }

是不是很方便呢,在 App Router 模式下就难搞了。

🛠App Router 模式的做法

App Router 模式基于 React 的服务端组件对渲染的模式进行了更细致的划分,这看起来就很 Coooooooool,除了某些 API 不太方便之外。在 App Router 模式,没有监听路由跳转发起的 API,只能通过监听路径(usePathnameuseParams)、query 参数(useSearchParams)等方式知道路由已经发生变化了。

参考一下在 Next 看了一下 issue [Next 13] router.events removed?[next/navigation] Next 13: useRouter events? 下面的讨论,监听路由开始切换,可以通过监听Link标签的click事件实现,具体操作是使用MutationObserver监听document,在子树变更时搜索所有Link标签加上监听器。然后通过代理history.pushStatehistory.replaceState来在路由变更后关闭进度条。借鉴 issue 中讨论的做法,我这里自定义了 Hook 函数来实现这一点。

ts
  1. import { useEffect } from 'react'
  2. import NProgress from 'nprogress'
  3. import 'nprogress/nprogress.css'
  4. type PushStateInput = [any, string, string | URL | null | undefined]
  5. const handleAnchorClick = (event: MouseEvent) => {
  6. // 新窗口打开不需要进度条
  7. if (event.ctrlKey || event.metaKey) {
  8. return
  9. }
  10. const targetUrl = (event.currentTarget as HTMLAnchorElement).href
  11. const currentUrl = window.location.href
  12. if (targetUrl !== currentUrl) {
  13. NProgress.start()
  14. }
  15. }
  16. export const useProgressBar = () => {
  17. useEffect(() => {
  18. NProgress.configure({ showSpinner: false, speed: 400 })
  19. const handleMutation: MutationCallback = () => {
  20. const anchorElements: NodeListOf<HTMLAnchorElement> = document.querySelectorAll('a[href]')
  21. anchorElements.forEach((anchor) => anchor.addEventListener('click', handleAnchorClick))
  22. }
  23. const mutationObserver = new MutationObserver(handleMutation)
  24. mutationObserver.observe(document, { childList: true, subtree: true })
  25. window.history.pushState = new Proxy(window.history.pushState, {
  26. apply: (target, thisArg, argArray: PushStateInput) => {
  27. NProgress.done()
  28. return target.apply(thisArg, argArray)
  29. },
  30. })
  31. window.history.replaceState = new Proxy(window.history.replaceState, {
  32. apply: (target, thisArg, argArray: PushStateInput) => {
  33. NProgress.done()
  34. return target.apply(thisArg, argArray)
  35. },
  36. })
  37. }, [])
  38. }

如果是使用router.push切换路由,类似地,我们也触发一次进度条就好了:

ts
  1. import NProgress from 'nprogress'
  2. import { useRouter } from 'next/navigation'
  3. import { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'
  4. type NextNavPushArgsType = [string, NavigateOptions | undefined]
  5. export const useProgressRouter = () => {
  6. const router = useRouter()
  7. router.push = new Proxy(router.push, {
  8. apply: (target, thisArg, argArray: NextNavPushArgsType) => {
  9. NProgress.start()
  10. return target.apply(thisArg, argArray)
  11. },
  12. })
  13. router.replace = new Proxy(router.replace, {
  14. apply: (target, thisArg, argArray: NextNavPushArgsType) => {
  15. NProgress.start()
  16. return target.apply(thisArg, argArray)
  17. },
  18. })
  19. return router
  20. }

📖结语

针对 App Router 模式下没有给出监听路由切换的特定 API 的情况,为了实现进度条的开始和结束。通过MutationObserver,我们通过监听所有Link标签的点击事件来开启进度条。包装history.pushState等 API,实现路由切换结束关闭进度条。另外,对使用router.push等情况,我们通过增强router来使其拥有开启进度条的功能。

0 条评论未登录用户
Ctrl or + Enter 评论
🌸 Run