美文网首页让前端飞Web前端之路JavaScript 进阶营
原生js面向对象书写移动端轮播图

原生js面向对象书写移动端轮播图

作者: 郝晨光 | 来源:发表于2019-06-26 22:00 被阅读21次

    先看一下效果图

    GIF.gif

    该项目为仿清欢美味严选商城小程序demo

    前言

    轮播图的原理,其实就是一个简单的 n+2 模式,即在原有图片的基础上,再添加两张图片,以达到障眼法的效果,在这个原理方面,我就不做过多的叙述,可以自行寻找度娘,该项目使用了原生js,面向对象,移动端的touchstart,touchmove,touchend事件,有一小部分使用了ES6的语法,如箭头函数,let声明变量等等,建议有一点基础的同学来读

    正文开始
    1. 定义一个构造函数,用来实例化我们的HSwipe;并且声明一个nameSpace常量,用来定义命名空间前缀,便于修改
    (function(window){
    const nameSpace = 'h-swipe';
    /**
     * @class HSwipe
     * @param {Object} option 轮播图配置
     * @param {HTMLElement|String} option.el 轮播图外层容器
     * @param {HTMLElement|String} option.wrapper 轮播图wrapper容器
     * @param {HTMLElement|String} option.slide 轮播图slide容器
     * @param {Number} option.activeIndex 初始激活的图像
     * @param {Number} option.duration  动画消耗时间
     * @param {Number} option.interval  每帧停留时间
     * @param {Object} option.pagination 配置分页器
     * @param {String} option.pagination.el 分页器选择器
     * @param {String} option.pagination.tagName  分页器生成的标签
     * @param {String} option.pagination.pageName  分页器的使用类名
     * @param {String} option.pagination.activeClass 分页器激活使用的类名
     * @return {Object} HSwipe 实例化一个HSwipe对象
     * */
    function HSwipe(option) {
        console.log('%c swipe from 郝晨光!!!', 'color:white;font-size:14px;text-shadow: 0px 0px 5px red;');
        if (this instanceof HSwipe) {
            return this._init(option);
        } else {
            return new HSwipe(option);
        }
    }
    
    window.HSwipe = HSwipe;
    }(window))
    

    定义一些函数方法满足我们的重复使用,可以先不用看这一段,当遇到不知道的函数的时候,可以返回来查看对应的函数的功能

    /**
     * @function getRootElement  获取根节点
     * @param {HTMLElement|String} select DOM节点或者选择器
     * @return {HTMLElement|Node} DOM节点
     * */
    function getRootElement(select) {
        if (select.nodeType === 1) {
            return select;
        }
        return document.querySelectorAll(select)[0];
    }
    
    /**
     * @function getChildElement 获取子节点
     * @param {HTMLElement|String} parent 父元素节点
     * @param {HTMLElement|String} select  子元素节点
     * @return {HTMLElement|NodeList} DOM节点
     * */
    function getChildElement(parent, select) {
        return getRootElement(parent).querySelectorAll(select);
    }
    
    /**
     * @function addTransition  添加transition动画
     * @param {HTMLElement} element 需要执行动画的DOM节点
     * @param {Number} duration 设置执行动画的时间
     * */
    function addTransition(element, duration) {
        element.style.transition = `transform ${duration}ms`;
        element.style.webkitTransition = `transform ${duration}ms`;
    }
    
    /**
     * @function addTransition  取消transition动画
     * @param {HTMLElement} element 需要取消动画的DOM节点
     * */
    function removeTransition(element) {
        element.style.transition = `none`;
        element.style.webkitTransition = `none`;
    }
    
    /**
     * @function addTransition  添加transition动画
     * @param {HTMLElement} element 需要设置偏移的DOM节点
     * @param {Number} distance 设置执行偏移的距离
     * @param {String = X} direction 设置translate的方向,默认为 X
     * */
    function setTranslate(element, distance, direction = 'X') {
        element.style.transform = `translate${direction}(${distance}px)`;
        element.style.webkitTransform = `translate${direction}(${distance}px)`;
    }
    
    /**
     * @function setClass 设置class类名
     * @param {HTMLElement} element 需要设置类名的DOM节点
     * @param {String} className 需要设置的类名
     * 当element存在相同的class类名时,直接返回,否则进行设置
     * */
    function setClass(element, className) {
        let otherClassName = element.className.split(' ');
        let index = otherClassName.indexOf(className);
        if (index === -1) {
            otherClassName.push(className);
            element.className = otherClassName.join(' ');
        }
    }
    
    /**
     * @function removeClass 删除class类名
     * @param {HTMLElement} element 需要删除类名的DOM节点
     * @param {String} className 需要删除的类名
     * 当element内存在类名则删除,不存在则返回
     * */
    function removeClass(element, className) {
        let allClassName = element.className.split(' ');
        let index = allClassName.indexOf(className);
        let newClassName;
        if (index > -1) {
            allClassName.splice(index, 1);
            newClassName = allClassName.join(' ');
        } else {
            newClassName = allClassName.join(' ');
        }
        element.className = newClassName;
    }
    
    /**
     * @function onEvent addEventListener监听事件
     * 兼容性处理
     * */
    function onEvent(element, event, callback) {
        if (element.addEventListener) {
            element.addEventListener(event, callback, false);
        } else if (element.attachEvent) {
            element.attachEvent('on' + event, callback);
        } else {
            element['on' + event] = callback;
        }
    }
    
    /**
     * @function onEvent removeEventListener取消监听事件
     * 兼容性处理
     * */
    function removeEvent(element, event, callback) {
        if (element.addEventListener) {
            element.removeEventListener(event, callback, false);
        } else if (element.attachEvent) {
            element.detachEvent('on' + event, callback);
        } else {
            element['on' + event] = null;
        }
    }
    
    正文继续 ---- 别错过
    1. 在HSwipe构造函数中,执行了一个 if 判断,其实就是判断当前构造函数,如果是通过new关键字调用的话,就执行this._init方法,传入option;如果不是通过new关键字调用的,则返回一个HSwipe对象;这样可以确保我们永远可以拿到一个由HSwipe构造函数生成的实例对象
    HSwipe.prototype._init = function (option) {
        this._option = option; // 保存初始化配置
        this.container = getRootElement(this._option.el || `.${nameSpace}-container`); // 外层容器
        this.currentIndex = 0; // 当前显示的图片的原始下标
        this.activeIndex = this._option.activeIndex || 1; // 当前激活的图片的轮播下标
        this.duration = this._option.duration || 800; // 动画时间
        this.interval = this._option.interval || 2000; // 间隔时间
        this.execute = this.duration + this.interval; // 定时器的执行时间
        this.$transitionEnd = this._option.transitionEnd;
        this.refresh(); // 刷新轮播图
    };
    
    1. 在HSwipe.prototype._init方法中,初始化了一部分只要实例化元素就立马可以获取到的数据;例如传入的配置项,根据配置项获取根节点;设置原始下标,设置激活下标,动画时间,间隔时间,定时器的执行时间应该是由动画时间+间隔时间得到;

    2. 在_init方法中,我调用了getRootElement函数,以及this.refresh方法,可以看一下;
      这个方法,就是用来获取根节点,如果当前传入的本身就是一个HTML的DOM节点的话,直接返回即可,如果不是的话,将通过querySelectorAll方法获取,并拿到其中的 0号(第一个)元素
      而在refresh方法中,我调用了更多的方法,让我们来一步一步的看

    HSwipe.prototype.refresh = function () {
        this._formatHSwipe();
        this.off(); // 先关闭之前开启的事件
        this.$transitionEnd = this._option.transitionEnd;
        this.timer = setInterval(this._move.bind(this), this.execute); // 开启定时器
        this._event(); // HSwipe的事件
    };
    

    首先说一下为什么要定义这个refresh方法

    • 我们都知道,前端很多时候都需要通过ajax来请求数据,在现在特别火的Vue,React等MVVM框架中,我们更是在通过操作数据来操作DOM节点,那我们在获取到数据之后,或者说结构发生改变的时候,就要重新刷新一遍我们的轮播图,来保证我们的轮播图不会因为数据的改变或者DOM节点的改变而出错
    1. 我在refresh方法中,调用了_formatHSwipe方法,初始化DOM节点的尺寸,格式化HSwipe,在这个方法中,执行了我们的 n + 2 模式;首先定义获取轮播的wrapper,这些为什么不放在_init方法中进行呢?是因为我们在每次refresh的时候,都需要重新定义获取一遍wrapper,以保证我们的wrapper数据不会发生任何改变
      getChildElement方法,就是获取父节点指定的子节点;
    HSwipe.prototype._formatHSwipe = function () {
        this.wrapper = getChildElement(this.container, this._option.wrapper || `.${nameSpace}-wrapper`)[0]; // 图片轮播容器
        let slides = getChildElement(this.wrapper, this._option.slide || `.${nameSpace}-slide`);
        this.slideWidth = this.container.offsetWidth; // 获取图片的宽度
        let len = slides.length; // 保存原始的slide长度
        this.wrapper.style.width = this.slideWidth * (len + 2) + 'px'; // 设置wrapper的宽度为每一项的宽度 * 总图片长度 + 2;即最终处理的 n + 2 模式的长度;使其能容纳所有图片
        if (!this.disguise) {
            this.slides = getChildElement(this.wrapper, this._option.slide || `.${nameSpace}-slide`); // 需要轮播的每一个slide
            this.len = this.slides.length; // 保存原始的slide长度
            if (this.len === 0) return; // 如果当前图片长度为0,则不进行刷新轮播
            // 标识,判断是否需要重新获取DOM节点和数据
            let endDisguise = this.slides[0].cloneNode(true); // 克隆第一张图片
            let startDisguise = this.slides[this.len - 1].cloneNode(true); // 克隆最后一张图片
            this.wrapper.appendChild(endDisguise); // 将克隆的第一张图片添加到尾部
            this.wrapper.insertBefore(startDisguise, this.slides[0]); // 将克隆的最后一张图片添加到头部
            this.disguise = true;
        }
        let distance = this.slideWidth * (-this.activeIndex); // 计算下一次的位置
        setTranslate(this.wrapper, distance); // 设定初始化的位置
        this._slides = getChildElement(this.wrapper, this._option.slide || `.${nameSpace}-slide`); // 重新获取所有的图片,保存在私有属性当中,并遍历设置宽度
        for (let i = 0; i < this._slides.length; i++) {
            this._slides[i].style.width = this.slideWidth + 'px';
        }
        // 如果有分页器配置的话,初始化分页器
        if (this._option.pagination) {
            if(typeof this._option.pagination === 'boolean') {
                this._option.pagination = {};
            }
            this._formatHSwipePagination();
        }
    };
    
    

    而在_formatHSwipe方法中,执行判断,如果当前有配置的pagination的话,执行this._formatHSwipePagination();方法

    而在这个方法中,我初始化了关于pagination的所有属性和数据

    /**
     * @method _formatHSwipePagination 初始化分页器
     * */
    HSwipe.prototype._formatHSwipePagination = function () {
        this.pagination = getChildElement(this.container, this._option.pagination.el || `.${nameSpace}-pagination`)[0]; // 分页器容器
        // 删除所有之前存在的分页,避免出现重复渲染
        for (let i = 0; this.pageBtns && i < this.pageBtns.length; i++) {
            this.pagination.removeChild(this.pageBtns[i]);
        }
        // 遍历生成新的分页器
        for (let i = 0; i < this.len; i++) {
            let pageBtn = document.createElement(this._option.pagination.tagName || 'span'); // 生成DOM节点,默认为 span
            pageBtn.className = this._option.pagination.pageName || `${nameSpace}-page-btn`; // 给DOM节点绑定类名,默认为HSwipe-page-btn
            this.pagination.appendChild(pageBtn); // 追加到DOM内
        }
        this.pagination.style.marginLeft = -this.pagination.offsetWidth / 2 + 'px'; // 设置pagination容器的位置
        let pageBtnsSelect = this._option.pagination.pageName ? '.' + this._option.pagination.pageName : `.${nameSpace}-page-btn`;
        this.pageBtns = getChildElement(this.pagination, pageBtnsSelect); // 获取新的分页器
        this._pageActive(); // 激活page-btn
    };
    

    因为我们要通过slide的长度来动态的创建分页器,并且,在pagination内始终应该保证只有对应数量的分页器,所以,我们应该在创建之前,先将原有的page-btn全部删除,然后在根据slide长度创建新的page-btn,其中的this._option中的属性都是可配置项,|| 后的为默认值
    最后,获取新创建的所有page-btn;保存在this.pageBtns中;调用this._pageActive方法,保证HSwipe初始化的时候activeIndex对应的page-btn激活
    再看看_pageActive方法,很简单,先遍历删除指定的activeClass,接着在对应的page-btn上在加上指定的activeClass类名;

    /**
     * @method _pageActive  分页器使用类名激活
     * */
    HSwipe.prototype._pageActive = function () {
        // 先遍历删除所有的激活类名
        for (let i = 0; i < this.pageBtns.length; i++) {
            removeClass(this.pageBtns[i], this._option.pagination.activeClass || 'active');
        }
        // 给对应的page-btn设置active类名
        setClass(this.pageBtns[this.currentIndex], this._option.pagination.activeClass || 'active');
    };
    
    1. 调用 this.off 事件,确保当前只会执行一次定时器,确保所有的事件都不会被多次监听,而 this.$transitionEnd 方法是一个传入的 option中的回调函数,每次轮播完成触发,在此处清空该函数,可以看到的是还给window删除了resize事件,这是因为我们在监听事件的时候,还监听了resize事件
    HSwipe.prototype.off = function () {
        clearInterval(this.timer);
        this.$transitionEnd = () => {};
        removeEvent(this.wrapper, 'touchstart', this._touchStart.bind(this)); // 触摸屏幕
        removeEvent(this.wrapper, 'touchmove', this._touchMove.bind(this)); // 触摸移动
        removeEvent(this.wrapper, 'touchend', this._touchEnd.bind(this)); // 触摸结束
        removeEvent(this.wrapper, 'transitionEnd', this._transitionEnd.bind(this)); // 动画结束
        removeEvent(this.wrapper, 'webkitTransitionEnd', this._transitionEnd.bind(this)); // 动画结束
        removeEvent(window, 'resize', this.refresh.bind(this)); // resize重新计算尺寸
        return null;
    };
    
    1. 接着,我们重新定义this.$transitionEnd方法,重新赋值为option中的transitionEnd方法;
    2. 开启定时器,执行this._move方法,并通过bind绑定this指向,确保不会因为setInterval的原因,影响this指向;setInterval的执行时间为我们在_init中初始化的执行时间
    3. this._event方法中,我们初始化了所有的HSwipe事件;
    /**
     * @method _event  HSwipe事件监听
     * 开启 HSwipe 的事件
     * */
    HSwipe.prototype._event = function () {
        onEvent(this.wrapper, 'touchstart', this._touchStart.bind(this)); // 触摸屏幕
        onEvent(this.wrapper, 'touchmove', this._touchMove.bind(this)); // 触摸移动
        onEvent(this.wrapper, 'touchend', this._touchEnd.bind(this)); // 触摸结束
        onEvent(this.wrapper, 'transitionEnd', this._transitionEnd.bind(this)); // 动画结束
        onEvent(this.wrapper, 'webkitTransitionEnd', this._transitionEnd.bind(this)); // 动画结束
        onEvent(window, 'resize', this.refresh.bind(this)); // resize重新计算尺寸
    };
    
    1. 接着我们来看this._move方法;通过activeIndex的自增和调用addTransition、setTranslate方法。来执行轮播,而每次动画执行完毕,都会触发transitionEnd这个事件,而我在初始化事件的时候,监听了transitionEnd这个事件,触发this._transitionEnd这个方法
    HSwipe.prototype._move = function () {
        // 使activeIndex和currentIndex自增
        this.activeIndex++;
        this.currentIndex++;
        let distance = this.slideWidth * (-this.activeIndex); // 计算下一次的位置
        addTransition(this.wrapper, this.duration);
        setTranslate(this.wrapper, distance);
    };
    
    1. 来看看this._transitionEnd这个方法,在这个方法内我们先执行了_formtIndex方法,判断是否需要将activeIndex或者currentIndex重置,接着计算下一次的位置,并调用_pageActive方法,激活当前显示的slide对应的page-btn;并且删除原有的transition,设置新的偏移值;在尾部进行了判断,当我们的配置项中有transitionEnd这个方法的时候,回调执行这个方法,并传入当前的currentIndex索引,表示原slide的真实索引,而activeIndex表示的是进行障眼法之后的运行索引
    /**
     * @method _transitionEnd
     * 动画结束以后执行
     * */
    HSwipe.prototype._transitionEnd = function () {
        this._formatIndex(); // 判断index是否需要重置
        let distance = this.slideWidth * (-this.activeIndex); // 计算下一次的位置
        this._pageActive();
        removeTransition(this.wrapper); // 删除transition
        setTranslate(this.wrapper, distance); // 设置偏移
        if (this._option.transitionEnd) {
            setTimeout(() => {
                this.$transitionEnd.call(this, this.currentIndex);
            });
        }
    };
    
    1. 最后,看一下触摸事件,即可达到我们文章开始的那个效果
    /**
     * @method _touchStart
     * @param {event} e
     * 触摸开始
     * */
    HSwipe.prototype._touchStart = function (e) {
        // 如果是多个手指按下,直接返回,不触发事件
        if (e.touches.length > 1) {
            return;
        }
        clearInterval(this.timer); // 清除定时器
        this.touchStartX = e.touches[0].clientX - this.container.offsetLeft; // 保存初始触碰位置
        this.touchStartTime = e.timeStamp; // 保存初始触碰时间
    };
    
    /**
     * @method _touchMove
     * @param {event} e
     * 触摸移动
     * */
    HSwipe.prototype._touchMove = function (e) {
        // 移动的距离
        let touchMoveX = e.touches[0].clientX - this.touchStartX; // 计算手指滑动的距离
        let distance = -this.activeIndex * this.slideWidth + touchMoveX; //  计算当前设置的偏移量
        removeTransition(this.wrapper); // 删除transition
        setTranslate(this.wrapper, distance); // 设置偏移
    };
    
    /**
     * @method _touchEnd
     * @param {event} e
     * 触摸结束
     * */
    HSwipe.prototype._touchEnd = function (e) {
        this.touchEndX = e.changedTouches[0].clientX; // 保存当前手指离开的位置
        this.touchEndTime = e.timeStamp; // 保存手指离开的时间
        // 当滑动时间小于150的时候,切换图片
        let direction = this.touchStartX - this.touchEndX; // 正数是向左,负数是向右
        if (this.touchEndTime - this.touchStartTime <= 150 || Math.abs(direction) >= this.slideWidth / 2) {
            if (direction > 0) {
                this.activeIndex++;
                this.currentIndex++;
            } else {
                this.activeIndex--;
                this.currentIndex--;
            }
        }
        let distance = this.slideWidth * (-this.activeIndex); // 计算下一次的位置
        addTransition(this.wrapper, this.duration); // 添加transition
        setTranslate(this.wrapper, distance); // 设置偏移
        this._formatIndex(); // 判断是否需要重置activeIndex和currentIndex
        clearInterval(this.timer); // 清除定时器
        this.timer = setInterval(() => this._move(), this.execute); // 重新开启定时器
    };
    
    1. 使用,写到最后还是为了用户能够良好的使用
    // 初始化HSwipe
    let mySwipe = new HSwipe({
                        el: '.h-swipe-container',
                        pagination: {
                          el: '.h-swipe-pagination'
                        },
                        transitionEnd: current => {
                           console.log(current)
                        }
                      })
    
    // 刷新HSwipe
    mySwipe.refresh();
    
    // 卸载HSwipe
    mySwipe = mySwipe.off();
    
    
    1. 最后看一下html和css样式文件吧,样式使用scss编写
    <div class="h-swipe-container">
            <ul class="h-swipe-wrapper">
                <li class="h-swipe-slide">
                        <img src="替换src" alt="">
                </li>
                <li class="h-swipe-slide">
                        <img src="替换src" alt="">
                </li>
                <li class="h-swipe-slide">
                        <img src="替换src" alt="">
                </li>
            </ul>
            <div class="h-swipe-pagination"></div>
        </div>
    
    .h-swipe-container {
        position: relative;
        width: 100%;
        overflow: hidden;
        .h-swipe-wrapper {
            width: 100%;
            &:after {
                content: '';
                clear: both;
                display: block;
                height: 0;
                overflow: hidden;
            }
            .h-swipe-slide {
                width: 100%;
                float: left;
                background: #FFF;
                a {
                    display: block;
                }
                img {
                    width: 100%;
                }
            }
        }
        .h-swipe-pagination {
            position: absolute;
            display: flex;
            align-items: center;
            bottom: 10%;
            left: 50%;
            .h-swipe-page-btn {
                width: 8px;
                height: 4px;
                border-radius: 4px;
                margin: 0 5px;
                background: #fff;
                opacity: 0.5;
                transition: all .3s;
                &.active {
                    width: 14px;
                    opacity: 1;
                }
            }
        }
    }
    
    结言
    感谢您的查阅,代码冗余或者有错误的地方望不吝赐教;菜鸟一枚,请多关照!

    相关文章

      网友评论

        本文标题:原生js面向对象书写移动端轮播图

        本文链接:https://www.haomeiwen.com/subject/rdvhcctx.html