以轮播效果为案例谈如何写优质代码

更新日期: 2019-06-28阅读: 1.8k标签: 效果
来自:https://zhuanlan.zhihu.com/p/71203445

作为程序员大家在写代码时谈的最多的就是代码的拓展性、复用性。本文就以大家熟悉的轮播效果为案例,讲一讲写优质代码的思路和实践。

文章分三个步骤。第一步,实现基本功能;第二步,考虑到代码的封装性和复用性;第三步,考虑到代码的拓展性。


实现基本功能

完整代码查看这里 ,下面只展示html结构和JavaScript

    <div class="carousel">
      <div class="panels">
        <a href="#">
          <img src="http://cdn.jirengu.com/book.jirengu.com/img/1.jpg">
        </a>
        <a href="#">
          <img src="http://cdn.jirengu.com/book.jirengu.com/img/2.jpg">
        </a>
        <a href="#">
          <img src="http://cdn.jirengu.com/book.jirengu.com/img/3.jpg">
        </a>
        <a href="#">
          <img src="http://cdn.jirengu.com/book.jirengu.com/img/4.jpg">
        </a>
      </div>
      <div class="action">
        <span class="pre">上一个</span>
        <span class="next">下一个</span>
        <div class="dots">
          <span class="active"></span>
          <span></span>
          <span></span>
          <span></span>
        </div>
      </div>
    </div>

JavaScript

//让document.querySelector 用起来更方便      
const $ = s => document.querySelector(s) 
const $$ = s => document.querySelectorAll(s)

const dotCt = $('.carousel .dots')
const preBtn = $('.carousel .pre')
const nextBtn = $('.carousel .next')

//把类数组对象转换为数组,便于之后使用数组方法
//这里对应的是包含图片面板的数组
const panels = Array.from($$('.carousel .panels > a')) 
//这里对应的是包含小圆点的数组
const dots = Array.from($$('.carousel .dots span'))

//要展示第几页,就先把所有页的z-index设置为0,再把要展示的页面z-index设置为10
const showPage = pageIndex => {
  panels.forEach(panel => panel.style.zIndex = 0)
  panels[pageIndex].style.zIndex = 10
}

const setDots = pageIndex => {
  dots.forEach(dot => dot.classList.remove('active'))
  dots[pageIndex].classList.add('active')   
}

//根据第几个小点上有active的类来判断在第几页
const getIndex = () => dots.indexOf($('.carousel .dots .active'))
const getPreIndex = () => (getIndex() - 1 + dots.length) % dots.length
const getNextIndex = () => (getIndex() + 1) % dots.length

dotCt.onclick = e => {
  if(e.target.tagName !== 'SPAN') return

  let index = dots.indexOf(e.target)
  setDots(index)
  showPage(index)
}

preBtn.onclick = e => {
  let index = getPreIndex()
  setDots(index)
  showPage(index)
}

nextBtn.onclick = e => {
  let index = getNextIndex()
  setDots(index)
  showPage(index)
}

这里查看代码效果 。上面的代码使用了原生ES6语法,核心代码逻辑是:当用户点击小圆点,得到小圆点的位置(index),设置小圆点集合的样式,切换到对应页面。页面(.panels的子元素)使用绝对定位相互重叠到一起,我们通过修改z-index把需要展示的页面放到最上层。


复用性与封装性

以上代码可以实现轮播基本功能,但做为意大利面条式的代码,并未做封装,无法给他人使用。另外也无法满足页面上有多个轮播的需求。

下面对代码做个封装。

class Carousel {
  constructor(root) {
    this.root = root
    this.panels = Array.from(root.querySelectorAll('.panels a'))
    this.dotCt = root.querySelector('.dots')
    this.dots = Array.from(root.querySelectorAll('.dots span'))
    this.pre = root.querySelector('.pre')
    this.next = root.querySelector('.next')

    this.bind()
  }

  get index() {
    return this.dots.indexOf(this.root.querySelector('.dots .active'))
  }

  get preIndex() {
    return (this.index - 1 + this.dots.length) % this.dots.length
  }

  get nextIndex () {
    return (this.index + 1) % this.dots.length
  }

  bind() {
    this.dotCt.onclick = e => {
      if(e.target.tagName !== 'SPAN') return

      let index = this.dots.indexOf(e.target)
      this.setDot(index)
      this.showPage(index) 
    }

    this.pre.onclick = e => {
      let index = this.preIndex
      this.setDot(index)
      this.showPage(index)  
    }

    this.next.onclick = e => {
      let index = this.nextIndex
      this.setDot(index)
      this.showPage(index)  
    }
  }

  setDot(index) {
    this.dots.forEach(dot => dot.classList.remove('active'))
    this.dots[index].classList.add('active')
  }

  showPage(index) {
    this.panels.forEach(panel => panel.style.zIndex = 0)
    this.panels[index].style.zIndex = 10
  }
}

new Carousel(document.querySelector('.carousel'))

