事件起因

由于需求迭代,在详情页增加了热门/相似职位推荐列表,为了进一步确认推荐列表对职位的效果提升,增加曝光埋点统计处理,一切从数据出发。

实现方案

由于之前做过H5端的埋点上报,自然而然想到了通过IntersectionObserver进行监听的方式来实现。至于老旧的滚动监听就不再赘述了,想必实在没办法的情况下才会去考虑[旺柴]。

IntersectionObserver

首先我们需要创建一个 IntersectionObserver Taro.createIntersectionObserver(Object component, Object options),IntersectionObserver 一共有四个方法

  • IntersectionObserver.relativeTo 使用选择器指定一个节点,作为参照区域之一。
  • IntersectionObserver.relativeToViewport(Object margins) 指定页面显示区域作为参照区域之一
  • IntersectionObserver.observeCallback callback) 指定目标节点并开始监听相交状态变化情况
  • IntersectionObserver.disconnect() 停止监听。回调函数将不再触发

开发实现

通过在JobItem组件中进行监听来实现每个职位曝光逻辑,简要代码如下:

function JobItem({
  item = {},
  exposure = () => {}, // 曝光处理
}) {
  const initObserver = () => {
    observe = Taro.createIntersectionObserver(this);
    observe
      .relativeToViewport({ bottom: -100, top: -100 })
      .observe('#Item_' + infoID, res => {
      if (res.intersectionRatio > 0) {
        exposure();
      }
    });
  };

  useEffect(() => {
    let timer = -1;
    timer = setTimeout(() => {
      initObserver();
    }, 10);

    return () => {
      clearTimeout(timer);
      observe && observe.disconnect();
    };
  }, [item]);

  return (
    <View id={`Item_${infoID}`}>
      ……
    </View>
  );
}

逻辑开发完之后在详情页观察推荐列表滚动曝光功能正常,心里美滋滋的想“Nice!”。

踩坑开始

由于推荐列表职位组件是复用首页列表的职位组件,所以想着给首页也增加上曝光逻辑,一次性补充完整。按照详情页逻辑追加相关处理后,简单看了下曝光触发没有问题,以为就这样顺利完工了。

由于曝光上报需要将每个职位在列表中的位置进行数据上报,所以额外留心了下位置数据是否正确,结果发现,首页中每个分页更新后,新分页的头部几个职位居然没有上报,出现了漏报的现象。

瞒报漏报是要承担责任的!!! 😈😈😈

分析问题

通过和详情页对比,发现可能是由于首页组件层级引起的。现有的首页由于之前做过性能优化,结合虚拟列表的思想,组件层级是按照List组件-Page组件-Item组件这个结构来设计的。先简单介绍下这个优化方案:

优化方案

通过对已有的长列表方案进行分析对比获知:

一次基于Taro的曝光逻辑踩坑记录_数据

由于当时上述两个方案都和我们小程序的实际情况不是很贴合,所以自行结合虚拟列表的原理,设计了现有的优化方案:

一次基于Taro的曝光逻辑踩坑记录_数据_02

这样就比常规方案里面把缓冲区外的item置成空的方案进一步减少了Dom结构。在这个方案里面,每个Page组件增加了IntersectionObserver监听,来更新当前分页与缓冲分页的页面编码。

找到原因

通过对组件Page层级组件的分析,怀疑是Page组件的监听事件优先级高于Item组件的监听事件,从而导致在更新分页时,新的分页的头部Item被分页的事件拦截了。

探寻解决方案

通过对IntersectionObserver相关资料的查找以及结合过往H5中的使用方法可以知道,其使用场景其实可以划分为两种:

  • 组件内部:此时监听的是组件自身,在调用createIntersectionObserver进行创建是传入的第一个参数是自定义组件的this;
  • 小程序页面级(Page):此时可以监听一类组件元素,通过给需要监听的组件增加统一的类来实现监听。

那这样我们是不是可以给上面的Page组件和Item组件添加相同的监听类,来统一监听,就不会有事件优先级的问题了啊,说干就干,改造开始。

Round One

将Page组件和Item组件中的监听逻辑全部去掉,然后同步增加相关的class ObserveItem。在首页的页面文件中增加监听方法:

initObserver = () => {
  setTimeout(() => {
    // this.$instance = getCurrentInstance()
    this._observer = Taro.createIntersectionObserver(this.$instance.page, {
      observeAll: true
    });

    this._observer
      .relativeToViewport({ bottom: 0, top: 0 })
      .observe('.ObserveItem', res => {
      console.log('ObserveItem res', res)
    });
  }, 30);
};

其中需要注意的是,在Taro开发小程序时,我们可以通过getCurrentInstance()方法来获取页面的路由、实例等数据。通过查阅小程序的官方文档得知,在监听回调里面时可以返回一些有用的数据信息的:

一次基于Taro的曝光逻辑踩坑记录_优先级_03

这样我们就可以通过在监听组件上面通过data-* 的方式来保留一些数据用于进行区分和使用了。

理想很丰满,现实很骨感

监听发现,在Taro框架下,监听回调的参数里面dataset就是个{},明明小程序官方的例子是有的,结果,Taro居然没有!!!

一次基于Taro的曝光逻辑踩坑记录_Taro_04

好的消息是,事件优先级的问题解决了😂

Round Two

既然事件优先级这个核心问题解决了,那其他的我们只能绕绕路来实现了。通过对回调方法的数据分析得知,组件元素的id是可以获取到的。通过查阅github相关issue得知,可以通过ref获取到组件元素的dataset数据。

那我们只需要每个组件id对应上它的ref就可以在曝光的时候拿到我们需要的数据了。所以在每个组件的Hooks中追加了收集ref的方法调用

// Page组件
const element = useRef(null)
useEffect(() => {
  collectRefs('page', 'JobPage_' + page, element);
}, [...]);

// Item组件
const element = useRef(null)
useEffect(() => {
  collectRefs('item', 'JobItem_' + id, element);
}, [...]);

// 页面文件中
collectRefs = (type, id, ref) => {
  this[`${type}RefList`][id] = ref;
};

这样通过维护两个对象来存储每个监听元素对应的ref,在曝光触发的回调中进行处理

this._observer
  .relativeToViewport({ bottom: 0, top: 0 })
  .observe('.ObserveItem', res => {
  const id = res.id;
  // 通过id中包含的专有前缀判定是哪个级别的组件,然后分别进行对应处理逻辑
  if (id.indexOf('JobPage') >= 0) {
    // 分页更新逻辑
  } else if (id.indexOf('JobItem') >= 0) {
    // 曝光处理
    this.exposure(
      this.itemRefList[id].current.dataset.item,
      res.intersectionRatio
    );
  }
});

踩坑回顾

其实这次需求处理是一个很常规的逻辑,只不过遇到了多级同类事件影响,以及框架封装能力和小程序官方不一致的情况,导致处理起来做了很多“迂回”处理。特此对这次问题的分析过程进行了本次汇总,希望能够帮助遇到同样问题的同学。如果您有更好的解决方案,也请留言告知,不胜感激!