是月影老师主讲的课,受益匪浅,特此记录
写好JS的一些原则
各司其责
HTML描述结构,CSS描述展现形式,JS描述行为
并不是单指代码位置上的分离,而是指本身承担的职责
应当避免不必要的由JS直接操作样式,可以用class来表示状态,纯展示类交互寻求零JS方案
例:写一段JS,控制网页切换深色模式
方案1. 在JS中直接修改样式(不便于后期维护)
方案2. 在JS中修改class名,而具体class内容的实现交给CSS(依然耦合)
方案3. 修改HTML结构,利用纯CSS实现。使用checkbox状态选择展示夜间或白天样式
组件封装
组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元
好的组件具备封装性、正确性、扩展性、复用性
例:封装一个轮播图
初步想法
- 结构设计:HTML
轮播图是一个典型的列表结构,可以使用
- 实现
<div>
<ul>
<li class="slider-list__item--selected"></li>
<li class="slider-list__item"></li>
<li class="slider-list__item"></li>
</ul>
</div>
- 表现:CSS
使用CSS绝对定位将图片重叠在同一位置
轮播图切换的状态使用修饰符(modifier)
轮播图的切换动画使用CSS transition
- JS
API设计应保证原子操作,职责单一,满足灵活性
一个Slider类可以含有以下API:
- getSelectedItem()
- getSelectedItemIndex()
- slideTo()
- slideNext()
- slidePrevious()
- start()
- end()
处理状态耦合
解耦的过程,就是将控制元素抽取成插件,插件与组件之间通过依赖注入
的方式建立联系。
插件化
轮播图有时会由一排controller控制播放切换,但controller并非必须的,而且可能会存在形式的不同(如可以水平、竖直排放;或者对样式进行调整)因此对controller的设计是需要被解耦的。此时我们可以利用自定义事件:如果存在controller时,令其监听scroll
事件,并实现相关控制api。这样轮播图的核心功能不会依赖controller,使controller可以被简单插拔。
一般希望代码行数不超过14行,最多20行,当超出这个范围,可能就需要考虑重构代码。
插件化之后的JS示例如下:
class Slider{
constructor(id, cycle = 3000){
this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item--selected','.slider-list__item--selected')
this.cycle = cycle;
}
// 不让slider知道插件的存在,于是通过依赖注入的方式注册插件。遍历运行插件构造的方法
// 这样插件就从实例中独立出来
registerPlugins(...plugins){
plugins.forEach(plugin => plugin(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';
}
const item = this.items[idx];
if(item){
item.className="slider-list__item--selected"
}
}
slideNext(){
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (this.items.length+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)
}
}
// 实现插件
// 实现一排controller
function pluginController(slider){
const controller = slider.container.querySelector('.slider-list__item')
if(controller){
const buttons = controller.querySelectorAll('.slide-list__controller,.slide-list__controller--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__controller');
if(selected){
selected.className="slide-list__controller"
}
buttons[idx].className="slide-list__controller--selected"
})
}
}
// 实例化slider
const slider = new Slider('my-slider')
// 注册插件
slider.registerPlugins(pluginController)
// 启动slider
slider.start()
HTML模板化
之前只是解耦了JS代码,但是HTML的内容还需要手动删除。此时可以用JS模板化渲染HTML。
class Slider{
constructor(id,opts={images:[],cycle:3000}){
// 初始化
}
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>`
)
}
}
过程抽象
将通用的组件模型抽象出来,用来处理局部细节控制的一些方法。是函数式编程思想的基础应用。
比如slider中,可以看到插件和slider类都有render方法,用于初始化HTML模板。因此可以抽象出一个Component类,将registerPlugin和render方法抽象出来,然后让slider类和插件都继承这个Component类,再交由具体类重写render方法
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){
// 抽象方法,交由具体类实现
return ''
}
}
总结
组件设计的原则:封装性、正确性、扩展性、复用性
实现组建的步骤:结构设计、展现效果、行为设计
三次重构:插件化、模板化、抽象化(组件框架)