需求背景
在前端开发中,我们经常需要统计页面元素的曝光数据,比如广告展示、商品曝光等。一个完善的曝光统计需要考虑以下场景:
- 元素是否在视口内可见
- 资源是否加载完成
- 页面切换状态时的重新曝光
- 防止重复曝光
核心设计思路
曝光判定机制
new IntersectionObserver((entries) => {
for (const entry of entries) {
exposedRef.current = entry.intersectionRatio >= 0.9;
// ...
}
}, {
threshold: 0.9
});
设计要点:
- 使用 IntersectionObserver API 替代传统的 scroll+getBoundingClientRect 方案
- 设置 90%曝光阈值,确保元素充分展示
- 使用 ref 记录曝光状态,避免重复触发
资源加载控制
const resourceLoaded = useRef(false);
const onResourceLoad = useCallback(() => {
resourceLoaded.current = true;
setTimeout(() => {
if (!document.hidden && exposedRef.current) {
invokeExposeCallback(onExposeRef.current);
}
}, 1);
}, []);
设计要点: 提供资源加载状态控制 使用 setTimeout 确保 DOM 布局稳定 同时判断页面可见性和曝光状态
页面可见性管理
const callbacks: Array<() => void> = [];
const onPageVisible = (cb: () => void) => {
callbacks.push(cb);
};
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
triggerExpose();
}
});
设计要点: 实现简单的发布订阅模式 统一管理页面可见性回调 支持多个曝光实例同时工作 4. 重渲染处理
useEffect(() => {
if (elRef.current === preElRef.current) {
return;
}
if (preElRef.current) {
exposedRef.current = false;
intersectionObserverRef.current!.unobserve(preElRef.current);
}
// ... 重新观察新元素
preElRef.current = elRef.current;
});
设计要点: 比较新旧 ref 避免重复处理 清理旧元素的观察者 重置曝光状态
生命周期管理
useEffect(() => {
onPageVisible(cb);
return () => {
exposedRef.current = false;
intersectionObserverRef.current?.disconnect();
offPageVisible(cb);
};
}, []);
设计要点: 组件卸载时清理所有监听器 重置曝光状态 移除页面可见性回调
性能优化考虑
防抖处理:
setTimeout(() => {
// 延迟处理资源加载后的曝光
}, 1);
缓存优化:
const onExposeRef = useRef(onExpose);
onExposeRef.current = onExpose;
条件判断优化:
if (elRef.current === preElRef.current) {
return;
}
使用示例
function Banner() {
const { ref, onResourceLoad } = useExpose({
needLoadResource: true,
onExpose: () => {
// 上报曝光数据
trackEvent('banner_expose');
}
});
return (
<div ref={ref}>
<img
src="banner.jpg"
onLoad={onResourceLoad}
/>
</div>
);
}
扩展性考虑
- 支持自定义曝光阈值
- 支持多个资源加载完成的场景
- 支持自定义曝光条件判断
总结
这个曝光 Hook 的设计充分考虑了:
- 准确性:通过 IntersectionObserver 保证曝光判定准确
- 性能:避免频繁 DOM 操作和重复计算
- 可靠性:完善的生命周期管理和清理机制
- 易用性:简单的 API 设计,开箱即用
- 扩展性:预留配置项,支持自定义场景 这样的设计使得埋点代码与业务逻辑解耦,提高了代码的可维护性和复用性。