看了本次 JavaScript 编码原则,让我受益匪浅,想想之前自己刚开始利用 js 写轮播图时的惨状,故想借此写写本次课程组件封装、插件化、模板化、抽象化的笔记,加深和巩固自己组件封装、插件化、模板化、抽象化的思想!
tips:本文以轮播图为例进行阐述。
无封装思想
一、实现轮播图代码演示(无封装版)
ps:之前写轮播图的时候真的完全没有封装思想
二、无封装写法的缺点
以上代码实现了一个简单的轮播图功能,但在封装化思想上存在一些缺点:
-
缺乏模块化:整个轮播图的逻辑被直接写在
<script>
标签中,没有进行合适的模块化封装。这使得代码的可维护性和可扩展性较差,很难在其他页面或项目中复用该功能。 -
存在全局变量:代码中使用了许多全局变量,如
boxTarget
、aOne
、aTwo
、spotsTarget
、sliderTarget
等。全局变量容易造成变量名冲突和命名污染,增加了代码的不稳定性。 -
代码重复和冗余:每次更改轮播图照片数量,都需要更新 css 样式和 js 方法中的相关计算,这导致了代码的重复和冗余。当轮播图中的照片数量很多时,手动更新和计算将变得非常繁琐和容易出错。
-
维护困难:由于 css 和 js 相互依赖,当调整了轮播图的照片数量后,必须同时更新两者,否则可能导致布局错乱或逻辑错误。这增加了维护代码的难度,并且容易出现错误,尤其在多人协作或长期维护的情况下。
封装和插件化写法
一、基本封装思想:
基本封装思想架构:
- 结构设计:html
<div class="slider-list" id="my-slider">
<ul>
<li class="slider-list__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png" alt="">
</li>
<li class="slider-list__item">
<img src="https://p5.ssl.qhimg.com/t01adbe3351db853eb3.png" alt="">
</li>
<li class="slider-list__item">
<img src="https://p5.ssl.qhimg.com/t01645cd5ba0c3b60cb.png" alt="">
</li>
<li class="slider-list__item">
<img src="https://p5.ssl.qhimg.com/t01331ac159b58f5478.png" alt="">
</li>
</ul>
<a class="slider-list__next"></a>
<a class="slider-list__pervious"></a>
<div class="slider-list__control">
<span class="slider-list__control-buttons--selected"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
</div>
</div>
- 展现效果:css
略
- 行为设计:js
- API(功能):API设计应保证原子操作,职责单一,满足灵活性
- Event控制流行为:使用自定义事件 解耦 。
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(".slider-list__control");
if (controller) {
const buttons = this.container.querySelectorAll(
".slider-list__control-buttons, .slider-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(); // 重新开启自动播放
});
// 监听滑动事件,给小圆点切换样式
this.container.addEventListener("slide", (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector(
".slider-list__control-buttons--selected"
);
if (selected) {
selected.className = "slider-list__control-buttons";
}
buttons[idx].className = "slider-list__control-buttons--selected";
});
}
// 上一张
const previous = this.container.querySelector(".slider-list__pervious");
if (previous) {
previous.addEventListener("click", (evt) => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
}
// 下一张
const next = this.container.querySelector(".slider-list__next");
if (next) {
next.addEventListener("click", (evt) => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
}
// 获取被选中的节点
getSelectedItem() {
const selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
// 获取被选中节点的下标
getSelectedItemIndex() {
// 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";
}
// 开发自定义事件
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);
}
// 开启定时器
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
// 关闭定时器
stop() {
clearInterval(this._timer);
}
}
let slider = new Slider("my-slider");
slider.start();[jcode](https://code.juejin.cn/pen/7262278134077554728)
基本封装思想的优缺点
- 优点:上述代码采用了面向对象的封装思想,将轮播图功能封装成了
Slider
类。这种封装思想带来了以下好处:
-
更好的代码组织和可读性:代码通过封装形成了一个独立的类,将相关的属性和方法放在一起,使代码结构更加清晰和易于理解。可以更容易地找到和修改相关逻辑,同时也便于其他开发人员理解和使用。
-
提高代码的可维护性:通过封装成类,可以更方便地对轮播图的功能进行修改和扩展。相关的代码逻辑和属性被封装在一个地方,当需要添加新功能或修改现有功能时,只需要在类定义中进行修改,而不需要在多个地方进行修改,降低了维护代码的复杂度。
-
提供可复用的组件:通过封装成类,可以创建多个独立的轮播图实例,每个实例具有各自的状态和行为。这样可以在同一页面中使用多个轮播图,或在其他页面和项目中复用该轮播图组件,提高了代码的可复用性和扩展性。
-
封装了对外接口:通过构造函数和公共方法,提供了对外的接口来控制轮播图的行为。其他开发人员只需要实例化
Slider
类,并通过公共方法来启动、停止、切换轮播图,而无需关心具体的实现细节,提高了代码的易用性。
- 缺点:上述代码尽管使用了面向对象的封装思想,但是如果不需要某一需求或者功能时,仍需要在上述代码进行大量改动。比如如果不需要上一张和下一张的切换,需要更改的地方也是和别的方法混合的,改动起来也是比较麻烦的!
二、优化封装思想-继续解耦(插件化)
优化封装思想-插件化架构
- 将控制元素抽取成插件
// 圆点控制器
const pluginController = (slider) => {
const controller = slider.container.querySelector(".slider-list__control");
if (controller) {
const buttons = controller.querySelectorAll(
".slider-list__control-buttons, .slider-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(
".slider-list__control-buttons--selected"
);
if (selected) {
selected.className = "slider-list__control-buttons";
}
buttons[idx].className = "slider-list__control-buttons--selected";
});
}
};
// 上一张
const pluginPrevious = (slider) => {
const previous = slider.container.querySelector(".slider-list__pervious");
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
};
// 下一张
const pluginNext = (slider) => {
const next = slider.container.querySelector(".slider-list__next");
if (next) {
next.addEventListener("click", (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
};
- 插件与组件之间通过依赖注入方式建立联系
let slider = new Slider("my-slider");
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
优化封装思想-插件化的优缺点:
- 优点
-
解耦和模块化:将控制元素抽取成插件后,可以将其作为独立的模块进行开发和维护。插件可以通过相应的接口来进行交互,组件只需要依赖插件提供的接口来使用其功能,而不需要关心插件的具体实现。这使得组件与插件之间的关系变得松耦合,方便独立调试和测试,同时也增强了代码的可维护性和可拓展性。
-
可复用性和扩展性:通过使用依赖注入的方式,组件可以动态地注入不同的插件实例来实现不同的功能。这样可以使得组件的功能更加灵活和可配置,可以根据具体需求选择不同插件的实现,而无需对组件的代码进行修改。这种设计思路提供了更高的可复用性和扩展性,使得组件可以应对不同的场景和需求。
-
单一职责原则:将控制元素抽取成插件后,组件只需要专注于自身的功能实现,而将控制元素的管理和操作交由插件来处理。这符合单一职责原则,使得组件和插件的功能边界更加清晰和易于维护。代码的变动和修复也更加集中和可控,减少了潜在的bug和错误。
-
灵活性和可定制性:通过插件的方式,可以根据需求选择不同的插件或为特定需求开发新的插件。这使得组件的功能可以根据具体场景和用户需求进行定制,提升了整体系统的灵活性和可定制性。同时,插件可以单独进行更新和升级,不会影响到组件的其他部分,提高了系统的可维护性和可扩展性。
- 缺点:在剔除某一特定需求时,虽然可以直接删除插件化的 js 功能代码,但是 html 仍需找到相应的代码进行剔除,仍有一定缺陷。
三、实现轮播图代码演示(封装和插件化版)
重构-模板化、组织框架-抽象化
一、重构-模板化
- 将 HTML 模板化,即将每个功能的 html 代码抽离出来,更易于扩展和管理。
<!-- 主页内容 -->
<div class="slider-list" id="my-slider"></div>
// 功能 html 模板化
class Slider {
//...
render() {
const images = this.options.images;
const content = images.map((image) =>
`
<li class="slider-list__item">
<img src="${image}" alt="">
</li>
`.trim()
);
return `<ul>${content.join("")}</ul>`;
}
// ...
}
// 圆点控制器
const pluginController = {
render(images) {
return `
<div class="slider-list__control">
${images.map((image, i) =>
`
<span class="slider-list__control-buttons${i === 0 ? "--selected" : ""}"></span>
`
).join("")}
</div>
`.trim();
},
action(slider) {
// ....
},
};
// 上一张
const pluginPrevious = {
render() {
return `
<a class="slider-list__pervious"></a>
`;
},
action(slider) {
// ....
},
};
// 下一张
const pluginNext = {
render() {
return `
<a class="slider-list__next"></a>
`;
},
action(slider) {
// ....
},
};
二、组织框架-抽象
- 将组件通用模型抽象出来
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) {
return "";
}
}
// 继承组件通用模型
class Slider extends Component {
//...
}
三、模板化抽象化后的 js 完整代码
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) {
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() {
const data = this.options.data;
const content = data.map((image) =>
`
<li class="slider-list__item">
<img src="${image}" alt="">
</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.data);
this.container.appendChild(pluginContainer);
plugin.action(this);
});
}
// 获取被选中的节点
getSelectedItem() {
const selected = this.container.querySelector(
".slider-list__item--selected"
);
return selected;
}
// 获取被选中节点的下标
getSelectedItemIndex() {
// 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";
}
// 开发自定义事件
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();
console.log(this.items);
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(data) {
return `
<div class="slider-list__control">
${data
.map(
(image, i) =>
`
<span class="slider-list__control-buttons${
i === 0 ? "--selected" : ""
}"></span>
`
)
.join("")}
</div>
`.trim();
},
action(slider) {
const controller = slider.container.querySelector(".slider-list__control");
if (controller) {
const buttons = controller.querySelectorAll(
".slider-list__control-buttons, .slider-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(
".slider-list__control-buttons--selected"
);
if (selected) {
selected.className = "slider-list__control-buttons";
}
buttons[idx].className = "slider-list__control-buttons--selected";
});
}
},
};
// 上一张
const pluginPrevious = {
render() {
return `
<a class="slider-list__pervious"></a>
`;
},
action(slider) {
const previous = slider.container.querySelector(".slider-list__pervious");
if (previous) {
previous.addEventListener("click", (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
},
};
// 下一张
const pluginNext = {
render() {
return `
<a class="slider-list__next"></a>
`;
},
action(slider) {
const next = slider.container.querySelector(".slider-list__next");
if (next) {
next.addEventListener("click", (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
},
};
let slider = new Slider("my-slider", {
data: [
"https://p5.ssl.qhimg.com/t0119c74624763dd070.png",
"https://p5.ssl.qhimg.com/t01adbe3351db853eb3.png",
"https://p5.ssl.qhimg.com/t01645cd5ba0c3b60cb.png",
"https://p5.ssl.qhimg.com/t01331ac159b58f5478.png",
],
cycle: 3000,
});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
结语
- 组件设计的原则:封装性、正确性、扩展性、复用性
- 实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构:(1)插件化 (2)模板化 (3)抽象化(组件框架)
~0v0~ 以上是有关封装、插件化、模板化、抽象化思想的笔记内容,如有错,还请指正!