功能描述
产品要求在h5页面实现集锚点、吸顶及滑动高亮为一体的功能,如下图展示的一样。当页面滑动时,内容区域对应的选项卡高亮。当点击选项卡时,内容区域自动滑动到选项卡正下方。
布局设计
css 布局
为了更清晰的描述各功能实现的方式,将页面布局进行了如下的拆分。
★ 最外层的元素定义为 contentWrap,是使用 Intersection 定义的观察根元素。 ★ 所有可纵向滑动的元素包裹在 vertScrollWrap 中,也是粘性定位需要找到的父元素。 ★ 横向可滑动的导航栏是 horiScrollWrap ,实现吸顶功能需要设置粘性定位。 ★ observerWrap 用来包裹可观察的元素,observerItem 用来形容每一个可观察的子元素。
数据结构
导航栏的数据结构为数组,里面包括了选项卡需要显示的文案,对应的值,以及唯一值 key 。
const list = {
label: "选项卡一",
value: "1",
key: "1",
height: 150, // 模拟使用,真实场景并不需要,数据会自动将盒子撑开
}]
在我们真实的业务场景中,导航栏的标题来源于后端接口,内容区域也需要根据标题类型结合数据展示不同的内容,在获取接口数据后,我会为每一条数据增加一个随机的 key(非索引值,不会重复的8位哈希值) ,在选项卡和内容区域增加自定义属性,如 data-tab-item-id,这样可以精准的获取到所需要的 dom 元素。
选项卡吸顶
按照这个场景,首先把选项卡横向滚动及吸顶的功能实现。这里代码语法很简单,通过 position: sticky 就能实现,但需要注意的是,这里的 dom 元素布局很重要,父元素需要包裹滑动时无需展示的中间区域,以及选项卡、及里面的内容区域。
具体代码如下,这样就能实现向上滑动时,选项卡一整行固定在头部区域和内容区域之间。
// 父元素
.vertScrollWrap {
position: relative;
overflow: scroll;
height: calc(100vh - 100px);
}
// 子元素
.horiScrollWrap {
position: sticky
top: 0
}
滑动导航高亮
当手指触摸页面滑动时,我们需要知道当前出现在可视区域的内容区域是哪些,传统方案可以通过绑定 scroll 方法,这里我使用的是 IntersectionObserver,通过观察元素与父元素的交叉状态,注意⚠️ 这个api有一定的浏览器版本要求。
map 保存 dom 结构信息
在页面滑动时,需要知道每个内容区域距离父元素顶部的距离,找出距离顶部最近的元素,才能高亮对应的选项卡。当选项卡点击时,我们希望知道每个内容区域的高度,高度计算后,滚动整体到指定的高度,让选项卡对应的内容元素放在选项卡的最下方。
根据以上逻辑,需要每个内容模块的属性,这里我使用map来保存这些数据,key 为 dom 元素,value 值为对象,其中包含是否与父元素相交、距离顶部元素、元素高度等属性。
// 初始化map
domMap = new Map();
// 设置map属性
setDomMap = (dom, obj) => {
const element = this.domMap.get(dom);
const value = {
key: element?.key,
top: element?.top,
height: element?.height,
index: element?.index,
isIntersecting: element?.isIntersecting,
...obj,
};
this.domMap.set(dom, value);
};
IntersectionObserver 观察相交状态
使用 new IntersectionObserver(callback[, options]) 来定义观察逻辑。
初始化 domMap
在组件挂载时,初始化map数据,遍历所有的内容区域元素。
const prefix = "nav";
const blockId = `${
prefix}-block-id`