原生js面向对象书写移动端轮播图@郝晨光
现已发布到npm及git,安装及使用方法见:https://gitee.com/haochenguang/hcg-swipe
先看一下效果图
该项目为仿清欢美味严选商城小程序demo
前言
轮播图的原理,其实就是一个简单的 n+2 模式,即在原有图片的基础上,再添加两张图片,以达到障眼法的效果,在这个原理方面,我就不做过多的叙述,可以自行寻找度娘,该项目使用了原生js,面向对象,移动端的touchstart,touchmove,touchend事件,有一小部分使用了ES6的语法,如箭头函数,let声明变量等等,建议有一点基础的同学来读
正文开始
- 定义一个构造函数,用来实例化我们的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;}
}
正文继续 ---- 别错过
- 在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(); // 刷新轮播图
};
-
在HSwipe.prototype._init方法中,初始化了一部分只要实例化元素就立马可以获取到的数据;例如传入的配置项,根据配置项获取根节点;设置原始下标,设置激活下标,动画时间,间隔时间,定时器的执行时间应该是由动画时间+间隔时间得到;
-
在_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节点的改变而出错
- 我在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`); // 需要轮播的每一个slidethis.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节点,默认为 spanpageBtn.className = this._option.pagination.pageName || `${nameSpace}-page-btn`; // 给DOM节点绑定类名,默认为HSwipe-page-btnthis.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');
};
- 调用 this.off 事件,确保当前只会执行一次定时器,确保所有的事件都不会被多次监听,而 this.$transitionEnd 方法是一个传入的 option中的回调函数,每次轮播完成触发,在此处清空该函数,可以看到的是还给window删除了resize事件,这是因为我们在监听事件的时候,还监听了resize事件
HSwipe.prototype.off = function () {clearInterval(this.timer);this.$transitionEnd = () => {};removeEvent(this.wrapper, 'touchstart', this._touchStart); // 触摸屏幕removeEvent(this.wrapper, 'touchmove', this._touchMove); // 触摸移动removeEvent(this.wrapper, 'touchend', this._touchEnd); // 触摸结束removeEvent(this.wrapper, 'transitionEnd', this._transitionEnd); // 动画结束removeEvent(this.wrapper, 'webkitTransitionEnd', this._transitionEnd); // 动画结束removeEvent(window, 'resize', this.refresh); // resize重新计算尺寸return null;
};
- 接着,我们重新定义this.$transitionEnd方法,重新赋值为option中的transitionEnd方法;
- 开启定时器,执行this._move方法,并通过bind绑定this指向,确保不会因为setInterval的原因,影响this指向;setInterval的执行时间为我们在_init中初始化的执行时间
- 而this._event方法中,我们初始化了所有的HSwipe事件;
/*** @method _event HSwipe事件监听* 开启 HSwipe 的事件* */
HSwipe.prototype._event = function () {this._touchStart = this._touchStart.bind(this); // 绑定事件的this指向,以及保存事件为具名函数,用于清除事件,避免重复触发this._touchMove = this._touchMove.bind(this);this._touchEnd = this._touchEnd.bind(this);this._transitionEnd = this._transitionEnd.bind(this);this.refresh = this.refresh.bind(this);onEvent(this.wrapper, 'touchstart', this._touchStart); // 触摸屏幕onEvent(this.wrapper, 'touchmove', this._touchMove); // 触摸移动onEvent(this.wrapper, 'touchend', this._touchEnd); // 触摸结束onEvent(this.wrapper, 'transitionEnd', this._transitionEnd); // 动画结束onEvent(this.wrapper, 'webkitTransitionEnd', this._transitionEnd); // 动画结束onEvent(window, 'resize', this.refresh); // resize重新计算尺寸
};
- 接着我们来看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);
};
- 来看看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); // 删除transitionsetTranslate(this.wrapper, distance); // 设置偏移if (this._option.transitionEnd) {setTimeout(() => {this.$transitionEnd.call(this, this.currentIndex);});}
};
- 最后,看一下触摸事件,即可达到我们文章开始的那个效果
/*** @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); // 删除transitionsetTranslate(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); // 添加transitionsetTranslate(this.wrapper, distance); // 设置偏移this._formatIndex(); // 判断是否需要重置activeIndex和currentIndexclearInterval(this.timer); // 清除定时器this.timer = setInterval(() => this._move(), this.execute); // 重新开启定时器
};
- 使用,写到最后还是为了用户能够良好的使用
// 初始化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();
- 最后看一下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;}}}
}
结言
感谢您的查阅,代码冗余或者有错误的地方望不吝赐教;菜鸟一枚,请多关照!
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
