这是我参与「第四届青训营 」笔记创作活动的第2天
一、重点内容如何写好JS
- html,css,js各司其职
- 组件的封装
- 抽象思想
二、详细知识点介绍(各司其职,组件封装,抽象思想)
1. 各司其职
核心是各自做各自的任务,html负责结构,css负责表现,js负责行为。
例子:想通过按钮来实现网页深色模式和浅色模式切换的效果
- 做法一(不推荐,新手容易犯,前端爱好者水平)
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(e.target.innerHTML === '🌞') {
body.style.backgroundColor = 'black';
body.style.color = 'white';
e.target.innerHTML = '🌜';
} else {
body.style.backgroundColor = 'white';
body.style.color = 'black';
e.target.innerHTML = '🌞';
}
});
做法一通过element.style的方式去修改了样式细则,虽然同样可以达到目的,单从各司其职的角度上js已经越界了,js应该关注的是这个行为,行为后的具体表现如何仍是css考虑的。并且这样写的代码十分复杂,如果是其他人来的话不能直观明白js做了什么工作。
- 做法二(改进,前端工程师水平,团队更易接受)
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
body.night {
background-color: black;
color: white;
transition: all 1s;
}
#modeBtn::after {
content: '🌞';
}
body.night #modeBtn::after {
content: '🌜';
}
同样是通过注册事件,if和else的逻辑,做法二明显比做法一更加清晰明了,能从代码中看出js负责的行为。通过改变类名的方式来达到目的,是开发中常用的手段,css和js各司其职。灵活运用css的伪元素来切换图标的显示内容,甚至可以再进一步,完全不用js,如做法三
- 做法三(进一步精简) 白天和夜晚的切换本质上就是改变样式,没有其他的行为逻辑在内,只有一个行为逻辑就是click切换。一般来说控制样式的代码是可以用纯CSS来实现的
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
...
</div>
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
#modeBtn::after {
content: '🌞';
}
#modeCheckBox:checked + .content #modeBtn::after {
content: '🌜';
}
通过对css的高级应用,伪类选择器根据checked的状态的添加样式,从而实现相同效果,同时将原本的图标的部分改写为label,通过for绑定到被隐藏的input上达到相同点击切换的效果。
总结思考:我们在面对此类纯样式的问题时,应该多思考如何用零js实现,避免不必要的由JS直接操控样式。应用时经常记得各司其职的编程原则
2. 组件封装
组件的封装应该包括三个部分:
-
结构设计
-
展示效果
-
行为设计
例子:轮播图的纯js实现,此处主要讲js行为设计的封装
- 轮播图自动播放部分(行为: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
}
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);
}
}
实现的是一个轮播图切换行为的api封装,代码看起来可能比较多,但其实内容并不复杂,下面依次说一下Slider类方法
constructor(id) 构造器,传入要操作节点的id,并获取该节点和该节点下的全部节点(slider-list__item和slider-list__item--selected,其中带selected的单独区分表示当前选中的)
getSelectedItem()获取当前轮播图里的焦点,并返回该节点
getSelectedItemIndex() 获取当前选中节点的索引
slideTo(idx) 传入给定索引值,跳转到对应的轮播图
slideNext() 切换到下一张
slidePrevious() 切换到上一张
在此不过多赘述其内部具体实现,主要是理解 行为:Api 的思想
亮点:将轮播图所具备的基本功能都给实现并封装好了,根据需求可以直接调用这些api,如下
const slider = new Slider('my-slider');
setInterval(()=>{
slider.sliderNext();
},2000)
- 轮播图事件部分,手动切换(行为:控制流)
使用自定义事件来解耦,在已有的api里面添加事件监听器来控制行为,理解思路。
下列代码为构造器里的部分改动,controller为轮播图下方的小圆点,cycle为轮播图切换的时间间隔,增加自定义事件,鼠标经过圆点时,轮播图切换到对应图片并停止自动播放,鼠标离开时继续自动播放
start(){
this.stop();
this._timer = setInterval(()=>this.slideNext(), this.cycle);
}
stop(){
clearInterval(this._timer);
}
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();
});
...
}
并且从代码中可知,自定义的事件大多在构造器里,不会过多影响原有的api。 但以上的代码仍具备改进的空间,对于小圆点部分的后续修改仍不方便,构造函数很复杂
解决思路:思考将构造函数插件化,将控制元素插件化,然后依赖注入的方式使用。这样就可以不改动slider的代码,通过让slider注册对应的插件来丰富原本的轮播图行为,并且如果不需要这些行为了也可以方便管理直接去掉,无需改动slider。
还可以将html模板化更易于扩展,此处就不展开
总结: 在组件封装部分,行为设计这一块采用API(功能)+Event(控制流)的方式实现封装。 好的组件应该具备封装性,复用性,正确性和扩展性。 通过这节课,循循渐进的理解封装的必要性,理解为什么要这样封装,收获相当的大,如何写好js真的是一门相当大的学问。