事件起因
由于需求迭代,在详情页增加了热门/相似职位推荐列表,为了进一步确认推荐列表对职位的效果提升,增加曝光埋点统计处理,一切从数据出发。
实现方案
由于之前做过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组件这个结构来设计的。先简单介绍下这个优化方案:
优化方案
通过对已有的长列表方案进行分析对比获知:
由于当时上述两个方案都和我们小程序的实际情况不是很贴合,所以自行结合虚拟列表的原理,设计了现有的优化方案:
这样就比常规方案里面把缓冲区外的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()方法来获取页面的路由、实例等数据。通过查阅小程序的官方文档得知,在监听回调里面时可以返回一些有用的数据信息的:
这样我们就可以通过在监听组件上面通过data-* 的方式来保留一些数据用于进行区分和使用了。
理想很丰满,现实很骨感
监听发现,在Taro框架下,监听回调的参数里面dataset就是个{}
,明明小程序官方的例子是有的,结果,Taro居然没有!!!
好的消息是,事件优先级的问题解决了😂
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
);
}
});
踩坑回顾
其实这次需求处理是一个很常规的逻辑,只不过遇到了多级同类事件影响,以及框架封装能力和小程序官方不一致的情况,导致处理起来做了很多“迂回”处理。特此对这次问题的分析过程进行了本次汇总,希望能够帮助遇到同样问题的同学。如果您有更好的解决方案,也请留言告知,不胜感激!