原生js面向对象书写移动端轮播图@郝晨光


现已发布到npm及git,安装及使用方法见:https://gitee.com/haochenguang/hcg-swipe

先看一下效果图

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); // 触摸屏幕
	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;
};
  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 () {
	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重新计算尺寸
};
  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;
			}
		}
	}
}
结言
感谢您的查阅,代码冗余或者有错误的地方望不吝赐教;菜鸟一枚,请多关照!

猜你喜欢

转载自blog.csdn.net/qq_44492790/article/details/93840580