这是我参与「第四届青训营 」笔记创作活动的的第5天
写好JS的一些原则
- 各司其责
- 让HTML、CSS、JavaScript职能分离
- 组件封装
- 好的UI组件具备封装性、正确性、扩展性、复用性
- 过程抽象
- 应用函数式编程思想
各司其责 例-深夜食堂
写一段JS,控制一个网页,让它支持浅色和深色两种浏览模式
版本1
点击按钮,如果按钮是'🌞',将背景颜色换成黑色、字体颜色换成白色、按钮图标换成'🌜';如果按钮是'🌜',将背景颜色换成白色、字体颜色换成黑色、按钮图标换成'🌞'。
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 = '🌞';
}
});
版本1的不足之处:使用JS控制CSS,不便于修改需求。
版本2
使用 class 来表示状态。默认class为白天,当点击按钮时,如果为白天,则将className切换为'night';如果为'night',则切换回白天
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
const body = document.body;
if(body.className !== 'night') {
body.className = 'night';
} else {
body.className = '';
}
});
版本3 纯CSS实现
在界面内隐藏一个checkbox,当checkbox被选择后,选择它的兄弟节点content,将content的样式改为深夜模式。
<input id="modeCheckBox" type="checkbox">
<div class="content">
<header>
<label id="modeBtn" for="modeCheckBox"></label>
<!-- for绑定checkbox,实现点击label后达到点击checkbox的效果 -->
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="https://p2.ssl.qhimg.com/t0120cc20854dc91c1e.jpg">
</div>
<div class="description">
<p>
这是一间营业时间从午夜十二点到早上七点的特殊食堂。这里的老板,不太爱说话,却总叫人吃得热泪盈眶。在这里,自卑的舞蹈演员偶遇隐退多年舞界前辈,前辈不惜讲述自己不堪回首的经历不断鼓舞年轻人,最终令其重拾自信;轻言绝交的闺蜜因为吃到共同喜爱的美食,回忆起从前的友谊,重归于好;乐观的绝症患者遇到同命相连的女孩,两人相爱并相互给予力量,陪伴彼此完美地走过了最后一程;一味追求事业成功的白领,在这里结交了真正暖心的朋友,发现真情比成功更有意义。食物、故事、真情,汇聚了整部剧的主题,教会人们坦然面对得失,对生活充满期许和热情。每一个故事背后都饱含深情,情节跌宕起伏,令人流连忘返 [6] 。
</p>
</div>
</main>
</div>
#modeCheckBox {
display: none;
/*隐藏checkbox*/
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
结论:
- HTML/CSS/JS 各司其责
- 应当避免不必要的由 JS 直接操作样式
- 可以用 class 来表示状态
- 纯展示类交互寻求零 JS 方案
组件封装 例-轮播图
组件是指Web页面上抽出来一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性、复用性。
结构设计: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>
<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
- 使用 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;
/*在1秒内过渡到完全透明*/
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 .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;
}
行为设计:JS
- API(功能)
- Slider
- +getSelectedItem()
- +getSelectedItemIndex()
- +slideTo()
- +slideNext()
- +slidePrevious()
- Slider
- 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('.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();
});
this.container.addEventListener('slide', evt => {
//监听slide事件,获取选中图片下标并将相应的圆点置为被选中(变成红色)
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(){ //得到当前所选中的图片
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}
//派发slide事件
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);
}
}
const slider = new Slider('my-slider');
slider.start();
重构
之前的实现方式控制点和组件绑定在一起,不够灵活;且构造函数过于复杂。
插件化
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式建立联系
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){ //将插件注册进plugin
plugins.forEach(plugin => plugin(this));
}
//······同上
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();
});
}
}
const slider = new Slider('my-slider');
//注册插件
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
优势:
- 简化了构造函数。
- 可以更方便的修改需求,如:若不需要底部圆点,只需要将相应的HTML代码删掉后取消注册:
slider.registerPlugins(pluginPrevious, pluginNext);即可实现。- 例-新插件:
<button id="randomGet">手气不错</button>
function pluginRandomGet(slider){
randomGet.addEventListener('click', evt => {
const idx = Math.floor(slider.items.length * Math.random());
slider.stop();
slider.slideTo(idx);
slide.start();
});
}
slider.registerPlugins(pluginRandomGet, pluginController, pluginPrevious, pluginNext);
模板化
插件化的缺陷:取消注册插件后还需要在HTML中删除代码。
- 将HTML模板化,更易于扩展
<div id="my-slider" class="slider-list"></div>
constructor(id, opts = {images:[], cycle: 3000}){
this.container = document.getElementById(id);
this.options = opts;
//将render()返回的innerHTML插入container
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;
//将得到的image生成li
const content = images.map(image => `
<li class="slider-list__item">
<img src="${image}"/>
</li>
`.trim());
//拼接到ul里
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);
});
}
//上一张图片
const pluginPrevious = {
//render()内容渲染
render(){
return `<a class="slide-list__previous"></a>`;
},
//action()初始化
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 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});
优势:
插件变更更加方便灵活,只需取消注册即可完成功能和样式的同步消除。
不足之处:
未将CSS也模板化。
抽象化(组件框架)
- 将组件通用模型抽象出来
- 抽象组件:Component
- registerPlugins(..plugins) 注册插件
- render() 内容渲染
- Slider 继承抽象组件
- SliderPlugin 完成渲染
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 ''
}
}
class Slider extends Component{
//继承后重写render()方法
}
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:3000});
优势:
组件设计简单,每一个组件都是由组件 + 控制插件组成
缺陷:
没有考虑嵌套:将子组件作为父组件的插件使用
过程抽象
- 用来处理局部细节控制的一些方法
- 函数式编程思想的基础应用
例-操作次数限制
- 一些异步交互
- 一次性的HTTP请求
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';
//动画:两秒后remove
setTimeout(() => {
list.removeChild(target.parentNode);
}, 2000);
}));
});
问题描述:当任务还没有删除时,连续点击,执行多次remove,但因为第一次点击时任务已经被移除,导致后面的操作报错:
解决方法:
将过程封装成一个once高阶函数
//第一次调用时,fn != null,返回ret同时将fn设为null;
//第二次调用时,fn == null,方法只会被调用一次
function once(fn) {
return function(...args) {
if(fn) {
const ret = fn.apply(this, args);
fn = null;
return ret;
}
}
}
button.addEventListener('click', once((evt) => {
//······
}));
Once: 为了能够让“只执行一次“的需求覆盖不同的事件处理,我们可以将这个需求剥离出来。这个过程我们称为过程抽象。
高阶函数HOF
- 以函数作为参数
- 以函数作为返回值
- 常用于作为函数装饰器
//等价函数:
function HOF0(fn) { //接受fn函数作为参数
return function(...args) { //返回fn
return fn.apply(this, args);
}
}
常用高阶函数
- Once 操作次数限制
- Throttle 节流函数
- Debounce 防抖函数
- Consumer/2 同步调用变异步调用;延时调用
- Iterative 可迭代方法
为什么要使用高阶函数
高阶函数一般都是纯函数,使用高阶函数可以大大减少使用非纯函数的可能性。纯函数更便于测试,系统可维护性更高。
编程范式
命令式 Imperative
详细的命令机器怎么(How)去处理一件事情以达到你想要的结果(What)
- 面向过程
- 面向对象
声明式 Declarative
只告诉你想要的结果(What),机器自己摸索过程(How)
- 逻辑式
- 函数式
例
- 命令式
switcher.onclick = function(evt){
if(evt.target.className === 'on'){
evt.target.className = 'warn';
}else if(evt.target.className === 'warn'){
evt.target.className = 'off';
}else{
evt.target.className = 'on';
}
}
- 声明式
function toggle(...actions){
return function(...args){
let action = actions.shift();
actions.push(action);
return action.apply(this, args);
}
}
switcher.onclick = toggle(
evt => evt.target.className = 'on',
evt => evt.target.className = 'warn',
evt => evt.target.className = 'off'
);