前言
组件封装对于学过React或者Vue框架的同学应该都不陌生,就我个人而言我之前的原生JS基础并不太是太好,一开始接触前端是因为参加学校的实验室做React Native项目,我是稍微了解一下原生JS之后就直接学React了,对于React组件的封装已经是比较熟悉了,而封装原生JS组件对我来说还是很新鲜的。上完这次课,自己跟着把代码敲了一遍真的是受益匪浅,让我对框架的理解又加深了一步,希望大家有时间的话都好好做一下这个轮播图组件,很棒。
什么是组件封装
组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS) 的单元。好的组件具备封装性
、正确性
、拓展性
、复用性
。
深入探讨前端组件化开发
这篇文章里概况的介绍了什么是组件,组件化的好处,如何设计组件等等,如果对这些概念还不是很清楚的可以先看看这篇文章,接下来我们就上个小例子。
轮播图小例子
效果
线上预览地址:
https://code.h5jun.com/vata/edit?html,css,js,output
分析
1.结构设计:HTML
轮播图是一个典型的列表结构,我们可以使用无序列表<ul>
元素来实现
2.表现:CSS
- 使用CSS绝对定位将图片重叠在同一个位置
- 轮播图切换的状态使用修饰符(modifier)
- 轮播图的切换动画使用CSS transition
3.行为:JS
- API设计应保证原子,职责单一,满足灵活性
3.行为:控制流
- 使用自定义事件来解耦
版本1 (API)
slider 表示组件名,-list表示元素,__item表示具体元素项,–selected表示的是状态
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#my-slider {
position: relative;
width: 790px;
}
.slider-list ul {
list-style: none;
position: relative;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
/* 经典子绝父相,子元素利用绝对定位重叠在一起 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
/* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
</style>
</head>
<body>
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png" />
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg" />
</li>
<li class="slider-list__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg" />
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg" />
</li>
</ul>
</div>
<script>
// 轮播图类 里面封装一些api
class Slider {
constructor(id) {
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll(
".slider-list__item, .slider-list__item--selected"
);
}
// 获取选中的图片元素
getSelectedItem() {
const selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
//获取选中图片的索引值,返回其在items数组中的位置
getSelectedItemIndex() {
// Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
// querySelectorAll返回NodeList 对象,NodeList 不是一个数组,是一个类似数组的对象(Like Array Object)。
// 虽然 NodeList 不是一个数组,但是可以使用 forEach() 来迭代。你还可以使用 Array.from() 将其转换为数组。
return Array.from(this.items).indexOf(this.getSelectedItem());
}
// 跳转到指定索引的图片
slideTo(idx) {
const selected = this.getSelectedItem();
if (selected) {
// 将之前选择的图片标记为普通状态
selected.className = "slider-list__item";
}
const item = this.items[idx];
if (item) {
// 将当前选中的图片标记为选中状态
item.className = "slider-list__item--selected";
}
}
// 跳转到下一索引的图片,将下一张图片标记为选中状态
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
// 跳转到上一索引的图片,将上一张图片标记为选中状态
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx =
(this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
}
const slider = new Slider("my-slider");
setInterval(() => {
slider.slideNext();
}, 1000);
</script>
</body>
</html>
版本2 (控制流)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#my-slider {
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul {
list-style-type: none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
/* 经典子绝父相,子元素利用绝对定位重叠在一起 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
/* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
/* 这行以下是新增的 */
.slide-list__control {
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slide-list__next,
.slide-list__previous {
display: inline-block;
position: absolute;
top: 50%; /*经典居中定位 绝对定位在50%处 上边框上移盒高度的一半*/
margin-top: -25px;
width: 30px;
height: 50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0, 0, 0, 0.2); /*设置为半透明*/
cursor: pointer;
opacity: 0;
transition: opacity 0.5s; /*变化的动画,时间为.5秒*/
}
.slide-list__previous {
left: 0; /*定位在slider元素的最左边*/
}
.slide-list__next {
right: 0; /*定位在slider元素的最右边*/
}
#my-slider:hover .slide-list__previous {
opacity: 1;
}
#my-slider:hover .slide-list__next {
opacity: 1;
}
.slide-list__previous:after {
content: "<";
}
.slide-list__next:after {
content: ">";
}
/*四个小圆点的样式*/
.slide-list__control-buttons,
.slide-list__control-buttons--selected {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
cursor: pointer; /*设置鼠标移动到这个元素时显示为手指状*/
}
/*当选择后,小圆点的颜色变成红色*/
.slide-list__control-buttons--selected {
background-color: red;
}
</style>
</head>
<body>
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png" />
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg" />
</li>
<li class="slider-list__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg" />
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg" />
</li>
</ul>
<a class="slide-list__next"></a>
<a class="slide-list__previous"></a>
<div class="slide-list__control">
<span class="slide-list__control-buttons--selected"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
</div>
</div>
<script>
class Slider {
constructor(id, cycle = 3000) {
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll(
".slider-list__item, .slider-list__item--selected"
);
this.cycle = cycle;
const controller = this.container.querySelector(
".slide-list__control"
);
if (controller) {
const buttons = controller.querySelectorAll(
".slide-list__control-buttons, .slide-list__control-buttons--selected"
);
// 给小圆点加监听,经过某个圆点的就将图片切换到该位置的图片
controller.addEventListener("mouseover", (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if (idx >= 0) {
this.slideTo(idx);
this.stop();
}
});
// 鼠标移开小圆点,就继续开始循环轮播
controller.addEventListener("mouseout", (evt) => {
this.start();
});
// 注册slide事件,将选中的图片和小圆点设置为selected状态
this.container.addEventListener("slide", (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector(
".slide-list__control-buttons--selected"
);
if (selected) selected.className = "slide-list__control-buttons";
buttons[idx].className = "slide-list__control-buttons--selected";
});
}
// 点击左边小箭头,翻到前一页
const previous = this.container.querySelector(
".slide-list__previous"
);
if (previous) {
previous.addEventListener("click", (evt) => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
// 点击右边小箭头,翻到后一页
const next = this.container.querySelector(".slide-list__next");
if (next) {
next.addEventListener("click", (evt) => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
}
getSelectedItem() {
let selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx) {
let selected = this.getSelectedItem();
if (selected) {
selected.className = "slider-list__item";
}
let item = this.items[idx];
if (item) {
item.className = "slider-list__item--selected";
}
const detail = { index: idx };
const event = new CustomEvent("slide", { bubbles: true, detail });
this.container.dispatchEvent(event);
}
slideNext() {
let currentIdx = this.getSelectedItemIndex();
let nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
let currentIdx = this.getSelectedItemIndex();
let previousIdx =
(this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
// 定义一个定时器,循环播放
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
// 停止循环播放(用户在自己操作的时候要停止自动循环)
stop() {
clearInterval(this._timer);
}
}
const slider = new Slider("my-slider");
slider.start();
</script>
</body>
</html>
你以为到这里就结束啦???不不不,能优化的地方还有很多,我们接着往下看
插件化解耦JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#my-slider {
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul {
list-style-type: none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
/* 经典子绝父相,子元素利用绝对定位重叠在一起 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
/* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
/* 这行以下是新增的 */
.slide-list__control {
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slide-list__next,
.slide-list__previous {
display: inline-block;
position: absolute;
top: 50%;
/*经典居中定位 绝对定位在50%处 上边框上移盒高度的一半*/
margin-top: -25px;
width: 30px;
height: 50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0, 0, 0, 0.2);
/*设置为半透明*/
cursor: pointer;
opacity: 0;
transition: opacity 0.5s;
/*变化的动画,时间为.5秒*/
}
.slide-list__previous {
left: 0;
/*定位在slider元素的最左边*/
}
.slide-list__next {
right: 0;
/*定位在slider元素的最右边*/
}
#my-slider:hover .slide-list__previous {
opacity: 1;
}
#my-slider:hover .slide-list__next {
opacity: 1;
}
.slide-list__previous:after {
content: "<";
}
.slide-list__next:after {
content: ">";
}
/*四个小圆点的样式*/
.slide-list__control-buttons,
.slide-list__control-buttons--selected {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
cursor: pointer;
/*设置鼠标移动到这个元素时显示为手指状*/
}
/*当选择后,小圆点的颜色变成红色*/
.slide-list__control-buttons--selected {
background-color: red;
}
</style>
</head>
<body>
<div id="my-slider" class="slider-list">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png" />
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg" />
</li>
<li class="slider-list__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg" />
</li>
<li class="slider-list__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg" />
</li>
</ul>
<a class="slide-list__next"></a>
<a class="slide-list__previous"></a>
<div class="slide-list__control">
<span class="slide-list__control-buttons--selected"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
<span class="slide-list__control-buttons"></span>
</div>
</div>
<script>
class Slider {
constructor(id, cycle = 3000) {
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll(
".slider-list__item, .slider-list__item--selected"
);
this.cycle = cycle;
}
registerPlugins(...plugins) {
plugins.forEach((plugin) => plugin(this));
}
getSelectedItem() {
const selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if (selected) {
selected.className = "slider-list__item";
}
const item = this.items[idx];
if (item) {
item.className = "slider-list__item--selected";
}
const detail = { index: idx };
const event = new CustomEvent("slide", { bubbles: true, detail });
this.container.dispatchEvent(event);
}
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx =
(this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
addEventListener(type, handler) {
this.container.addEventListener(type, handler);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
// 注意 这里把之前代码中的this都替换成了参数slider
function pluginController(slider) {
const controller = slider.container.querySelector(
".slide-list__control"
);
if (controller) {
const buttons = controller.querySelectorAll(
".slide-list__control-buttons, .slide-list__control-buttons--selected"
);
controller.addEventListener("mouseover", (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if (idx >= 0) {
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener("mouseout", (evt) => {
slider.start();
});
slider.addEventListener("slide", (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector(
".slide-list__control-buttons--selected"
);
if (selected) selected.className = "slide-list__control-buttons";
buttons[idx].className = "slide-list__control-buttons--selected";
});
}
}
function pluginPrevious(slider) {
const previous = slider.container.querySelector(
".slide-list__previous"
);
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
function pluginNext(slider) {
const next = slider.container.querySelector(".slide-list__next");
if (next) {
next.addEventListener("click", (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
const slider = new Slider("my-slider");
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
</script>
</body>
</html>
既然能解耦JavaScript那能不能给HTML解耦呢?当然可以啦,接下来我们将对代码优化的方式是进行HTML模板化
。
模板化解耦HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#my-slider {
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul {
list-style-type: none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
/* 经典子绝父相,子元素利用绝对定位重叠在一起 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
/* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
/* 这行以下是新增的 */
.slide-list__control {
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slide-list__next,
.slide-list__previous {
display: inline-block;
position: absolute;
top: 50%;
/*经典居中定位 绝对定位在50%处 上边框上移盒高度的一半*/
margin-top: -25px;
width: 30px;
height: 50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0, 0, 0, 0.2);
/*设置为半透明*/
cursor: pointer;
opacity: 0;
transition: opacity 0.5s;
/*变化的动画,时间为.5秒*/
}
.slide-list__previous {
left: 0;
/*定位在slider元素的最左边*/
}
.slide-list__next {
right: 0;
/*定位在slider元素的最右边*/
}
#my-slider:hover .slide-list__previous {
opacity: 1;
}
#my-slider:hover .slide-list__next {
opacity: 1;
}
.slide-list__previous:after {
content: "<";
}
.slide-list__next:after {
content: ">";
}
/*四个小圆点的样式*/
.slide-list__control-buttons,
.slide-list__control-buttons--selected {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
cursor: pointer;
/*设置鼠标移动到这个元素时显示为手指状*/
}
/*当选择后,小圆点的颜色变成红色*/
.slide-list__control-buttons--selected {
background-color: red;
}
</style>
</head>
<body>
<div id="my-slider" class="slider-list"></div>
<script>
class Slider {
constructor(id, opts = { images: [], cycle: 3000 }) {
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render();
this.items = this.container.querySelectorAll(
".slider-list__item, .slider-list__item--selected"
);
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render() {
const images = this.options.images;
const content = images.map((image) =>
`
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim()
);
return `<ul>${content.join("")}</ul>`;
}
registerPlugins(...plugins) {
plugins.forEach((plugin) => {
const pluginContainer = document.createElement("div");
pluginContainer.className = ".slider-list__plugin";
pluginContainer.innerHTML = plugin.render(this.options.images);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
getSelectedItem() {
const selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if (selected) {
selected.className = "slider-list__item";
}
let item = this.items[idx];
if (item) {
item.className = "slider-list__item--selected";
}
const detail = { index: idx };
const event = new CustomEvent("slide", { bubbles: true, detail });
this.container.dispatchEvent(event);
}
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx =
(this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
addEventListener(type, handler) {
this.container.addEventListener(type, handler);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
const pluginController = {
render(images) {
return `
<div class="slide-list__control">
${images
.map(
(image, i) => `
<span class="slide-list__control-buttons${
i === 0 ? "--selected" : ""
}"></span>
`
)
.join("")}
</div>
`.trim();
},
action(slider) {
const controller = slider.container.querySelector(
".slide-list__control"
);
if (controller) {
const buttons = controller.querySelectorAll(
".slide-list__control-buttons, .slide-list__control-buttons--selected"
);
controller.addEventListener("mouseover", (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if (idx >= 0) {
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener("mouseout", (evt) => {
slider.start();
});
slider.addEventListener("slide", (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector(
".slide-list__control-buttons--selected"
);
if (selected) selected.className = "slide-list__control-buttons";
buttons[idx].className = "slide-list__control-buttons--selected";
});
}
},
};
const pluginPrevious = {
render() {
return `<a class="slide-list__previous"></a>`;
},
action(slider) {
const previous = slider.container.querySelector(
".slide-list__previous"
);
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
},
};
const pluginNext = {
render() {
return `<a class="slide-list__next"></a>`;
},
action(slider) {
const previous = slider.container.querySelector(".slide-list__next");
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
},
};
const slider = new Slider("my-slider", {
images: [
"https://p5.ssl.qhimg.com/t0119c74624763dd070.png",
"https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg",
"https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg",
"https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg",
],
cycle: 3000,
});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
</script>
</body>
</html>
现在再看这代码是不是有内味了,最后我们可以再进行一次优化来提高复用性。
组件框架
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#my-slider {
position: relative;
width: 790px;
height: 340px;
}
.slider-list ul {
list-style-type: none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
/* 经典子绝父相,子元素利用绝对定位重叠在一起 */
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
/* 这行上下opacity一个为0一个为1 意思是选中的不透明 未选中的透明 */
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
/* 这行以下是新增的 */
.slide-list__control {
position: relative;
display: table;
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slide-list__next,
.slide-list__previous {
display: inline-block;
position: absolute;
top: 50%;
/*经典居中定位 绝对定位在50%处 上边框上移盒高度的一半*/
margin-top: -25px;
width: 30px;
height: 50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
background: transparent;
color: white;
background: rgba(0, 0, 0, 0.2);
/*设置为半透明*/
cursor: pointer;
opacity: 0;
transition: opacity 0.5s;
/*变化的动画,时间为.5秒*/
}
.slide-list__previous {
left: 0;
/*定位在slider元素的最左边*/
}
.slide-list__next {
right: 0;
/*定位在slider元素的最右边*/
}
#my-slider:hover .slide-list__previous {
opacity: 1;
}
#my-slider:hover .slide-list__next {
opacity: 1;
}
.slide-list__previous:after {
content: "<";
}
.slide-list__next:after {
content: ">";
}
/*四个小圆点的样式*/
.slide-list__control-buttons,
.slide-list__control-buttons--selected {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin: 0 5px;
background-color: white;
cursor: pointer;
/*设置鼠标移动到这个元素时显示为手指状*/
}
/*当选择后,小圆点的颜色变成红色*/
.slide-list__control-buttons--selected {
background-color: red;
}
</style>
</head>
<body>
<div id="my-slider" class="slider-list"></div>
<script>
class Component {
constructor(id, opts = { name, data: [] }) {
this.container = document.getElementById(id);
this.options = opts;
this.container.innerHTML = this.render(opts.data);
}
registerPlugins(...plugins) {
plugins.forEach((plugin) => {
const pluginContainer = document.createElement("div");
pluginContainer.className = `.${name}__plugin`;
pluginContainer.innerHTML = plugin.render(this.options.data);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
render(data) {
/* abstract */
return "";
}
}
class Slider extends Component {
constructor(id, opts = { name: "slider-list", data: [], cycle: 3000 }) {
super(id, opts);
this.items = this.container.querySelectorAll(
".slider-list__item, .slider-list__item--selected"
);
this.cycle = opts.cycle || 3000;
this.slideTo(0);
}
render(data) {
const content = data.map((image) =>
`
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim()
);
return `<ul>${content.join("")}</ul>`;
}
getSelectedItem() {
const selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if (selected) {
selected.className = "slider-list__item";
}
const item = this.items[idx];
if (item) {
item.className = "slider-list__item--selected";
}
const detail = { index: idx };
const event = new CustomEvent("slide", { bubbles: true, detail });
this.container.dispatchEvent(event);
}
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx =
(this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
addEventListener(type, handler) {
this.container.addEventListener(type, handler);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
const pluginController = {
render(images) {
return `
<div class="slide-list__control">
${images
.map(
(image, i) => `
<span class="slide-list__control-buttons${
i === 0 ? "--selected" : ""
}"></span>
`
)
.join("")}
</div>
`.trim();
},
action(slider) {
let controller = slider.container.querySelector(
".slide-list__control"
);
if (controller) {
let buttons = controller.querySelectorAll(
".slide-list__control-buttons, .slide-list__control-buttons--selected"
);
controller.addEventListener("mouseover", (evt) => {
var idx = Array.from(buttons).indexOf(evt.target);
if (idx >= 0) {
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener("mouseout", (evt) => {
slider.start();
});
slider.addEventListener("slide", (evt) => {
const idx = evt.detail.index;
let selected = controller.querySelector(
".slide-list__control-buttons--selected"
);
if (selected) selected.className = "slide-list__control-buttons";
buttons[idx].className = "slide-list__control-buttons--selected";
});
}
},
};
const pluginPrevious = {
render() {
return `<a class="slide-list__previous"></a>`;
},
action(slider) {
let previous = slider.container.querySelector(
".slide-list__previous"
);
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
},
};
const pluginNext = {
render() {
return `<a class="slide-list__next"></a>`;
},
action(slider) {
let previous = slider.container.querySelector(".slide-list__next");
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
},
};
const slider = new Slider("my-slider", {
name: "slide-list",
data: [
"https://p5.ssl.qhimg.com/t0119c74624763dd070.png",
"https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg",
"https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg",
"https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg",
],
cycle: 3000,
});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
</script>
</body>
</html>
至此我们就优化完这个轮播图组件了,真正的实现了封装性
、正确性
、拓展性
、复用性
!