代码里用了getter,便于或者index的值。这里需要注意的是,每次调用setDot后 this.index、this.preIndex、this.nextIndex均会自动发生变化,调用showPage的时候需要留意。

现在轮播可以复用了,但仍有缺憾,轮播的效果太单调。假设轮播想使用fade或者slide效果,我们可以在showPage方法内修改代码。但存在的问题是效果和轮播组件做了强绑定,假设我需要另外一个效果的轮播就得新建一个组件。比如,有这样一个需求,用户可以再切页时可以随时更改效果,用上面的代码就很难办到。

能不能实现组件和效果的解绑呢?当然可以。


代码拓展性

设计模式中的桥接模式可以实现上述的分离。直接给代码

class Carousel {
  constructor(root, animation) {
    this.animation = animation || ((to, from, onFinish) => onFinish())
    this.root = root
    ...
  }

  ...

  //showPage传递2个参数,toIndex 表示要切换到的页面(终点页面)序号,fromIndex 表示从哪个页面(起始页面)切换过来
  showPage(toIndex, fromIndex) {
    //animation函数传递3个参数,分别为终点页面dom元素,起始页面dom元素,动画执行完毕后的回调
    this.animation(this.panels[toIndex], this.panels[fromIndex], () => {
      //这里是动画执行完成后的回调          
    })
  }
}

const Animation = {
  fade(during) {
    return function(to, from, onFinish) {
      //to表示终点页面dom元素,from表示起始页面dom元素
      //对这两个元素进行适当的处理即可实现平滑过渡效果
      ...
    }
  },

  zoom(scale) {
    return function(to, from, onFinish) { /*todo...*/}
  }

}

new Carousel(document.querySelector('.carousel'), Animation.fade(300))

上述代码中,我们把动画类型作为参数传递给Carousel,在执行setPage的时候调用动画。 而动画函数本身做的事情比较简单:处理两个绝对定位并且相互重叠的DOM元素,以特定效果让一个元素消失另外一个元素出现。


动画的实现

动画可以用JS来实现(requestAnimationFrame来实现动画),也可以用css3来实现。相比JS实现动画,用CSS3性能更好并且代码更简单。

const Animation = (function(){
  const css = (node, styles) => Object.entries(styles)
  .forEach(([key, value]) => node.style[key] = value)
  const reset = node => node.style = ''

  return {
    fade(during = 400) {
      return function(to, from, onFinish) {
        css(from, {
          opacity: 1,
          transition: `all ${during/1000}s`,
          zIndex: 10
        })
        css(to, {
          opacity: 0,
          transition: `all ${during/1000}s`,
          zIndex: 9
        })

        setTimeout(() => {
          css(from, {
            opacity: 0,
          })
          css(to, {
            opacity: 1,
          })              
        }, 100)

        setTimeout(() => {
          reset(from)
          reset(to)
          onFinish && onFinish()
        }, during)

      }
    },

    zoom(scale = 5, during = 600) {
      return function(to, from, onFinish) {
        css(from, {
          opacity: 1,
          transform: `scale(1)`,
          transition: `all ${during/1000}s`,
          zIndex: 10
        })
        css(to, {
          zIndex: 9
        })

        setTimeout(() => {
          css(from, {
            opacity: 0,
            transform: `scale(${scale})`
          })             
        }, 100)

        setTimeout(() => {
          reset(from)
          reset(to)
          onFinish && onFinish()
        }, during)

      }
    }
  }
})()


以下是最终代码,大家可以再Animation对象里增加更多特效,比如把前段时间流行的灭霸特效加进去。

class Carousel {
  constructor(root, animation) {
    this.animation = animation || ((to, from, onFinish) => onFinish())
    this.root = root
    this.panels = Array.from(root.querySelectorAll('.panels a'))
    this.dotCt = root.querySelector('.dots')
    this.dots = Array.from(root.querySelectorAll('.dots span'))
    this.pre = root.querySelector('.pre')
    this.next = root.querySelector('.next')

    this.bind()
  }

  get index() {
    return this.dots.indexOf(this.root.querySelector('.dots .active'))
  }

  get preIndex() {
    return (this.index - 1 + this.dots.length) % this.dots.length
  }

  get nextIndex () {
    return (this.index + 1) % this.dots.length
  }

  bind() {
    this.dotCt.onclick = e => {
      if(e.target.tagName !== 'SPAN') return

      let lastIndex = this.index
      let index = this.dots.indexOf(e.target)
      this.setDot(index)
      this.showPage(index, lastIndex) 
    }

    this.pre.onclick = e => {
      let index = this.preIndex
      this.setDot(index)
      this.showPage(index, this.nextIndex)  
    }

    this.next.onclick = e => {
      let index = this.nextIndex
      this.setDot(index)
      this.showPage(index, this.preIndex)  
    }
  }

  setDot(index) {
    this.dots.forEach(dot => dot.classList.remove('active'))
    this.dots[index].classList.add('active')
  }

