写好JS的一些原则
各司其责
HTML描述结构,CSS描述展现形式,JS描述行为
并不是单指代码位置上的分离,而是指本身承担的职责
应当避免不必要的由JS直接操作样式,可以用class来表示状态,纯展示类交互寻求零JS方案
例:写一段JS,控制网页切换深色模式
方案1. 在JS中直接修改样式(不便于后期维护) 方案2. 在JS中修改class名,而具体class内容的实现交给CSS(依然耦合) 方案3. 修改HTML结构,利用纯CSS实现。使用checkbox状态选择展示夜间或白天样式
组件封装
组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元
好的组件具备封装性、正确性、扩展性、复用性
例:封装一个轮播图
初步想法
- 结构设计:HTML
轮播图是一个典型的列表结构,可以使用<ul>实现
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示例如下:
JavaScriptclass 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。
JavaScriptclass 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方法
JavaScriptclass 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 '' } }
总结
组件设计的原则:封装性、正确性、扩展性、复用性
实现组建的步骤:结构设计、展现效果、行为设计
三次重构:插件化、模板化、抽象化(组件框架)