手写一个简单的Swiper

前言

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

分析组件需求

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- 主要结构,下面的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>
    </div>

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

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

开始之前: SwiperOptions

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

核心功能: Swiper

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

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

动态添加和删除

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

自动播放与悬停停止

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

个性化定制

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

详细代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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)`;
}
}

结语

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