侧边栏壁纸
博主头像
MicroMatrix博主等级

曲则全,枉则直,洼则盈,敝则新,少则得,多则惑。是以圣人抱一为天下式。不自见,故明;不自是,故彰;不自伐,故有功;不自矜,故长。夫唯不争,故天下莫能与之争。古之所谓“曲则全”者,岂虚言哉!诚全而归之。

  • 累计撰写 80 篇文章
  • 累计创建 21 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

手写一个简单的Swiper

蜗牛
2023-04-26 / 0 评论 / 0 点赞 / 5 阅读 / 10237 字 / 正在检测是否收录...

前言

起初我是要编写一个画布的组件功能的。考虑到目前我的项目代码有vue2,vue3,本人还在学习React。所以我想编写一个可以不受框架限制的组件。正好借着这个机会学习一下面相对象开发组件。

分析组件需求

这一步要明确组件需要哪些基本功能。首先从使用方面来说,只需要满足指定的dom就行,然后就是满足基本的轮播图功能。那么使用场景和需求确定了。下面就初步构想下组件的功能了。

  1. 使用一个Swiper类来编写,这样就可以在new的时候传入已经存在的dom结构,来生成组件

  2. dom结构我参考了知名开源项目Swiper的结构。

    <!-- 主要结构,下面的ul主要是为了后面的页码点击跳转到指定图 -->
    <div class="canvas-list swiper" data-v-1703472734957="true">
    	<div class="swiper-wrapper" data-v-1703472734957="true" style="transform: translateX(-703px); transition: transform 0.3s ease-in-out 0s;">
    		<div id="1703472746911" class="swiper-slide" data-index="1"><div class="konvajs-content" role="presentation" style="position: relative; user-select: none; width: 703px; height: 876px;">
    			<canvas width="703" height="876" style="padding: 0px; margin: 0px; border: 0px; background: transparent; position: absolute; top: 0px; left: 0px; width: 703px; height: 876px; display: block;"></canvas>
    		</div>
    	</div>
    </div>
    <ul class="pagination-list" data-v-1703472734959="true"><li class="pagination-item" data-page="0">1</li></ul>
    

下面之后的想法是在项目开发过程中改进的。

  1. 外部传入参数来控制自动轮播图或者轮播图的切换事件
  2. 添加了新的轮播图或者轮播图切换的时候对外部进行通知

开始之前: SwiperOptions

在开始创建我们的 Swiper 类之前,首先需要设定一个名为 SwiperOptions 的预定义接口。这个接口收纳了我们的轮播器中需要的配置,包括是否启动自动播放 (autoplay)、播放间隔 (delay)、是否显示分页指示器 (pagination),及用户交互行为的启用 (enablePlayenablePagination)。我们同样还定义了两个回调函数,onSlideAddedonSlideChanged,以便有新图片添加或者当前图片更换时触发。

核心功能: Swiper

接下来就是我们的主角,Swiper 类。它需要接收一个HTML元素(或其选择器),以及我们刚才定义的 SwiperOptions 接口作为构造函数的参数。在这个类中,我们会获取并设置图片容器,轮播元素,以及其他相关参数。

我们的轮播器中提供了三个关键的方法在进行图片切换操作:slideToNextslideToPrevslideTo ,这三个方法分别用于切换到下一张、上一张或指定索引的图片。并且与一个数据 currentSlideIndex 携手工作,准确地记住和展示当前的图片。

动态添加和删除

为了允许图片的动态增删,我们采用了 MutationObserver 来监视DOM的变化。在新的图片添加或移除时,相应的方法 handleSlideAddedhandleSlideDeleted 会被调用,这样就能动态地刷新轮播器的状态,并触发对应的轮播器事件。

自动播放与悬停停止

此外,我们的轮播器还具备自动播放的特性。在配置中讲 enablePlayautoplay 设为 true,并指定合适的 delay 延迟时长,轮播器就会自动切换图片了。为了用户体验,当鼠标指针移动到轮播器上时,自动播放会被暂停,鼠标离开又会恢复。

个性化定制

最后,为了做到个性化定制,我们可以提供更多的自定义配置选项,通过直接调整 SwiperOptions 接口或者继承并扩展Swiper类中的方法来实现。

详细代码

interface SwiperOptions {
	autoplay?: boolean;
	delay?: number;
	enablePlay?: boolean;
	enablePagination?: boolean;
	pagination?: any;
	onSlideAdded?: ((newSlide: HTMLElement) => void) | null;
	onSlideChanged?: ((index: number) => void) | null;
}

interface Pagination {
	updatePageNumber: (length: number) => void;
	activePageNumberByIndex: (index: number) => void;
}

export class Swiper {
	lastPagination: HTMLElement | null = null;
	settings: SwiperOptions;
	container: HTMLElement;
	wrapper: HTMLElement;
	slides: NodeListOf<HTMLElement>;
	currentSlideIndex: number = 0;
	slideWidth: number;
	autoSlideInterval: any;
	observer: MutationObserver;

	constructor(selector: string | HTMLElement, options: SwiperOptions) {
		const defaultOptions: SwiperOptions = {
			autoplay: true,
			delay: 3000,
			enablePlay: true,
			enablePagination: false,
			pagination: null,
		};

		this.settings = Object.assign(
			{
				onSlideAdded: null,
				onSlideChanged: null,
			},
			defaultOptions,
			options
		);

		this.container =
			typeof selector === "string"
				? (document.querySelector(selector)! as HTMLElement)
				: selector;
		this.wrapper = this.container.querySelector(
			".swiper-wrapper"
		)! as HTMLElement;
		this.slides = this.wrapper.querySelectorAll(
			".swiper-slide"
		) as NodeListOf<HTMLElement>;

		this.slideWidth = this.slides[0].clientWidth;
		this.autoSlideInterval = setTimeout(() => {}, 0);

		this.observer = new MutationObserver((mutations) => {
			mutations.forEach((mutation) => {
				if (mutation.type === "childList" && mutation.addedNodes.length) {
					this.handleSlideAdded(mutation.addedNodes[0] as HTMLElement);
				} else if (
					mutation.type === "childList" &&
					mutation.removedNodes.length
				) {
					this.handleSlideDeleted(mutation.removedNodes[0] as HTMLElement);
				}
			});
		});

		this.observer.observe(this.wrapper, { childList: true });

		if (this.settings.enablePlay) {
			this.container.addEventListener(`mouseenter`, () => {
				this.stopAutoSlide();
			});

			this.container.addEventListener(`mouseleave`, () => {
				this.startAutoSlide();
			});
		}

		this.slideTo = this.slideTo.bind(this);
	}

	destroy() {
		this.container.removeEventListener(`mouseenter`, () => {
			this.stopAutoSlide();
		});

		this.container.removeEventListener(`mouseleave`, () => {
			this.startAutoSlide();
		});
	}

	handleSlideDeleted(deletedSlide: HTMLElement) {
		this.refreshSlide();
		this.updatePageNumber();
		let slideIndex = this.currentSlideIndex % this.slides.length;
		this.slideTo(slideIndex);
	}

	handleSlideAdded(newSlide: HTMLElement) {
		this.refreshSlide();
		this.updatePageNumber();
		if (typeof this.settings.onSlideAdded === "function") {
			this.settings.onSlideAdded(newSlide);
		}
		this.slideTo(this.currentSlideIndex);
	}

	updatePageNumber() {
		if (this.settings.enablePagination) {
			(this.settings.pagination as Pagination).updatePageNumber(
				this.slides.length
			);
		}
	}

	activePageNumberByIndex(index: number) {
		if (this.settings.enablePagination) {
			(this.settings.pagination as Pagination).activePageNumberByIndex(index);
		}
	}

	refreshSlide() {
		this.slides = this.wrapper.querySelectorAll(
			".swiper-slide"
		) as NodeListOf<HTMLElement>;
	}

	slideToNext() {
		this.currentSlideIndex++;
		if (this.currentSlideIndex > this.slides.length - 1) {
			this.currentSlideIndex = 0;
		}
		this.activePageNumberByIndex(this.currentSlideIndex);
		this.wrapper.style.transition = "transform 0.3s ease-in-out";
		this.wrapper.style.transform = `translateX(-${
			this.slideWidth * this.currentSlideIndex
		}px)`;
	}

	startAutoSlide() {
		this.autoSlideInterval = setInterval(() => {
			this.slideToNext();
		}, this.settings.delay!);
	}

	stopAutoSlide() {
		clearInterval(this.autoSlideInterval);
	}

	init() {
		this.wrapper.style.transform = `translateX(-${
			this.slideWidth * this.currentSlideIndex
		}px)`;
		this.refreshSlide();
		this.updatePageNumber();
		if (this.settings.enablePlay && this.settings.autoplay) {
			this.startAutoSlide();
		}
		this.slideTo(this.currentSlideIndex);
	}

	slideTo(index: number) {
		this.currentSlideIndex = index;
		if (this.currentSlideIndex < 0) {
			this.currentSlideIndex = this.slides.length - 1;
		} else if (this.currentSlideIndex > this.slides.length - 1) {
			this.currentSlideIndex = 0;
		}
		this.activePageNumberByIndex(index);
		if (typeof this.settings.onSlideChanged === "function") {
			this.settings.onSlideChanged(index);
		}
		this.wrapper.style.transition = "transform 0.3s ease-in-out";
		this.wrapper.style.transform = `translateX(-${
			this.slideWidth * this.currentSlideIndex
		}px)`;
	}

	slideToPrev() {
		this.currentSlideIndex--;
		if (this.currentSlideIndex < 0) {
			this.currentSlideIndex = this.slides.length - 1;
		}
		this.activePageNumberByIndex(this.currentSlideIndex);
		this.wrapper.style.transition = "transform 0.3s ease-in-out";
		this.wrapper.style.transform = `translateX(-${
			this.slideWidth * this.currentSlideIndex
		}px)`;
	}
}

结语

这只是一个菜鸟的开发学习过程,如果有更好的改进代码的方法,希望各位大佬不要惜言。

0

评论区