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

事件起因

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

实现方案

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

IntersectionObserver

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

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

开发实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 组件这个结构来设计的。先简单介绍下这个优化方案:

优化方案

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

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

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

找到原因

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

探寻解决方案

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

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

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

Round One

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()方法来获取页面的路由、实例等数据。通过查阅小程序的官方文档得知,在监听回调里面时可以返回一些有用的数据信息的:

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

理想很丰满,现实很骨感

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

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

Round Two

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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,在曝光触发的回调中进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
);
}
});

踩坑回顾

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

查看评论