Next.js App Router 项目,想加个进度条,但是怎么监听页面跳转?

发布于:2024-05-06 ⋅ 阅读:(24) ⋅ 点赞:(0)

❓想要加一个进度条

问题起因是我想给自己博客页面切换时加一个进度条。博客前端技术栈是 React,使用 Next 做了 SSR。在进度条方面,npm 上有非常成熟的库 。我们只需要调用NProgress.start()NProgress.done()即可开启和结束进度条。我们监听到页面开始、结束切换的时候,调用NProgress.start()NProgress.done()即可。但是问题是,Next 13 版本以后推出的 App Router 模式并没有给出监听路由切换的 API。

🌐Pages Router 模式:router.events

先来看看 Pages Router 模式的做法:

可以发现,在 npm 上面有一个 Next 下封装 NProgress 的库:,它是基于 Pages Router 开发的。

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

import { useEffect } from 'react'
import { useRouter } from 'next/router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

export function useProgressBar() {
  const router = useRouter()
  NProgress.configure({ showSpinner: false, speed: 400 })
  useEffect(() => {
    const startHandler = () => {
      NProgress.start()
    };
    const completeHandler = () => {
      NProgress.done()
    };
    router.events.on('routeChangeStart', startHandler)
    router.events.on('routeChangeComplete', completeHandler)
  }, [])
}

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

🛠App Router 模式:手动监听 + 代理

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

为了解决这个问题,这里参考了一下在 Next 的 issue:、 下面的讨论。

监听路由开始切换,可以通过监听Link标签的click事件实现。具体操作是使用MutationObserver监听document,在子树变更时搜索所有<a>标签加上监听器。然后通过代理history.pushStatehistory.replaceState来在路由变更后关闭进度条。

借鉴 issue 中讨论的代码,这里自定义了 Hook 函数useProgressBar来实现这一点。

import { useEffect } from 'react'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

type PushStateInput = [any, string, string | URL | null | undefined]

const handleAnchorClick = (event: MouseEvent) => {
  // 新开页面不处理
  if (event.ctrlKey || event.metaKey) {
    return
  }

  const target = (event.currentTarget as HTMLAnchorElement | null)?.target
  if (target && target !== '_self') {
    return
  }
  
  const targetUrl = (event.currentTarget as HTMLAnchorElement | null)?.href
  // 定位的 anchor 不处理
  if (!targetUrl || targetUrl.includes('#')) {
    return
  }

  const currentUrl = window.location.href
  if (targetUrl !== currentUrl) {
    NProgress.start()
  }
}

export const useProgressBar = () => {
  useEffect(() => {
    NProgress.configure({ showSpinner: false, speed: 400 })
  
    const handleMutation: MutationCallback = () => {
      const anchorElements: NodeListOf<HTMLAnchorElement> = document.querySelectorAll('a[href]')
      anchorElements.forEach((anchor) => anchor.addEventListener('click', handleAnchorClick))
    }
  
    // 监听所有 dom 的改变
    const mutationObserver = new MutationObserver(handleMutation)
    mutationObserver.observe(document, { childList: true, subtree: true })
  
    window.history.pushState = new Proxy(window.history.pushState, {
      apply: (target, thisArg, argArray: PushStateInput) => {
        NProgress.done()
        return target.apply(thisArg, argArray)
      },
    })
    window.history.replaceState = new Proxy(window.history.replaceState, {
      apply: (target, thisArg, argArray: PushStateInput) => {
        NProgress.done()
        return target.apply(thisArg, argArray)
      },
    })
  }, [])
}

useProgressBar处理全局的Link标签,如果是使用router.push切换路由,类似地,我们也在router.push触发一次进度条好了:

import NProgress from 'nprogress'
import { useRouter } from 'next/navigation'
import { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'

type NextNavPushArgsType = [string, NavigateOptions | undefined]

export const useProgressRouter = () => {
  const router = useRouter()
  router.push = new Proxy(router.push, {
    apply: (target, thisArg, argArray: NextNavPushArgsType) => {
      NProgress.start()
      return target.apply(thisArg, argArray)
    },
  })
  router.replace = new Proxy(router.replace, {
    apply: (target, thisArg, argArray: NextNavPushArgsType) => {
      NProgress.start()
      return target.apply(thisArg, argArray)
    },
  })
  return router
}

到这里,进度条就施工完毕了。

📖结语

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

大家的阅读是我发帖的动力。
本文首发于我的博客: