SYSTEM: ONLINE
VER. ...
SLEET LOG
SLEET'S LOG
/2022年1月16日/2 MIN READ

【字节青训营记录】如何写好JS(上)

JavaScript 字节青训营

写好JS的一些原则

各司其责

HTML描述结构,CSS描述展现形式,JS描述行为

并不是单指代码位置上的分离,而是指本身承担的职责

应当避免不必要的由JS直接操作样式,可以用class来表示状态,纯展示类交互寻求零JS方案

例:写一段JS,控制网页切换深色模式

方案1. 在JS中直接修改样式(不便于后期维护) 方案2. 在JS中修改class名,而具体class内容的实现交给CSS(依然耦合) 方案3. 修改HTML结构,利用纯CSS实现。使用checkbox状态选择展示夜间或白天样式

组件封装

组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元

好的组件具备封装性、正确性、扩展性、复用性

例:封装一个轮播图

初步想法
  1. 结构设计: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>
  1. 表现:CSS

使用CSS绝对定位将图片重叠在同一位置

轮播图切换的状态使用修饰符(modifier)

轮播图的切换动画使用CSS transition

  1. JS

API设计应保证原子操作,职责单一,满足灵活性

一个Slider类可以含有以下API:

  • getSelectedItem()
  • getSelectedItemIndex()
  • slideTo()
  • slideNext()
  • slidePrevious()
  • start()
  • end()
处理状态耦合

解耦的过程,就是将控制元素抽取成插件,插件与组件之间通过依赖注入的方式建立联系。

插件化

轮播图有时会由一排controller控制播放切换,但controller并非必须的,而且可能会存在形式的不同(如可以水平、竖直排放;或者对样式进行调整)因此对controller的设计是需要被解耦的。此时我们可以利用自定义事件:如果存在controller时,令其监听scroll事件,并实现相关控制api。这样轮播图的核心功能不会依赖controller,使controller可以被简单插拔。

一般希望代码行数不超过14行,最多20行,当超出这个范围,可能就需要考虑重构代码。

插件化之后的JS示例如下:

JavaScript
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。

JavaScript
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方法

JavaScript
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 '' } }

总结

组件设计的原则:封装性、正确性、扩展性、复用性

实现组建的步骤:结构设计、展现效果、行为设计

三次重构:插件化、模板化、抽象化(组件框架)

Article Index