javaScript工具栏吸顶思路与代码实现
前言
上下滚动是页面上常见的交互行为之一,为了让一些重要的信息始终出现在界面上,不随着页面滚动而看不见。前端会在它向上滚动出屏幕前把它固定在顶部,向下滚动到该出现的位置时把它释放掉,这样的交互方式称为吸顶。比如页面的通栏标题,一些重要导购场景的筛选条件。
场景用例
有时候写了一个很通用的组件,如果它有吸顶能力,那么吸顶的位置一定是首次开发的时候根据设计稿决定的。我们的前端组件面向的是搭建场景,可以被使用者自由组合搭建页面。你无法确定使用者是否在它前面增加了其它的吸顶组件(或许使用者都不知组件有吸顶能力,只是看着UI不错就用了),滚动的时候多个组件吸顶的位置很可能会冲突。最常见的是大家都抢占top:0的位置,就都叠在一起了。使用者当然不希望为了吸顶而迁就页面的搭配和排版,就应该吸附在正常人觉得合适的位置上。
解决思路
核心是解决多个吸顶组件之间的冲突,那就把它们管理起来吧。使用公共的方式注册到吸顶队列中,按照组件在DOM中的位置顺序决定吸顶的前后关系。滚动行为是引起吸顶交互的发动机,发生滚动行为后轮询吸顶队列,依次判断组件是否吸顶。只要组件距离上一个吸顶组件的底部边界小于等于0(快要重叠到一起了),说明是时候吸顶了。首位的吸顶组件的吸顶时机是距离视口的位置小于等于0。
API设计
- 足够简单,一行代码声明就能让目标组件具备吸顶功能。
- 组件的吸顶编排的顺序按照DOM中的自然顺序,而不是组件的声明顺序。
- 吸顶和解除吸顶前后,页面自然不抖动。
- 吸顶后的样式可控制。如果吸顶后的层级不够高,不吸顶但是层级更高的组件在滚动中就会盖住组件。顺便也可以实现吸顶前白色,吸顶变红色。
- 默认提供吸顶判断的逻辑,开发者可以重写。可重写的方法中要自动注入吸顶队列,自身的元素的各种状态,以及和前面吸顶元素之间的位置关系。
声明方式
类继承
默认导出一个Class,继承Class的组件直接具备吸顶能力。如果组件的render方法返回多个根元素,只有第一个根元素能吸顶,最好返回单个根节点。
import Sticky from '@ali/rox-sticky-helper';
export default Class extends Sticky {
render() {
return ...
}
}
组件使用
提供一个StickyView组件,该组件的子组件会具备吸顶能力。StickyView不会给子组件包裹任何元素。同样,如果有多个直接子组件,只有第一个直接子组件能吸顶,最好只有一个直接子组件。
import { StickyView } from '@ali/rox-sticky-helper';
export default function() {
return (
<StickyView>
<!-- 子组件 -->
</StickyView>
);
}
Hooks
React16和Rax1.x都提供了hooks,这里也提供一个createStickyRef钩子,作用和React的createRef钩子相同,增加了让组件吸顶的能力。
import { createStickyRef } from '@ali/rox-sticky-helper';
const stickyRef = createStickyRef();
export default function() {
return (
<div ref={stickyRef}>
</div>
);
}
吸顶编排顺序
正常情况下,组件按照A、B、C的先后顺序显示在界面上,吸顶的时候也应该按照同样的顺序吸附在屏幕上。所以,每声明一个吸顶组件,会按照它在html排版中的位置关系放入吸顶队列中合适的位置。
使用标准的DOM APIcompareDocumentPosition(),能够很容易判断2个DOM之间的位置关系。IE9就开始支持了。
返回结果一览表
代码示例
<html>
<head></head>
<body></body>
</html>
// case1: 获取head元素和body元素的位置关系
document.head.compareDocumentPosition( document.body )
// 返回结果是4,符合上表格中第3条规则
// 反过来
document.body.compareDocumentPosition( document.head )
// 返回结果是2,符合上表格中第2条规则
// case2: 获取document和body的位置关系
document.compareDocumentPosition( document.body )
// 返回结果是20
// body被document包含,返回16,符合上表格中第2条规则
// 既然document包含body,那么document必然在body之前出现,返回结果是4,符合上表格中第3条规则
// 16 + 4 = 20
// 反过来
document.body.compareDocumentPosition( document )
// 返回结果是10
// document包含body,返回8,符合上表格中第4条规则
// document在body前面,返回2,符合上表格中第2条规则
// 8 + 2 = 10
吸顶占位
吸顶方式
css3新增了position: sticky属性让元素在普通情况保持原样,在滚动到指定的top后固定不动。不过,sticky属性受父元素的高度、overflow的影响,碰到灵活多变的结构排版会失效。最终决定使用简单粗暴的position: fixed,屏蔽一切层级结构的影响。
高度占位
一个正常在文档流中的元素,假设高度是50px。吸顶后的position变为fixed,脱离了正常文档流,原来的位置上减少了50px,它后面的元素就会向上跳50px;同理,页面向下滚动,元素解除吸顶状态回归到正常文档流中,会立即多出50px把后面的元素往下挤。为了防止页面抖动,吸顶的时候插入一个不可见的同高度的占位元素,解除吸顶后再移除。
解除吸顶
组件吸顶后脱离了文档流,留下一个占位元素插入原来的位置。反过来,当占位元素快要完全从前一个吸顶元素边界上离开的时候,说明是时候解除吸顶。把原组件的样式还原回来,然后从DOM中移除占位元素。
吸顶样式
组件在文档流中的时候,宽、高可以受父元素布局影响呈现自适应状态,背景色也可以直接透出父元素的。
无样式吸顶
有样式吸顶
代码示例-继承
import Sticky from '@ali/rox-sticky-helper';
export default class extends Sticky {
getStickyStyle() {
return { color: '#fff', backgroundColor: '#2990dc' };
}
render() {
return (
<div id="d1" style={{ width: 750, lineHeight: 100, backgroundColor: '#fff' }}>
吸顶模块1
</div>
);
}
}
代码示例-组件
import { StickyView } from '@ali/rox-sticky-helper';
function Module2() {
return <StickyView
getStickyStyle={() => ({ color: '#fff', backgroundColor: '#f15a4a' })}
>
<div id="d2" style={{ width: 750, lineHeight: 100, backgroundColor: '#fff' }}>
吸顶模块2
</div>
</StickyView>;
}
代码示例-Hooks
import { createStickyRef } from '@ali/rox-sticky-helper';
const ref = createStickyRef({
getStickyStyle() {
return { color: '#fff', backgroundColor: '#f39826' };
}
});
function Module3() {
return (
<div ref={ref}
style={{ width: 750, lineHeight: 100, backgroundColor: '#fff' }}
>
吸顶模块hook
</div>
);
}
控制吸顶时机
这里再赘述一遍吸顶控制时机吧!!!
API会自动监听页面上的滚动事件,默认情况下,第一个元素距离视口小于等于0,判定吸顶。非第一个元素距离前一个元素的底边界小于等于0,判定吸顶。
如果有奇葩的业务逻辑需要自定一吸顶时机,重写onStickyScroll方法即可。
代码示例
import Sticky from '@ali/rox-sticky-helper';
class Module1 extends Sticky {
onStickyScroll(helper) {
// 默认处理逻辑
const {
prevTopDiff, // 和前一个吸顶组件之间的距离
prevBottom, // 前一个吸顶组件的底部边界在视口的位置
self, // 吸顶队列中的自己
stickyAPI, // 吸顶相关的API
forceReflow
} = helper;
const {
dom, // 组件的dom元素
isSticky // 是否吸顶
} = self;
if (isSticky) {
if (!self.keepSticky) { // 是否该解除吸顶了
stickyAPI.resetSticky(self);
} else if (dom.getBoundingClientRect().top != prevBottom) {
// 吸顶后位置发生偏差,进行二次校准
// 有时候前一个元素一边吸顶一边高度发生变化,后面的元素需要不断修改吸顶位置
dom.style.top = `${prevBottom}px`;
}
} else if (prevTopDiff <= 0) { // 距离上个吸顶元素小于等于0
stickyAPI.setSticky(self, { // 调用吸顶设置API
position: 'fixed',
zIndex: 100,
top: `${prevBottom}px`,
...self.ref.getStickyStyle(helper), // 获取用户自定义吸顶样式
}, forceReflow);
}
}
}
初始化立刻吸顶
导航组件这类初始化就处于页面最顶上吸顶组件,页面首次向上滚动的时候组件追随页面至少上移3个像素,此时组件距离视口的top是-3px,该吸顶了。于是组件需要从-3px移动到0px的位置来,页面上会出现导航组件先上后下抖动感。
于是,默认情况下,组件声明为吸顶后自动执行一次吸顶判断。这样,在屏幕首位的组件初始化后就能立刻吸顶,不会出现滚动抖动。如果你的组件和页面顶部有足够的安全距离,可以手动关闭自动吸顶。
import Sticky, { StickyView, createStickyRef } from '@ali/rox-sticky-helper';
// 继承方式
class Module1 extends Sticky {
// 默认true,初始化立即执行吸顶检测。
autoStickyTrigger = false;
}
// 组件方式
<StickyView autoStickyTrigger={false} />
// Hooks
createStickyRef({ autoStickyTrigger: false })