介绍
在C端应用中,轮播组件作为一个高性能的组件,经常在页面中出现。如果引入第三方的组件库,可能会遇到以下问题:组件功能单一且难以扩展,打包后的代码体积过大,以及对外部组件的依赖性过强。因此,封装一个C端的Swiper组件来解决这些问题。 npm地址
方案
实现方式:本质上,这是一系列的DOM元素,通过调整这些DOM的偏移量,使得内部的卡片看起来仿佛在滚动。
方式 | position:left方式 | 前端控制位移量方式并使用transform:translateX() | 滑动全部交给浏览器并使用 transform:translateX() 推荐 |
---|---|---|---|
使用 | 监听页面拖拽,引起页面的重绘,明显卡顿,舍弃 | 监听页面的拖拽事件,使DOM容器随之移动。当用户放手后,通过 requestAnimationFrame API 实现DOM的缓慢位移(每次位移的偏移量是固定的)。优点:1. 较丝滑实现 2. 能够精准获取页面偏移,缺点:1. 页面偏移量不好控制 2. 不同尺寸的设备偏移结束的时间会不同 3. 回弹效果比较生硬 | 监听页面的拖拽事件,使DOM容器随之移动。当用户松手后,为DOM元素设置 transition-duration: 300ms 属性,让浏览器处理过渡动画。然后,监听 transitionend 事件,以便在滚动完成后执行相应的操作。优点:1. css 动画,依赖原生属性,丝滑滚动 2. 不同设备上,不同拖拽偏移量都能丝滑的滚动完成。 |
应用
基本的复杂使用
缩放效果
实现
基本的实现
通过监听 touch 事件,让 dom 配合移动,在放手的时候,dom 移动完成整个卡片
wrapEle.current?.setAttribute(
'style',
`transform: translateX(${-left.current.toFixed(
2
)}px); transition-duration: ${transitionDuration}ms;`
);
然后监听dom完成事件
const transitionend = (e: TransitionEvent) => {
if (e.propertyName === 'transform' && wrapEle.current) {
const style = wrapEle.current.getAttribute('style') || '';
wrapEle.current.setAttribute(
'style',
style?.replace(/transition-duration:\s(\d+)ms/, (obj, res) => {
return obj.replace(res, '0');
})
);
endTouch?.();
_update((old) => old + 1);
done();
}
};
回弹效果
判断 dom 移动的距离是否大于设置的伐值
const touchend = (e: Event) => {
removeEventListener(e.target as Element);
if (!moving.current) {
return;
}
if (wrapEle.current) {
const width = getChildWidth() || 0;
// 是否回弹
if (Math.abs(hasMove.current) > width * bounce) {
setTranslate();
return;
} else {
if (hasMove.current) {
setTranslate(true);
} else {
moving.current = false;
}
}
}
};
无限滚动
可以一直重复向左、向右滚动。在保证滚动对象的数量大于1的前提下,把对象的第一位复制放在最后,同时把复制前的最后一位复制放在第一位,并在滚动结束后更改dom的位置
useEffect(() => {
if (list.length) {
if (infinite && list.length > 1) {
const end = list[list.length - 1];
const first = list[0];
const oldList = [...list];
oldList.unshift(end);
oldList.push(first);
setSwiperList(oldList);
} else {
setSwiperList(list);
}
}
}, [list.length, infinite]);
移动结束:调整滚动结束后卡片的位置
if (infinite) {
if (activeIndex.current === -1) {
setActiveIndex(list.length - 1);
left.current = width * list.length;
} else if (activeIndex.current === list.length) {
setActiveIndex(0);
left.current = width;
}
setStyle();
}
缩放
通过传入 dom 的缩放比例,让滚动卡片跟随滚动动态变化,需要把当前展示的dom初始化设置100%比例,其他元素为缩放比例。
if (scale) {
let sameIndex = -1;
child.forEach((ele, index) => {
if (infinite) {
if (index === activeIndex.current + 1) {
sameIndex = Number(child[index].getAttribute('data-same'));
} else {
setTransform(ele as HTMLElement, scale);
}
} else {
if (index !== activeIndex.current) {
setTransform(ele as HTMLElement, scale);
}
}
});
if (sameIndex > -1) {
setTransform(child[sameIndex] as HTMLElement, 1);
}
}
无限滚动缩放情况:需要把复制的元素同时缩放、扩大,这里会给复制 dom 添加 data-same 属性来控制
自动轮播
定时让 dom 自己去轮播
移动到特定卡片,上一个、下一个
const swiperTo = (index: number) => {
clearTimer();
if (list.length === 1 || moving.current) {
return;
}
if (index >= -1 && index !== activeIndex.current) {
moving.current = true;
startTouch?.(activeIndex.current);
let targetIndex = index;
if (!infinite && targetIndex >= list.length) {
targetIndex = list.length - 1;
}
const moveLen = activeIndex.current - targetIndex;
nextActiveIndex.current = targetIndex;
hasMove.current = moveLen > 0 ? -1 : 1;
// dom 运动
setTranslate();
}
};
封装时候遇到的问题
-
把偏移量的数值付给react中的style属性,会导致页面滚动卡顿,原因:react不是实时更新的,解决:直接取dom元素,然后直接更改style属性
-
判断是左右移动、上下移动
- 偏移量 X 轴的变化大于 Y 轴的变化,就是水平移动,然后取消滚动的默认行为就能保持
-
移动的时候不要让整个 dom 跟着移动
-
卡片滚动的时候,在切换卡片的时候卡片会有明显的闪光的效果,解决:启动硬件加速渲染
-
处理缩放的场景下,transform: scale(90%) 在12pm上失效,解决:统一改成 transform: scale(0.9)
todo
- 虚拟组件
- 动态渲染,组件将要展示的时候才渲染内容
- 组件支持vue
- 支持滚动子组件为不同的宽度、上下滚动