  showPage(toIndex, fromIndex) {
    //执行动画,执行完成后最终结果
    //如果没传递动画,直接执行结果
    this.animation(this.panels[toIndex], this.panels[fromIndex], () => {
      this.panels.forEach(panel => panel.style.zIndex = 0)
      this.panels[toIndex].style.zIndex = 10            
    })
  }

  setAnimation(animation) {
    this.animation = animation
  }
}

const Animation = (function(){
  const css = (node, styles) => Object.entries(styles)
  .forEach(([key, value]) => node.style[key] = value)
  const reset = node => node.style = ''

  return {
    fade(during) {
      return function(to, from, onFinish) {
        css(from, {
          opacity: 1,
          transition: `all ${during/1000}s`,
          zIndex: 10
        })
        css(to, {
          opacity: 0,
          transition: `all ${during/1000}s`,
          zIndex: 9
        })

        setTimeout(() => {
          css(from, {
            opacity: 0,
          })
          css(to, {
            opacity: 1,
          })              
        }, 100)

        setTimeout(() => {
          reset(from)
          reset(to)
          onFinish && onFinish()
        }, during)

      }
    },

    zoom(scale = 5, during = 1000) {
      return function(to, from, onFinish) {
        css(from, {
          opacity: 1,
          transform: `scale(1)`,
          transition: `all ${during/1000}s`,
          zIndex: 10
        })
        css(to, {
          zIndex: 9
        })

        setTimeout(() => {
          css(from, {
            opacity: 0,
            transform: `scale(${scale})`
          })             
        }, 100)

        setTimeout(() => {
          reset(from)
          reset(to)
          onFinish && onFinish()
        }, during)

      }
    }
  }
})()


const carousel = new Carousel(document.querySelector('.carousel'), Animation.fade(300))
//new Carousel(document.querySelector('.carousel'), Animation.zoom(3, 500))

document.querySelector('select').onchange = function(e) {
  carousel.setAnimation(Animation[this.value]())
}

查看效果 查看源码

咨询前端系统班课程加课程小助手微信: hungervalley , 或者 astak10。或者点此扫描qr加好友,或者在这里直接咨询。


链接: https://fly63.com/article/detial/3920

使用 JavaScript 实现分屏视觉效果

今天这篇文章就来讲讲使用JavaScript来实现这种分屏的视觉UI效果。现在在网站上这种分屏视觉效果应用的也非常广泛,比如 Corsair website。

CSS3的过渡效果,使用transition实现鼠标移入/移出效果

在css中使用伪类虽然实现了样式的改变,但由于没有过渡效果会显得很生硬。以前如果要实现过渡,就需要借助第三方的js框架来实现。现在只需要使用CSS3的过渡(transition)功能,就可以从一组样式平滑的切换到另一组样式。

js如何实现新手引导效果?

js最近有个小伙伴问到了怎么实现新手引导的效果,然后便去网上找了下实现方案。可以通过css的border来实现。

css3 斜切角/斜边的实现方式

设计图含有斜切角的效果时,我们一般想到的方法是切出四个角为背景,然后用border连起来,这样就能显示出该效果了,那么直接使用css呢?下面就整理css做斜边的效果。

JavaScript 实现打字机效果,跑马灯效果

这篇文章在不使用任何插件的情况,以最简洁的原生javascript来实现打字机效果和跑马灯效果。打字效果即把一段话一个字一个字的显示出来。

CSS遮罩效果(模糊效果,阴影效果,毛玻璃效果)

一般遮罩加上透明度opacity就是阴影效果。阴影效果和一般遮罩一样,唯一不同的是设置.mask遮罩的背景色用rgba()表示,当然hsla()也是可以的。模糊效果(毛玻璃效果) 通过 filter来实现

纯css实现气泡效果

主要运用的是1.border 组成的直角三角形。2,before 和 after 伪元素 。3,z-index属性;将元素的长宽设置为0,并且将border的3条边设置为透明的,就会出现border颜色的直角三角形

css文字选中效果

文字选中效果,这个可能很少有人注意过。在默认状态先一般选中的文本颜色是白字蓝底的,不过可以通过CSS进行设置。::selection定义元素上的伪选择器,以便在选定元素时设置其中文本的样式。

text-fill-color:仿苹果官网介绍效果 CSS设置文字渐变效果 文字背景图遮罩

发布iPhone XR的时候 各种心动 去官网看了一遍又一遍。闲着无聊发现 里面的介绍很用大篇幅的有背景文字来介绍。Like this:看着挺酷炫的还不错 就看了下实现方式。还挺简单的。

Vue 中多个元素、组件的过渡,及列表过渡

多元素之间如何实现过渡动画效果呢?这么写行不行呢?肯定是不行的,因为 Vue 在两个元素进行切换的时候,会尽量复用dom,就是因为这个原因,导致现在动画效果不会出现。如果不让 Vue 复用dom的话,应该怎么做呢?只需要给这两个div不同的key值就行了

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!