day02-js
# 前言:如何写好JS
- HTML、CSS、JavaScript 职能分离
- 组件封装
- 应用函数式编程思想
# 职能分离
在使用 js 编写代码时,要注意 js 最好只负责网页的行为。以下例子说明
const btn = document.getElementById('modeBtn')
btn.addEventListener('click',(e)=>{
if(e.target.innerHTML === '🌝'){
body.style.backgroundColor = 'black';
body.style.color = 'white';
body.style.innerHTML = '🌛';
}else{
body.style.backgroundColor = 'white';
body.style.color = 'black';
body.style.innerHTML = '🌝';
}
})
2
3
4
5
6
7
8
9
10
11
12
这种写法在JS中直接操作了CSS样式,不够规范或者说没有体现出职能分离的作用(用JS完成了CSS的工作),对此进行改进的写法
const btn = document.getElementById('modeBtn')
btn.addEventListener('click',(e)=>{
const body = document.body
if(body.clasName !== 'night'){
body.className = 'night';
}else{
body.className = '';
}
})
2
3
4
5
6
7
8
9
这种写法虽然没有直接操作css样式,但是还不够完美的各司其职。对于这种纯展示的交互,可以使用纯 CSS提供的功能来完成
参考:青训营-深夜食堂-3 - 码上掘金 (juejin.cn) (opens new window)
结论:
- HTML/CSS/JS 各司其职
- 应当避免不必要的由JS直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 js 方案
# JS组件封装
组件是指web页面上抽取出来一个个包含模板(HTML)、功能(JS)和样式的(CSS)的单元。好的模板具有:正确性、扩展性、复用性
需求:使用JS封装一个轮播图
# 结构设计
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>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
css
#my-slider{
position: relative;
width:790px;
}
.slider-list ul{
list-style-type:none;
postion: relative;
padding: 0;
margin: 0;
}
.slider-list_item,
.slider-list_item--selected{
postion: absolute;
transition: opacity 1s;
opacity: 0;
text-aligen: center;
}
.slider-list_item--selected{
transition: opacity 1s;
opacity: 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
经典教学轮播图的结构设计
class Slider{
constructor(id){//构造函数
this.container =doucment.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list_item, .slider-list_item--selected');
}
getSelectedItem(){//获取选中list
const selected = this.container.querySelector('.slider-list_item--selected');
return selected;
}
getSelectedItemIndex(){//获取选中list的index
return Array.from(this.items).indexOf(this.getGetSelectedItem());
}
slideTo(idx){
const selected = this.getSelectedItem();
if(selected){
selected.className = 'slider-list_item';
}
const item = this.item[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(nextIdx);
}
}
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
青训营-轮播图-1 - 码上掘金 (juejin.cn) (opens new window)
# 加上JS行为
加上控件
html
<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>
2
3
4
5
6
7
8
css
.slider-list_control {
position: realative;
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;
font-size: 24px;
text-align: center;
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,
#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;
}
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
修改原有js
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',e=>{
const idx = Array.from(buttons).indexOf(e.target);
if(idx>=0){
this.slideTo(idx);
this.stop();//暂停定时器,防止效果冲突
}
});
controller.addEventListener('mouseout',e=>{
this.start();
})
//这个方法会更新 buttons 的选中
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',e=>{
this.stop();
this.sliderPrevious();
this.start();
e.preventDefault();
})
}
const next = this.container.querySelector('.slide-list-next');
if(next){
next.addEventListener('click',e=>{
this.stop();
this.slideNext();
this.start();
e.preventDefault();
})
}
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})
//会向一个指定的事件目标派发一个 Event 事件
this.container.dispatchEvent(event)
}
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
}
const slider = new Slider('my-slider');
slider.start();
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
至此一个功能完整的轮播图就做出来了
青训营-轮播图-2 - 码上掘金 (juejin.cn) (opens new window)
此时的轮播图虽然功能完整、但是有许多可以改进的地方。。比如当我们想要删除部分模块时,如删除 buttons-list 这个轮播小控件、或者 next、previous 按钮时。。就需要重新修改、删除代码
解决优化方法:把模块做成插件
如下所示
# 重构-插件化
- 将控制元素做抽取成插件
- 插件与组件之间通过
依赖注入方式建立联系
对 js 代码进行改进
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))
}
}
// 编写插件功能
function pluginController(slider){
const controller = slider.container.querySelector('.slide-list__control')
if(controller){
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();
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
这样就可以增删不需要的功能了,青训营-轮播图-3 - 码上掘金 (juejin.cn) (opens new window)
但是还有个问题——样式仍然存在,于是有了更加优化的版本——模板化,如下
# 重构-模板化
将HTML模板化,更易于扩展
改造 HTML 和 JS
留一个div,作为轮播图的人柱力
<div id="my-slider" class="slider-list"></div>
js
class Slider{
contructor(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(){
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();
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
这样HTML也抽象成了模板,功能删除后。插件的模板也一同删除了
青训营-轮播图-4 - 码上掘金 (juejin.cn) (opens new window)
大佬解决仍然不够完美,于是又有了抽象化
# 重构-抽象化
将组件的通用模型抽取出来
class Component {
constructor(id,opts={name,data:[]){
this.container = document.getEelmentById(id);
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
青训营-轮播图-final - 码上掘金 (juejin.cn) (opens new window)
结算如此,大佬解决代码依旧不够完美,比如父子节点的调用处理问题、js中依靠大量的css类名属性等问题是否有优化空间。