想给之前上线的工具站做个博客数据统计: blogs。
理所当然用到了document.referer
,最方便的获取上一个页面的方式,结果发现获取到的都是第一次 load 的页面,而不是导航前的那个页面。
为什么会这样?
Next.js 在生产环境中默认采用前端导航(Client-side Navigation
)的方式。这意味着页面加载完之后,在网站内部点击链接时,浏览器并不会执行一次完整的页面刷新。相反,Next.js 会在客户端通过 JavaScript 异步加载新页面的数据和组件,然后更新 DOM。
这种类似 单页应用(SPA) 的行为导致了一个关键问题:
document.referrer
的值在页面首次加载后保持不变。当通过内部链接从blog/zh/1
导航到 blog/zh/post/[slug]
时,由于没有发生完整的页面重载,document.referrer
的值仍然会是最初访问网站的那个值,而不是blog/zh/1
。
那要如何处理这个问题呢,其实也很简单,因为我们是可以拿到导航变化的,我们把上一页存下来再拿出来就好了。
nextjs 官方也提到了这种做法:Router events
简单来说,首先准备个 utils 用来存取 session storage(也可以是其他地方,看需求):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const REFERRER_KEY = "internal_referrer";
export const getReferrer = (): string | null => { if (typeof window === "undefined") { return null; } return sessionStorage.getItem(REFERRER_KEY); };
export const setReferrer = (value: string): void => { if (typeof window !== "undefined") { sessionStorage.setItem(REFERRER_KEY, value); } };
|
然后增加一个client component
,在 path 和 search 变化的时候存进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| "use client";
import { useEffect } from "react"; import { usePathname, useSearchParams } from "next/navigation"; import { setReferrer } from "@/lib/storage";
export function NavigationEvents() { const pathname = usePathname(); const searchParams = useSearchParams();
useEffect(() => { let url = pathname; if (searchParams.toString()) { url = `${pathname}?${searchParams.toString()}`; } console.log("nav change:", url); setReferrer(url); }, [pathname, searchParams]);
return null; }
|
然后把这个组件放到 layout 里去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export default async function RootLayout({ children, params, }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { ...
return ( <html lang={locale} suppressHydrationWarning> <body> ... <Suspense fallback={null}> <NavigationEvents /> </Suspense> ... </body> </html> ); }
|
完成了,后续其他client component
要使用就直接用 storage 获取就行了,但是注意如果他为空再去获取一下document.referer
,主要用在第一次的时候。
比如这个组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export function ViewCounter({ slug, language }: ViewCounterProps) { const searchParams = useSearchParams();
useEffect(() => { if (typeof window !== "undefined") { const internalReferrer = getReferrer(); const externalReferrer = document.referrer;
let referrerToSend = "";
if (internalReferrer) { referrerToSend = internalReferrer; } else if (externalReferrer) { referrerToSend = externalReferrer; }
addBlogViewCount(slug, language, referrerToSend, searchParams.toString()); } }, [slug, language, searchParams]);
return null; }
|
这里可能会有人奇怪了,这里记录的不是这次的 path 吗,怎么就变成上次了,那是因为react
的useEffect
执行是从子组件到父组件的,子组件会拿到更新前(也就是上一个页面)的值。
详细点:
当用户从页面 A 导航到页面 B 时,页面 B 的组件会 mount
(挂载),同时 NavigationEvents
组件会因为 pathname
改变而 re-render
。
页面 B 的 useEffect
和 NavigationEvents
的 useEffect
都会被调度执行。
在页面 B 的 useEffect
执行时,它从 localStorage
读取 referrer
。此时,NavigationEvents
中更新 localStorage
的 useEffect
还未执行。
因此,页面 B 读取到的是上一个页面(页面 A)导航完成时存入的值。
在页面 B 的 useEffect
执行之后,NavigationEvents
的 useEffect
才执行,将当前的 pathname
(页面 B 的路径)存入 localStorage
,为下一次导航做准备。