如何基于参数计算滚动文本显示窗口内的可见文本范围
功能实现方案
这个需求可以通过浏览器原生API实现,不需要依赖第三方库,核心逻辑和可复用代码如下:
1. 可视文本范围判断
不需要手动通过字体大小、行高参数计算,直接用Range和getBoundingClientRect()接口实现精准判断,误差可以控制在1像素以内:
- 先调用
scrollContainer.getBoundingClientRect()获取滚动容器的可视区域边界坐标 - 用
document.createTreeWalker遍历容器内的所有文本节点,为每个文本节点创建Range对象,调用range.getBoundingClientRect()拿到节点的位置坐标 - 判断节点坐标是否和容器可视范围有重叠,重叠占比超过设定阈值(通常设为30%即可判定为用户可见)的部分即为当前展示的文本范围
- 如果需要行级粒度的统计结果,可以调用
Range.getClientRects()拿到每一行文本的单独坐标再做匹配
2. 事件监听与时长统计逻辑
- 滚动事件做100~200ms的节流优化,避免高频触发影响页面性能,每次触发时更新当前可视文本范围
- 监听
resize事件,窗口尺寸变化时立即重新计算当前可视范围,同步记录对应时间戳 - 维护两个数据集合:当前可视文本片段的首次展示时间戳映射表、所有文本片段的累计展示时长统计表
- 每次范围更新时对比新旧可视集合:
- 对于不再可视的片段,用当前时间戳减去首次展示时间,累计到该片段的总展示时长中,清除其首次展示时间记录
- 对于新进入可视区的片段,记录当前时间戳作为首次展示时间
- 快速滚动时如果片段可视时间小于300ms可以判定为无效浏览,不计入时长,避免误统计
核心参考代码
// 获取滚动容器内当前可视的文本片段 function getVisibleTextRanges(scrollContainer, visibleThreshold = 0.3) { const containerRect = scrollContainer.getBoundingClientRect(); const visibleRanges = []; const textWalker = document.createTreeWalker(scrollContainer, NodeFilter.SHOW_TEXT, null); let currentTextNode; while (currentTextNode = textWalker.nextNode()) { if (!currentTextNode.textContent.trim()) continue; // 跳过空文本节点 const nodeRange = document.createRange(); nodeRange.selectNodeContents(currentTextNode); const nodeRect = nodeRange.getBoundingClientRect(); // 计算重叠高度占比 const overlapTop = Math.max(nodeRect.top, containerRect.top); const overlapBottom = Math.min(nodeRect.bottom, containerRect.bottom); const overlapHeight = Math.max(0, overlapBottom - overlapTop); const visibleRatio = overlapHeight / nodeRect.height; if (visibleRatio >= visibleThreshold) { visibleRanges.push({ text: currentTextNode.textContent, visibleRatio: visibleRatio, nodeId: currentTextNode.parentNode.dataset.paragraphId || Math.random().toString(36).slice(2) // 可提前给段落绑定唯一ID方便统计 }); } } return visibleRanges; } // 节流工具函数 function throttle(fn, delay) { let lastCall = 0; return function(...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; fn.apply(this, args); } } } // 统计逻辑绑定 const scrollContainer = document.querySelector('#your-scroll-container'); const visibleTextMap = new Map(); // 存储当前可视片段的首次展示时间 const durationStats = new Map(); // 存储各片段累计展示时长 const updateStats = throttle(() => { const currentVisible = getVisibleTextRanges(scrollContainer); const currentNodeIds = new Set(currentVisible.map(item => item.nodeId)); const now = Date.now(); // 处理不再可视的片段 for (const [nodeId, startTime] of visibleTextMap.entries()) { if (!currentNodeIds.has(nodeId)) { const showDuration = now - startTime; if (showDuration >= 300) { // 过滤快速划过的无效时长 durationStats.set(nodeId, (durationStats.get(nodeId) || 0) + showDuration); } visibleTextMap.delete(nodeId); } } // 处理新进入可视区的片段 currentVisible.forEach(item => { if (!visibleTextMap.has(item.nodeId)) { visibleTextMap.set(item.nodeId, now); } }); }, 150); scrollContainer.addEventListener('scroll', updateStats); window.addEventListener('resize', updateStats); // 页面隐藏/显示时也可同步触发统计修正 document.addEventListener('visibilitychange', updateStats);
内容的提问来源于stack exchange,提问作者snarky
相关产品推荐
相关产品推荐

