Swipe轮播组件开发中的那些坑与优化技巧

公孙沁仪 组件 阅读 526
赞 8 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

这次的项目是个移动端为主的电商活动页,需要实现一个商品展示轮播组件。当时团队讨论了几个方案:纯CSS动画、Swiper.js、还是自己手写一个Swipe逻辑。

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轮播的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论