Swipe轮播组件开发中的那些坑与优化技巧
项目初期的技术选型
这次的项目是个移动端为主的电商活动页,需要实现一个商品展示轮播组件。当时团队讨论了几个方案:纯CSS动画、Swiper.js、还是自己手写一个Swipe逻辑。
考虑到时间成本和功能需求,最后选择了基于touch事件自己实现Swipe轮播。原因很简单:一来项目对体积要求严格,二来现有库的功能我们用不上那么多,三来…说实话我就是想练练手,毕竟这种基础组件迟早要自己掌握。
最大的坑:性能问题
开始写的时候觉得挺简单,不就是监听touchstart、touchmove、touchend嘛。代码很快就写出来了:
let startX = 0;
let moveX = 0;
let currentX = 0;
function handleTouchStart(e) {
startX = e.touches[0].pageX;
}
function handleTouchMove(e) {
moveX = e.touches[0].pageX - startX;
// 直接更新样式
carousel.style.transform = translateX(${currentX + moveX}px);
}
function handleTouchEnd() {
// 简单判断滑动距离
if (Math.abs(moveX) > 100) {
currentX = currentX + moveX > 0 ? currentX + moveX : currentX;
}
// 恢复位置
carousel.style.transform = translateX(${currentX}px);
}
结果在真机测试时直接傻眼了,卡顿严重。特别是快速滑动时,整个页面都在抖动。折腾了半天发现问题出在频繁操作DOM上。
后来改成了使用requestAnimationFrame,并且加入了节流处理:
let ticking = false;
function handleTouchMove(e) {
moveX = e.touches[0].pageX - startX;
if (!ticking) {
window.requestAnimationFrame(() => {
carousel.style.transform = translateX(${currentX + moveX}px);
ticking = false;
});
ticking = true;
}
}
这个改动让性能提升了不少,但快速滑动时还是会有一点点掉帧。不过考虑到项目实际需求,这个程度已经可以接受了。
又踩坑了,touchmove滚动失效
解决了性能问题后,另一个更头疼的问题来了:当轮播区域有纵向滚动内容时,touchmove会被拦截,导致页面无法正常滚动。
最开始的解决思路是判断滑动角度,小于某个阈值就认为是横向滑动,阻止默认行为:
const ANGLE_THRESHOLD = Math.PI / 6; // 30度
function handleTouchMove(e) {
const deltaY = e.touches[0].pageY - startY;
const angle = Math.atan2(Math.abs(deltaY), Math.abs(moveX));
if (angle < ANGLE_THRESHOLD) {
e.preventDefault();
// 原来的移动逻辑
}
}
但是这样又带来了新的问题:快速滑动时会误判方向,导致体验很差。
最终采用了更稳妥的方案,在touchstart时记录初始位置,然后在touchmove中动态判断:
let isVerticalScroll = false;
function handleTouchMove(e) {
if (!isVerticalScroll) {
const deltaY = Math.abs(e.touches[0].pageY - startY);
const deltaX = Math.abs(e.touches[0].pageX - startX);
if (deltaY > deltaX && deltaY > 10) {
isVerticalScroll = true;
} else {
e.preventDefault();
// 原来的移动逻辑
}
}
}
function handleTouchEnd() {
isVerticalScroll = false;
// 原来的结束逻辑
}
核心代码就这几行
经过几轮优化,最终的核心代码结构大概是这样的:
class SwipeCarousel {
constructor(el, options = {}) {
this.carousel = el;
this.items = Array.from(el.children);
this.currentIndex = 0;
this.itemWidth = el.clientWidth;
this.threshold = options.threshold || 50;
this.duration = options.duration || 300;
this.startX = 0;
this.moveX = 0;
this.currentX = 0;
this.isVerticalScroll = false;
this.ticking = false;
this.init();
}
init() {
this.carousel.style.transition = transform ${this.duration}ms ease;
this.carousel.style.display = 'flex';
this.carousel.style.width = ${this.items.length * 100}%;
this.items.forEach(item => item.style.width = ${100 / this.items.length}%);
this.bindEvents();
}
bindEvents() {
this.carousel.addEventListener('touchstart', this.handleTouchStart.bind(this));
this.carousel.addEventListener('touchmove', this.handleTouchMove.bind(this));
this.carousel.addEventListener('touchend', this.handleTouchEnd.bind(this));
}
handleTouchStart(e) {
this.startX = e.touches[0].pageX;
this.isVerticalScroll = false;
}
handleTouchMove(e) {
if (!this.isVerticalScroll) {
const deltaY = Math.abs(e.touches[0].pageY - this.startY);
const deltaX = Math.abs(e.touches[0].pageX - this.startX);
if (deltaY > deltaX && deltaY > 10) {
this.isVerticalScroll = true;
return;
}
e.preventDefault();
this.moveX = e.touches[0].pageX - this.startX;
if (!this.ticking) {
window.requestAnimationFrame(() => {
this.carousel.style.transform = translateX(${this.currentX + this.moveX}px);
this.ticking = false;
});
this.ticking = true;
}
}
}
handleTouchEnd() {
if (!this.isVerticalScroll) {
if (Math.abs(this.moveX) > this.threshold) {
this.currentIndex += this.moveX > 0 ? -1 : 1;
this.currentIndex = Math.max(0, Math.min(this.items.length - 1, this.currentIndex));
}
this.currentX = -this.currentIndex * this.itemWidth;
this.carousel.style.transform = translateX(${this.currentX}px);
}
this.moveX = 0;
}
}
回顾与反思
最终效果还算满意,基本满足了项目需求。性能方面在大多数设备上都表现良好,只是在一些低端安卓机上偶尔还会有点小卡顿。
做得比较好的地方:轻量级实现、较好的兼容性、灵活的配置项。特别是解决了touchmove冲突的问题,这个方案我觉得还挺优雅的。
还能改进的地方:快速滑动的惯性效果还没做,目前是直接切换到目标位置;循环播放功能因为时间关系也没加进去;另外如果能加上自动播放就更好了。
以上是我个人对这个Swipe轮播的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

暂无评论