写好 JS 的一些原则
- 各司其责
- 组件封装
- 过程抽象
组件封装
组件是指 Web 页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。
好的组件具备封装性,正确性、扩展性、复用性。
现在又来看一个例子:
用原生 JS 写一个电商网站的轮播图,应该怎么实现?
版本一
结构:HTML
轮播图是一个典型的列表结构,可以使用无序列表 <ul>
元素来实现
<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>
表现:CSS
使用 CSS 绝对定位将图片重叠在同一个位置,轮播图切换的状态使用修饰符(modifier),轮播图的切换动画使用 CSS transition
#my-slider {
position: relative;
width: 790px;
}
.slider-list ul {
list-style-type: none;
position: relative;
padding: 0;
margin: 0;
}
.slider-list__item,
.slider-list__item--selected {
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider-list__item--selected {
transition: opacity 1s;
opacity: 1;
}
行为设计:API
API 设计应保证原子操作,职责单一,满足灵活性
Slider 类的大致职责如下:
class Slider {
constructor(id) {
this.container = document.getElementById(id);
this.items = this.container
.querySelectorAll('.slider-list__item, .slider-list__item--selected');
}
// 获取选中的图片元素:通过选择器 .slider__item--selected 获得被选中的元素
getSelectedItem() {
const selected = this.container
.querySelector('.slider-list__item--selected');
return selected
}
// 获取选中图片的索引值:返回选中的元素在 items 数组中的位置。
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';
}
}
// 跳转到下一索引的图片:将下一张图片标记为选中状态
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);
}
}
现在再在 HTML 中加一段 JS 代码就可以看到效果了
const slider = new Slider('my-slider');
setInterval(() => {
slider.slideNext();
}, 1000);
效果图如下:
版本二
结构:HTML
<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>
表现:CSS
#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;
}
.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%;
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;
}
.slide-list__previous {
left: 0;
}
.slide-list__next {
right: 0;
}
#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;
}
行为设计:控制流
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);
}
}
现在就可以在 HTML 加上一点代码开始实现
const slider = new Slider('my-slider');
slider.start()
实现如下效果:
重构一:插件化
现在需要提出一个叫解耦的概念,上面两个例子的代码略显臃肿,利用解耦思想优化下代码
解耦:
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入的方式来建立联系
这里就将例如按钮切换、左右按钮切换可以抽象成一个插件
首先实现一个 pluginController 作为对圆点切换的功能的插件
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.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';
});
}
}
向前翻页功能的插件
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();
});
}
}
最后对 Slider 类进行一点修改
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) {
// 这里的 this 就是组件的实例对象
plugins.forEach(plugin => plugin(this));
}
/*
more code
*/
}
现在就可实现了,再在 HTML 中添加一点代码
const slider = new Slider('my-slider');
// 注册三个插件
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
重构二:模板化
第一次重构可以实现效果,JS 的扩展性是较好的,但是 HTML 中代码没有扩展性
现在就不是在 HTMl 代码中定义有几张图片,而是将图片传给 JS,让 JS 来动态渲染
重写 Slider 类
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);
});
}
/*
more code
*/
}
重构 pluginController 函数(控制圆点的函数)
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.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 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();
});
}
}
};
现在重构之后,HTML 中代码就不需要写太多标签
<div id="my-slider" class="slider-list"></div>
然后再在 HTML 中执行 JS 代码
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();
重构三:组件框架
这里就用到的抽象(将通用的组件模型抽象出来)
这个类图简单的描述了函数之间的关系
SliderPlugin 与 Component 是关联关系;Slider 继承自 Component
Component 类组件
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 ''
}
}
现在写 Slider 类,它是继承自 Component 类
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.container.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();
});
}
}
};
接下来就只用直接使用了
<body>
<div id="my-slider" class="slider-list"></div>
<script src="./index.js"></script>
<script>
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: 1000
});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
</script>
</body>
现在这段代码可扩展性是比较高的(月影 nb)
以上就是三个原则中的组件封装
最后
JS 的写法还真是不少,想要写好还需要长时间的磨练🤣。
若有不当之处,欢迎评论指出