一个轻量级、高性能的页面元素曝光追踪库,支持静态和动态元素的曝光检测。
- 🚀 高性能: 使用 Intersection Observer API,性能优异
- 🎯 精确检测: 支持自定义曝光阈值和曝光时间
- 🔄 动态支持: 自动检测动态添加的元素
- 🛠️ 易于集成: 简单的 API 设计,支持多种框架
- 📊 数据导出: 支持曝光数据的导出和分析
- 🎨 可定制: 丰富的配置选项和回调函数
- 🔧 ID 管理: 智能的 ID 冲突处理和自动生成
- 📈 实时统计: 提供详细的统计信息和实时更新
ViewportTracker/
├── src/
│ ├── exposure-tracker.js # 核心追踪器类
│ ├── exposure-adapter.js # 框架适配器
│ ├── exposure-tracker.d.ts # TypeScript 类型定义
│ └── exposure-adapter.d.ts # TypeScript 类型定义
├── tests/
│ ├── fixtures/
│ │ ├── merged-test.html # 综合测试页面
│ │ └── base.css # 测试页面样式
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── e2e/ # 端到端测试
├── scripts/ # 构建和开发脚本
├── dist/ # 构建输出目录
└── README.md # 项目说明文档
<!-- 引入核心追踪器 -->
<script src="src/exposure-tracker.js"></script>
<!-- 可选:引入适配器 -->
<script src="src/exposure-adapter.js"></script>// 创建追踪器实例
const tracker = new UniversalExposureTracker({
threshold: 0.5, // 50% 可见时触发(默认)
minExposureTime: 1000, // 最小曝光时间 1000ms(默认)
once: true, // 只触发一次(默认)
debug: false, // 关闭调试模式(默认)
autoObserve: true // 自动观察新元素(默认)
});
// 设置曝光回调
tracker.setExposureCallback(function(exposureData) {
console.log('元素曝光:', exposureData.elementId);
console.log('曝光时间:', exposureData.exposureTime);
console.log('元素类型:', exposureData.elementType);
});
// 设置统计更新回调
tracker.setStatsUpdateCallback(function(stats) {
console.log('统计更新:', stats.totalObserved, stats.totalExposed);
});
// 开始观察元素
const element = document.getElementById('my-element');
tracker.observe('my-element', element);// 初始化适配器
exposureTrack.init({
threshold: 0.3,
minExposureTime: 500
});
// 设置回调
exposureTrack.onExposure(function(data) {
console.log('曝光事件:', data);
});
// 观察元素
exposureTrack.observe('element-id', document.getElementById('element-id'));| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
threshold |
number | 0.5 | 元素可见比例阈值 |
rootMargin |
string | '0px' | 根边距 |
minExposureTime |
number | 1000 | 最小曝光时间(ms) |
maxExposureTime |
number | 30000 | 最大曝光时间(ms) |
once |
boolean | true | 是否只触发一次 |
debug |
boolean | false | 是否开启调试模式 |
autoObserve |
boolean | true | 是否自动观察新元素 |
debounceTime |
number | 100 | 防抖时间(ms) |
observeHiddenElements |
boolean | false | 是否观察隐藏元素 |
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
autoUpdateStats |
boolean | true | 是否自动更新统计 |
statsUpdateDelay |
number | 150 | 统计更新延迟(ms) |
enableStatsCallbacks |
boolean | true | 是否启用统计回调 |
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
enforceUniqueIds |
boolean | true | 是否强制唯一ID |
idConflictStrategy |
string | 'suffix' | ID冲突处理策略 |
autoGenerateIds |
boolean | true | 是否自动生成ID |
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
visibilityCheckers |
Array | [] | 自定义可见性检查器 |
elementTypeDetectors |
Array | [] | 自定义元素类型检测器 |
exposureRules |
Array | [] | 自定义曝光规则 |
attributeWatchers |
Array | [] | 自定义属性监听器 |
tracker.addVisibilityChecker('custom-check', (element) => {
// 自定义可见性检查逻辑
return element.offsetWidth > 0 && element.offsetHeight > 0;
});tracker.addElementTypeDetector('product-card', (element) => {
if (element.classList.contains('product-card')) {
return 'product_card';
}
return null;
});tracker.addExposureRule('product-rule', {
shouldExpose: (element, intersectionRatio) => {
if (element.classList.contains('product-card')) {
return intersectionRatio >= 0.5; // 产品卡片需要50%可见
}
return intersectionRatio >= 0.3; // 其他元素30%可见
},
getExposureTime: (element) => {
if (element.classList.contains('product-card')) {
return 1000; // 产品卡片需要1秒曝光时间
}
return 500; // 其他元素500ms
}
});<!-- 自动检测带有 data-exposure-track 属性的元素 -->
<div data-exposure-track="dynamic-element-1">
动态内容
</div>
<!-- 自动生成ID的元素 -->
<div data-exposure-track>
自动生成ID的元素
</div>// 设置动态元素回调
tracker.setDynamicElementCallback(function(elementId, element) {
console.log('检测到动态元素:', elementId);
// 可以在这里添加自定义逻辑
});// 获取统计信息
const stats = tracker.getStats();
console.log('总观察元素:', stats.totalObserved);
console.log('已曝光元素:', stats.totalExposed);
console.log('曝光率:', stats.exposureRate + '%');
console.log('动态元素统计:', stats.dynamicStats);
console.log('观察元素详情:', stats.observedElements);
console.log('曝光数据:', stats.exposureData);
// 设置统计更新回调
tracker.setStatsUpdateCallback(function(stats) {
// 实时更新统计显示
updateStatsDisplay(stats);
});{
totalObserved: 10, // 总观察元素数
totalExposed: 7, // 已曝光元素数
exposureRate: 70, // 曝光率(百分比)
dynamicStats: { // 动态元素统计
totalDynamic: 5,
dynamicExposed: 3,
dynamicExposureRate: 60
},
observedElements: [ // 观察元素详情
{
id: 'element-1',
hasExposed: true,
isDynamic: false,
elementType: 'div'
}
],
exposureData: [ // 曝光数据详情
{
elementId: 'element-1',
element: HTMLElement,
isDynamic: false,
elementType: 'div',
exposureTime: 1500,
timestamp: 1640995200000,
pageUrl: 'https://example.com',
userAgent: 'Mozilla/5.0...',
viewport: { width: 1920, height: 1080 }
}
],
lastUpdated: 1640995200000
}// 导出所有曝光数据
const allData = tracker.getAllExposureData();
const exportData = {
timestamp: new Date().toISOString(),
pageUrl: window.location.href,
exposureData: Array.from(allData.entries()).map(([id, data]) => ({
elementId: id,
...data
}))
};
// 下载为 JSON 文件
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `exposure-data-${Date.now()}.json`;
link.click();项目包含完整的测试套件:
- tests/fixtures/merged-test.html: 综合测试页面,包含所有功能演示
- tests/unit/: 单元测试,测试核心功能
- tests/integration/: 集成测试,测试完整流程
- tests/e2e/: 端到端测试,使用 Puppeteer 进行浏览器测试
# 运行所有测试
npm test
# 运行单元测试
npm test -- tests/unit/
# 运行集成测试
npm test -- tests/integration/
# 运行端到端测试
npm test -- tests/e2e/
# 查看测试覆盖率
npm run test:coverage# 启动开发服务器
npm run dev
# 打开浏览器访问 http://localhost:8080/tests/fixtures/merged-test.htmlnew UniversalExposureTracker(options)| 方法 | 参数 | 返回值 | 描述 |
|---|---|---|---|
observe(id, element, options) |
id: string, element: Element, options: Object | boolean | 开始观察元素 |
unobserve(element) |
element: Element | void | 停止观察元素 |
observeHiddenElement(id, element, options) |
id: string, element: Element, options: Object | boolean | 观察隐藏元素 |
setExposureCallback(callback) |
callback: Function | void | 设置曝光回调 |
setDynamicElementCallback(callback) |
callback: Function | void | 设置动态元素回调 |
setStatsUpdateCallback(callback) |
callback: Function | void | 设置统计更新回调 |
getStats() |
- | Object | 获取统计信息 |
getExposureData(id) |
id: string | Object|null | 获取指定元素曝光数据 |
getAllExposureData() |
- | Map | 获取所有曝光数据 |
getDynamicStats() |
- | Object | 获取动态元素统计 |
clearExposureData(id) |
id: string | void | 清除曝光数据 |
resetElementExposureStates() |
- | void | 重置元素曝光状态 |
destroy() |
- | void | 销毁追踪器 |
| 方法 | 参数 | 返回值 | 描述 |
|---|---|---|---|
addVisibilityChecker(name, checker) |
name: string, checker: Function | void | 添加可见性检查器 |
addElementTypeDetector(name, detector) |
name: string, detector: Function | void | 添加元素类型检测器 |
addExposureRule(name, rule) |
name: string, rule: Object | void | 添加曝光规则 |
addAttributeWatcher(name, watcher) |
name: string, watcher: Object | void | 添加属性监听器 |
| 方法 | 参数 | 返回值 | 描述 |
|---|---|---|---|
exposureTrack.init(options) |
options: Object | Object | 初始化追踪器 |
exposureTrack.observe(id, element, options) |
id: string, element: Element, options: Object | void | 观察元素 |
exposureTrack.unobserve(element) |
element: Element | void | 停止观察 |
exposureTrack.onExposure(callback) |
callback: Function | void | 设置曝光回调 |
exposureTrack.onStatsUpdate(callback) |
callback: Function | void | 设置统计回调 |
exposureTrack.getStats() |
- | Object | 获取统计信息 |
exposureTrack.clear() |
- | void | 清除数据 |
exposureTrack.destroy() |
- | void | 销毁追踪器 |
# 构建生产版本
npm run build
# 构建压缩版本
npm run build:minify# 启动开发服务器
npm run dev
# 运行手动测试
npm run test:manual- Chrome 51+
- Firefox 55+
- Safari 12.1+
- Edge 15+
需要以下 API 支持:
- Intersection Observer API
- MutationObserver API
- Map 和 Set(ES6)
MIT License
欢迎提交 Issue 和 Pull Request!
如有问题或建议,请通过以下方式联系:
- 提交 Issue
- 发送邮件
Universal Exposure Tracker - 让页面元素曝光检测变得简单高效!
- ✨ 初始版本发布
- 🚀 支持基础曝光检测功能
- 🔄 支持动态元素自动检测
- 📊 提供完整的统计功能
- 🛠️ 支持多种配置选项
- 🎨 提供扩展接口
- 📈 支持实时统计更新
- 🔧 智能ID管理
- 📝 完整的TypeScript类型定义
- 🧪 完整的测试套件