Next.js页面埋点场景下初始加载/刷新时router.events.on未触发的问题
兼容首屏加载与客户端路由的GA埋点实现方案
原有实现的核心问题
- 对路由事件的触发时机认知有偏差:
routeChangeComplete只会在客户端触发路由跳转完成后派发,首屏服务端加载、页面刷新场景下路由早就初始化完成了,根本不会等组件挂载后再发这个事件,所以这两个场景下必然漏报 - 事件解绑的写法有硬伤:原代码里
router.events.on绑定的是一个匿名函数,router.events.off的时候又重新写了一个长得一模一样的匿名函数,JS里两个不同的函数哪怕代码完全一样也不是同一个引用,根本解绑不掉,跑久了会绑定一堆重复监听,跳一次路由报N次数据 - 单独加首屏上报容易重复:如果直接写一个独立的首屏上报effect,不加上报状态的边界标记,很容易和原有路由事件的上报逻辑撞车,导致同一个页面被上报多次
可直接复用的实现逻辑
核心逻辑就是把首屏上报和路由跳转上报的边界划清楚,两类场景都覆盖,还不会重复触发:
- 用
useRef存首屏上报完成的标记,不会因为组件重渲染、闭包问题乱触发逻辑 - 满足Cookie授权条件时,先检查是不是已经报过首屏了,没报过就直接走一次首屏pageview上报,把标记打上去之后再绑路由跳转的监听
- 所有路由变化的回调都写在同一个effect作用域里,保证绑定和解绑用的是同一个函数引用,不会出现解绑失效的问题
- 把Cookie授权状态放到effect的依赖数组里,兼容用户最开始拒绝Cookie、后面手动授权的场景,不会漏报授权后的第一次访问
import { useEffect, useRef } from 'react'; import { useRouter } from 'next/router'; import ReactGA from 'react-ga'; export const setupGAPageTracking = (isCookieConsentGiven: boolean) => { const router = useRouter(); // 用ref存首屏上报状态,避免不必要的重渲染和闭包问题 const hasInitialReported = useRef(false); useEffect(() => { // 没拿到用户Cookie授权就啥也不做,符合合规要求 if (!isCookieConsentGiven) return; // 路由跳转完成后的上报回调,和解绑用的是同一个函数引用 const handleRouteComplete = (targetUrl: string) => { // 直接用事件传的目标URL,比读window.location更准,不会碰到路由更新了但DOM还没同步的情况 ReactGA.pageview(targetUrl); } // 只在第一次满足授权条件时跑一次首屏上报,覆盖首次加载、刷新场景 if (!hasInitialReported.current) { ReactGA.pageview(window.location.pathname); hasInitialReported.current = true; } // 绑定客户端路由跳转的监听 router.events.on('routeChangeComplete', handleRouteComplete); // 组件卸载或者依赖变化时,用同一个引用解绑监听 return () => { router.events.off('routeChangeComplete', handleRouteComplete); } }, [isCookieConsentGiven, router.events]); }
容易踩的坑
- 别把
router.pathname当成上报effect的依赖来触发埋点,这种写法本来就是为了兼容首屏的,但是会和routeChangeComplete的事件触发逻辑重叠,必然造成重复上报 - 解绑事件的时候千万别重新写个匿名函数凑数,必须保证on和off传的是同一个函数引用,不然时间长了会残留一堆无效监听,一次路由跳转会触发十几次上报
- 首屏上报的标记别用普通的useState存,不然状态一变就会触发组件重渲染,甚至把effect再跑一遍,本来是想防重复上报的,结果反而造成重复
内容的提问来源于stack exchange,提问作者Maxime Ghéraille
相关产品推荐
相关产品推荐

