Browse Source

feat: 优化三级嵌套iframe下spa应用监听问题

zhangyuhan 4 days ago
parent
commit
17ee4dc21c
3 changed files with 68 additions and 34 deletions
  1. 1 1
      package.json
  2. 66 32
      src/module/exposure.js
  3. 1 1
      src/module/types.js

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "rollup-js",
-  "version": "1.7.2",
+  "version": "1.8.0",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 66 - 32
src/module/exposure.js

@@ -1,6 +1,7 @@
 /* ============================================================
  * exposureDetector.js
  * 支持 Light DOM + open Shadow DOM 的曝光统计
+ * 改动:新增 scan() 方法、防抖扫描、debug 日志
  * ============================================================ */
 export function exposureDetector(options = {}) {
   /* ---------- 0. 环境检测 ---------- */
@@ -12,14 +13,14 @@ export function exposureDetector(options = {}) {
 
   /* ---------- 1. 默认配置 ---------- */
   const defaultOptions = {
-    threshold: 90,                 // 默认曝光百分比阈值
-    container: 'body',             // 观察的根容器
-    minExposureDuration: 1000,     // ms,至少曝光多久才上报
-    progressiveIntervals: [3000, 6000, 9000], // 渐进式上报间隔
-    reportThreshold: 10,           // 队列长度 > N 立即上报
+    threshold: 90,
+    container: 'body',
+    minExposureDuration: 1000,
+    progressiveIntervals: [3000, 6000, 9000],
+    reportThreshold: 10,
     error: console.error,
-    callback: () => {},            // 收到批量上报数据时的回调
-    custom: null                   // 自定义判定函数 (ratio)=>boolean
+    callback: () => {},
+    custom: null
   };
   const settings = { ...defaultOptions, ...options };
 
@@ -43,7 +44,12 @@ export function exposureDetector(options = {}) {
     const walker = document.createTreeWalker(root, Node.ELEMENT_NODE, null, false);
     let node;
     while ((node = walker.nextNode())) {
-      if (node.matches?.(selector)) res.push(node);
+      if (node.matches?.(selector)) {
+        // 简单去重:已观察过的节点直接跳过
+        if (node.__exposure_observed__) continue;
+        node.__exposure_observed__ = true;
+        res.push(node);
+      }
       if (node.shadowRoot) res.push(...querySelectorAllDeep(node.shadowRoot, selector));
     }
     return res;
@@ -92,7 +98,6 @@ export function exposureDetector(options = {}) {
           : ratio >= threshold;
 
         if (!shouldTrigger) {
-          // 离开可视区域
           if (window.__exposure__data__[name]?.timer) {
             clearTimeout(window.__exposure__data__[name].timer);
             delete window.__exposure__data__[name].timer;
@@ -101,7 +106,6 @@ export function exposureDetector(options = {}) {
           return;
         }
 
-        // 开始计时
         if (!window.__exposure__data__[name] || el.hasAttribute('data-exposure-loop')) {
           if (!window.__exposure__data__[name]) window.__exposure__data__[name] = {};
           const count = (window.__exposure__data__[name].count || 0) + 1;
@@ -128,51 +132,81 @@ export function exposureDetector(options = {}) {
     { root: rootEl, threshold: Array.from({ length: 101 }, (_, i) => i / 100) }
   );
 
-  /* ---------- 7. 递归监听所有 Shadow Root 的 MutationObserver ---------- */
+  /* ---------- 7. 防抖扫描 & 全量扫描 ---------- */
+  let scanTimer = null;
+  function debounceScan() {
+    clearTimeout(scanTimer);
+    scanTimer = setTimeout(() => {
+      debugLog('防抖扫描:开始');
+      scan();
+    }, 200);
+  }
+
+  function scan() {
+    const nodes = querySelectorAllDeep(rootEl, '[data-exposure]');
+    debugLog(`扫描到 ${nodes.length} 个节点`);
+    nodes.forEach(el => {
+      io.observe(el);
+      debugLog(`观察节点:${el.dataset.exposure}`, el);
+    });
+  }
+
+  /* ---------- 8. 递归监听所有 Shadow Root 的 MutationObserver ---------- */
   function observeDeep(root = document) {
     const mo = new MutationObserver(records => {
+      let needScan = false;
       records.forEach(rec => {
         rec.addedNodes.forEach(n => {
           if (n.nodeType !== Node.ELEMENT_NODE) return;
-          // 查找并观察
-          querySelectorAllDeep(n, '[data-exposure]').forEach(el => {
-            io.observe(el);
-            debugLog(`观察新节点:${el.dataset.exposure}`);
-          });
-          // 若本身是 Shadow Host,继续监听其 shadowRoot
+          needScan = true;
           if (n.shadowRoot) observeDeep(n.shadowRoot);
         });
       });
+      if (needScan) debounceScan();
     });
     mo.observe(root, { childList: true, subtree: true });
     return mo;
   }
 
-  /* ---------- 8. 初始化 ---------- */
-  // 8.1 先观察当前已存在的节点
-  querySelectorAllDeep(rootEl, '[data-exposure]').forEach(el => io.observe(el));
+  /* ---------- 9. 初始化 ---------- */
+  // 9.1 文档 ready 后再扫一次,防止首屏节点遗漏
+  function ready(fn) {
+    if (document.readyState !== 'loading') fn();
+    else document.addEventListener('DOMContentLoaded', fn);
+  }
+  ready(() => {
+    debugLog('文档 ready,执行首次全量扫描');
+    scan();
+  });
 
-  // 8.2 监听后续动态节点(含 Shadow DOM)
+  // 9.2 监听后续动态节点(含 Shadow DOM)
   const deepMo = observeDeep(document);
 
-  // 8.3 页面离开前上报
+  // 9.3 页面离开前上报
   window.addEventListener('beforeunload', () => handleReport(true));
 
-  /* ---------- 9. 返回清理函数 ---------- */
-  return () => {
-    io.disconnect();
-    deepMo.disconnect();
-    clearTimeout(reportTimeoutId);
-    window.removeEventListener('beforeunload', handleReport);
-    debugLog('已清理所有观察器');
+  /* ---------- 10. 返回清理函数 + scan 方法 ---------- */
+  return {
+    destroy: () => {
+      io.disconnect();
+      deepMo.disconnect();
+      clearTimeout(reportTimeoutId);
+      clearTimeout(scanTimer);
+      window.removeEventListener('beforeunload', handleReport);
+      debugLog('已清理所有观察器');
+    },
+    scan // 手动触发全量扫描
   };
 }
 
 /* ============================================================
  * 使用示例
  * ============================================================ */
-// exposureDetector({
+// const detector = exposureDetector({
 //   container: '#app',
-//   callback: data => console.table(data),
-//   debug: true   // 打开 cookie 中 debug=1 即可看日志
+//   callback: data => console.table(data)
 // });
+// // 路由切换后手动补扫
+// detector.scan();
+// // 销毁
+// detector.destroy();

+ 1 - 1
src/module/types.js

@@ -3,6 +3,6 @@ export const Subscribe = {
 }
 
 export const SDK_INFO = {
-  VERSION: '1.7.2',
+  VERSION: '1.8.0',
   PREFIX: 'JyTrack'
 }