这是我参与「第四届青训营」笔记创作活动的第11天
三大原则
各司其职
前端 JS 的主要作用就是为了跟用户完成交互过程,所以首先写好 JS 的一步就是 “各司其职”,什么是 “各司其职” 呢?比如 HTML 是页面结构,CSS 是页面样式,而 JS 就是页面交互逻辑,我们就不能用 JS 去操作 CSS,而 JS 只做属于它自己的部分这就是 “各司其职”。
组件封装
当下前端各种框架下,拿 Vue 来举例,在它当中就会有一个重要的概念,那就是组件化思想,什么是组件化思想呢?就是会将一整个页面,可以把它们单独封装成一个个独立的组件,这些独立的组件会向外暴露一些 props等属性,这样在使用这些组件的时候就可以传递不同的参数,从而展示出不一样的结果。合理的组件封装可以大大提高我们的开发效率。组件封装最重要的就是为了让 UI 组件具有更强的扩展性,复用性等特点。
过程抽象
过程抽象简单理解就是,将众多事物当中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。
过程抽象就是将用来处理局部细节控制的一些方法,它是函数式编程思想的基础应用
各司其职
下面我们就使用栗子的形式来讲解 “各司其职” 这个概念
上面图片就是当我们点击标题右侧的按钮时,实现页面背景色的一个切换,可能我们第一个想到的就是使用 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 = '🌞';
}
});
很显然,虽然可以实现,但是会给我们一种感觉就是代码量很多,下面是经过优化的一个版本,改用控制class类名的方式来完成切换效果
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
现在的代码比上面的显然简洁了需要,但是这真的是最优的方案麻?
很显然不是,因为我们已经打破了一个原则,那就是 各司其职,这种操作样式的事情不应该交给 JS 来完成,而是由 CSS 来完成,下面是 CSS 的实现
首先需要在页面当中增加一个 checkbox
<input id="modeCheckBox" type="checkbox">
并且使用一个 label 来引用这个 checkbox
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
下面就是 css 实现主题切换效果
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
我们以后在写 JS 的时候也一定遵循以下过程,一些简单的样式操作的情况下,能避免使用 JS 操作就避免
- HTML/CSS/JS各司其责
- 应当避免不必要的由JS直接操作样式
- 可以用class来表示状态
- 纯展示类交互寻求零JS方案
组件封装
栗子:我们该怎么样去封装一个轮播图组件呢?
首先我们得考虑,我们该怎样去实现这个轮播图组件,实现完成之后,我们又该考虑,该如何去将它进行封装,使得它更加的独立于我们开发的项目之外,在任何项目中都能使用
轮播图结构
<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>
</ul>
</div>
轮播图样式
#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;
}
轮播图逻辑代码 首先我们肯定会想到如下的实现逻辑,能实现,但是缺点也很明显,就是所有的逻辑都被集中在一起,很难进行维护于改进,如果需要进行一些功能的拓展,那就需要修改源代码的方式才能进行完成
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);
}
}
const slider = new Slider('my-slider');
slider.slideTo(3);
代码重构 -- 解耦
什么是解耦呢?
简单点理解就是为了降低功能于功能之间的耦合度,使得它们之间不具有那么高的耦合度,以上的代码它们就具有很高的耦合度
插件化:将轮播图切换的功能使用插件的形式注入进去,比如轮播图切换上一页下一页等功能,将它们抽离成插件的形式,然后统一进行一个注入进去
// 获取容器
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();
});
}
}
模块化:将 html 进行模块化,改为 js 动态生成,原因是因为,当我们传递轮播图 url 时并不知道会传递多少个,所以使用 js 动态生成的方式可以使得更加灵活可扩展
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>`;
}
...
}
抽象化:将组件的通用模型进行抽离
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 ''
}
}
组件设计的原则:封装性、正确性、扩展性、复用性实现组件的步骤:结构设计、展现效果、行为设计
- 三次重构
- 插件化
- 模板化
- 抽象化(组件框架)
过程抽象
过程抽象简单理解就是,将众多事物当中抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程。
在了解了抽象的定义,我们再来看“过程抽象”这个词。过程抽象就是将用来处理局部细节控制的一些方法,它是函数式编程思想的基础应用。
下面我们来简单举例一下:
上面案例是一个类似于 todolist 当我们点击左边的按钮时,会进行网络请求,然后来删除我们所要完成的任务列表,但是网络请求的时间是不固定的,下面我们就来通过定时器来模拟一下网络请求。
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button) => {
button.addEventListener('click', (evt) => {
const target = evt.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
});
});
当点击右侧按钮时,会进行网络请求,这里是定时器操作,当我们定时器到达时间之后,就会将该节点进行删除,模拟后端删除该条任务,但是这样会存在问题,如果我们多次点击,就会出现下面报错
这个报错很显然出现的原因是因为我们多次点击了,但是在上一次的时候该节点可能已经被删除了,并没有了,由于延迟执行的原因导致,我们多次点击的时候,那个时间差里,该节点并没有被删除,但是当时间到达之后就会删除该节点,下一次在执行这次 removeChild 的时候,这个节点已经不存在了,所以才报了如上的错误信息。
那么我们如何处理这种错误呢?也很简单,我们只需要让绑定到该按钮上的函数只触发一次即可,但是我们可以想一想,这种函数如果直接写在监听事件的回调中真的麻?
答案肯定是不好的,因为这个函数不只是可以将这个事件监听内的回调使它执行一次,它也可以是其他的任何函数都只执行一次,因此我们需要将此函数进行抽离,因此我们会将这个过程称之为 过程抽象
下面我们就来实现一下这个函数
该函数会返回另一个函数,这个也被称之为 高阶函数 ,首先也很简单,首先先判断一下传递的函数是否存在,如果存在就使用 apply 将其进行执行,使用 apply 执行函数,只是为了 this 指向不会丢失,执行之后将 传递的函数 赋值为 null,并且返回其执行结果,赋值为 null 之后,如果下一次我们再次点击了,该函数已经为 null 了,所以不在执行了
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
现在我们使用 once 高阶函数来改进一下我们的代码
button.addEventListener('click', once((e) => {
const target = e.target;
target.parentNode.className = 'completed';
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}));
总结
首先我们想写好 JS 的前提下一定要考虑到 “各司其职” 这个原则,能使用 css 来完成的功能,一定不要使用 JS 来完成。组件封装思想,这个思想在软件开发当中应用广泛,通过封装,将一些公用的逻辑以及样式等等进行抽离,暴露出这些公共的一些属性等,方便日后维护和拓展。过程抽象化,将一些公共的逻辑进行抽离,封装成一个个独立于项目之外的功能,这个过程称之为过程抽象。