Web前端UI组件设计

注:内容来自奇舞学院前端星课程

一、如何UI组件设计

组件的设计分三个步骤,如下:

  1. 结构设计:分析组件UI的布局,用相应的dom元素设计html结构
  2. API设计:设计JavaScript API来实现交互效果
  3. 控制流设计:为用户提供交互所触发的事件。组件中的控制流可作为插件抽象出来,并模板化。

二、轮播图组件具体设计

接下来以轮播图组件的设计过程具体讲解组件设计过程。

步骤1. 结构设计

  1. 图片结构是一个列表型结构,所以主体用 <ul>,图片放<li>
  2. 使用 css 绝对定位将图片重叠在同一个位置
  3. 轮播图切换的状态使用修饰符(modifier),用挂类名的方式修改状态对应样式
  4. 轮播图的切换动画使用 css transition

HTML代码

<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>
</div>

css代码

#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; /* 绝对定位使列表项目重叠在一起 */
  transition: opacity 1s;
  opacity: 0; /* 默认透明隐藏元素 */
  text-align: center;
}
.slider-list__item--selected{
  transition: opacity 1s;
  opacity: 1; /* 选中时显示元素 */
}

步骤2.API设计

  • 轮播图组件的API设计还是比较简单,用户的交互主要是手动切换轮播图,而切换轮播图的过程需要获取当前选中的选项元素以及其索引。轮播图组件的API设计如下类图。
    轮播图组件的类图
  • 具体实现:
    使用ES6的class,用面向对象的方式实现。
 class Slider {
    constructor(id) { // 构造函数的参数id为要轮播图组件的id,初始化container和items属性
      // 获取轮播图的容器元素
      this.container = document.getElementById(id)
      // 获取轮播图中的图片列表
      this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected')
    }
    /* Slider类的方法 */
    getSelectedItem () {
      const selectedItem = this.container.querySelector('.slider-list__item--selected')
      return selectedItem
    }
    getSelectedItemIndex () {
      const selectedItem = this.container.querySelector('.slider-list__item--selected')
      // this.items的类型是类数组对象,需要转换为数组类型
      return Array.from(this.items).indexOf(selectedItem)
    }
    slideTo (index) {
      const selectedItem = this.getSelectedItem()
      if(selectedItem) selectedItem.className = 'slider-list__item'
      const idx = index%this.items.length
      if(this.items[idx]) this.items[idx].className = 'slider-list__item--selected'
    }
    slideNext () {
      const currentId = this.getSelectedItemIndex()
      this.slideTo(currentId + 1)
    }
    slidePrevious () {
      const currentId = this.getSelectedItemIndex()
      this.slideTo(this.tems.length + currentId - 1)
    }
  }
  // 实例化对象
  const slider = new Slider('my-slider')
  // 自动轮播
  setInterval(() => {
    slider.slideNext()
  }, 3000);

步骤3.控制流设计

首先要添加控制结构,即在html和css中添加轮播图的上下切换按钮和控制条。然后在组件构造函数中给控制结构添加相应事件处理。
添加控制流版本的轮播图代码如下:

代码中的修改:html中加入按钮和控制条并给相应的样式,Slider类构造函数中给控制条添加悬浮事件切换选项和上下切换按钮添加点击事件,Slider类加了两个函数startstop来控制是否自动轮播效果。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>轮播图</title>
    <style>
      #my-slider{
        position: relative;
        width: 790px;
        height: 340px;
      }
      .slider-list ul{
        list-style-type:none;
        position: relative;
        padding: 0;
        margin: 0;
      }
      .slider-list__item,
      .slider-list__item--selected{
        position: absolute; /* 绝对定位使列表项目重叠在一起 */
        transition: opacity 1s;
        opacity: 0; /* 默认透明隐藏元素 */
        text-align: center;
      }
      .slider-list__item--selected{
        transition: opacity 1s;
        opacity: 1; /* 选中时显示元素 */
      }
      /* 上下切换按钮 */
      .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{
        position: relative;
        display: table;
        background-color: rgba(255, 255, 255, 0.5);
        padding: 5px;
        border-radius: 12px;
        top: 280px;
        margin: auto;
      }
      .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;
      }

    </style>
</head>
<body>
  <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>
  <script>
  class Slider {
    constructor(id, cycle = 3000) { // 构造函数的参数id为要轮播图组件的id,cycle为轮播周期
      this.cycle = cycle
      // 获取轮播图的容器元素
      this.container = document.getElementById(id)
      // 获取轮播图中的图片列表
      this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected')
      // 添加控制流
      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', event => {
          const idx = Array.from(buttons).indexOf(event.target)
          if (idx >= 0) {
            this.slideTo(idx)
            this.stop()
          }
        })
        controller.addEventListener('mouseout', event => {
          this.start()
        })
        // 滑动时修改控制条选中样式
        this.container.addEventListener('slide', event => {
          const idx = event.detail.index
          console.log('索引', idx, buttons)
          const selectedItem = controller.querySelector('.slide-list__control-buttons--selected')
          if(selectedItem) selectedItem.className = 'slide-list__control-buttons'
          if(buttons[idx]) 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()
        });
      }
    }
    /* Slider类的方法 */
    getSelectedItem () {
      const selectedItem = this.container.querySelector('.slider-list__item--selected')
      return selectedItem
    }
    getSelectedItemIndex () {
      const selectedItem = this.container.querySelector('.slider-list__item--selected')
      // this.items的类型是类数组对象,需要转换为数组类型
      return Array.from(this.items).indexOf(selectedItem)
    }
    slideTo (index) {
      const selectedItem = this.getSelectedItem()
      if(selectedItem) selectedItem.className = 'slider-list__item'
      const idx = index%this.items.length
      if(this.items[idx]) this.items[idx].className = 'slider-list__item--selected'
      // 触发滑动事件
      const event = new CustomEvent('slide', 
        {
          bubbles:true, 
          detail: {index: idx}
        }
      )
      this.container.dispatchEvent(event)
    }
    slideNext () {
      const currentId = this.getSelectedItemIndex()
      this.slideTo(currentId + 1)
    }
    slidePrevious () {
      const currentId = this.getSelectedItemIndex()
      this.slideTo(this.items.length + currentId - 1)
    }
    start () {
      this.stop()
      this._timer = setInterval(() => {
        this.slideNext()
      }, this.cycle);
    }
    stop () {
      if(this._timer) clearInterval(this._timer)
    }
  }
  // 实例化对象
  const slider = new Slider('my-slider')
  // 自动轮播
  slider.start()
  </script>
</body>
</html>

这个版本的轮播图已经实现了该有的功能,但是代码还需要进行优化,将组件中的一些东西抽象出来。在Slider组件的切换按钮和控制条都可以抽象出来作为插件,然后插件将Slider组件对象作为依赖注入,降低控制流插件与组件的耦合度。
在这里插入图片描述
js代码中在组件的构造函数中添加的控制流抽离出来作为注册插件函数,组件对象作为参数传入注册插件函数。
修改后的js代码如下:

class Slider {
    constructor(id, cycle = 3000) { // 构造函数的参数id为要轮播图组件的id,cycle为轮播周期
      this.cycle = cycle
      // 获取轮播图的容器元素
      this.container = document.getElementById(id)
      // 获取轮播图中的图片列表
      this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected')
    }
    /* Slider类的方法 */
    registerPlugins (...plugins) { // 引入插件函数
      plugins.forEach(plugin => plugin(this))
    }
    getSelectedItem () {
      const selectedItem = this.container.querySelector('.slider-list__item--selected')
      return selectedItem
    }
    getSelectedItemIndex () {
      const selectedItem = this.container.querySelector('.slider-list__item--selected')
      // this.items的类型是类数组对象,需要转换为数组类型
      return Array.from(this.items).indexOf(selectedItem)
    }
    slideTo (index) {
      const selectedItem = this.getSelectedItem()
      if(selectedItem) selectedItem.className = 'slider-list__item'
      const idx = index%this.items.length
      if(this.items[idx]) this.items[idx].className = 'slider-list__item--selected'
      // 触发滑动事件
      const event = new CustomEvent('slide', 
        {
          bubbles:true, 
          detail: {index: idx}
        }
      )
      this.container.dispatchEvent(event)
    }
    slideNext () {
      const currentId = this.getSelectedItemIndex()
      this.slideTo(currentId + 1)
    }
    slidePrevious () {
      const currentId = this.getSelectedItemIndex()
      this.slideTo(this.items.length + currentId - 1)
    }
    start () {
      this.stop()
      this._timer = setInterval(() => {
        this.slideNext()
      }, this.cycle);
    }
    stop () {
      if(this._timer) clearInterval(this._timer)
    }
  }
  /* 抽象出来的插件函数 */ 
  // 控制条
  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', event => {
        const idx = Array.from(buttons).indexOf(event.target)
        if (idx >= 0) {
        slider.slideTo(idx)
        slider.stop()
        }
      })
      controller.addEventListener('mouseout', event => {
        slider.start()
      })
      // 滑动时修改控制条选中样式
      slider.container.addEventListener('slide', event => {
        const idx = event.detail.index
        console.log('索引', idx, buttons)
        const selectedItem = controller.querySelector('.slide-list__control-buttons--selected')
        if(selectedItem) selectedItem.className = 'slide-list__control-buttons'
        if(buttons[idx]) 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()

经过js的修改将控制流作为插件引入,但是这样插件的是否引入对html结构并没有影响。所以这里还需要改进插件,将组件和插件模板化。这里需要再设计了组件和插件API(如下图),在插件中加入了renderaction方法:render方法根据传入render方法的数据data来构造html结构;action方法根据传入的组件对象component作为依赖注入,从而给组件添加事件和行为。
在这里插入图片描述
具体实现:

  • html代码:
<div id="my-slider" class="slider-list"></div>
  • js代码:
  class Slider {
    constructor(id, options={images: [], cycle: 3000}) { 
    // 构造函数的参数id为要轮播图组件的id,options为参数
      // 获取轮播图的容器元素
      this.container = document.getElementById(id)
      // 获取渲染组件的参数
      this.options = options
      this.container.innerHTML = this.render()
      // 获取轮播图中的图片列表
      this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected')
      this.cycle = options.cycle || 3000
      this.slideTo(0)
    }
    /* Slider类的函数 */
    // 渲染html结构
    render () {
      const images = this.options.images
      const content = images.map(item => `
        <li class="slider-list__item">
          <img src="${item}"/>
        </li> 
      `.trim())
      return `<ul>${content.join('')}</ul>`
    }
    // 引入插件函数
    registerPlugins (...plugins) { 
      plugins.forEach(plugin => {
        const pluginContainer = document.createElement('div')
        pluginContainer.className = '.slider-list__plugin'
        // 渲染插件html结构
        pluginContainer.innerHTML = plugin.render(this.options.images)
        this.container.appendChild(pluginContainer)
        // 添加事件和行为
        plugin.action(this)
      })
    }
 	/*
 	......
 	此处省略了未改动的函数
	*/
  }
  /* 抽象出来的插件函数 */ 
  // 控制条
  const pluginController = {
    render (data) {
      return `
        <div class="slide-list__control">
          ${data.map(item=>`
            <span class="slide-list__control-buttons"></span>
          `).join('')}
        </div>
      `.trim()
    },
    action (component) {
      // 添加控制流
      const controller = component.container.querySelector('.slide-list__control')
      // 控制轮播图选中控制条显示对应图
      if (controller) {
        const buttons = controller.querySelectorAll('.slide-list__control-buttons,.slide-list__control-buttons--selected')
        controller.addEventListener('mouseover', event => {
          const idx = Array.from(buttons).indexOf(event.target)
          if (idx >= 0) {
          component.slideTo(idx)
          component.stop()
          }
        })
        controller.addEventListener('mouseout', event => {
          component.start()
        })
        // 滑动时修改控制条选中样式
        component.container.addEventListener('slide', event => {
          const idx = event.detail.index
          console.log('索引', idx, buttons)
          const selectedItem = controller.querySelector('.slide-list__control-buttons--selected')
          if(selectedItem) selectedItem.className = 'slide-list__control-buttons'
          if(buttons[idx]) buttons[idx].className = 'slide-list__control-buttons--selected'
        })
      }
    }
  }
  // 上一页按钮
  const pluginPrevious = {
    render (data) {
      return `
        <a class="slide-list__previous"></a>
      `.trim()
    },
    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 pluginNext = {
    render (data) {
      return `
        <a class="slide-list__next"></a>
      `.trim()
    },
    action (slider) {
      const next = slider.container.querySelector('.slide-list__next');
      if(next){
        next.addEventListener('click', evt => {
          slider.stop();
          slider.slideNext();
          slider.start();
          evt.preventDefault();
        })
      }  
    }
  }
  const 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'
  ]
  // 实例化对象
  const slider = new Slider('my-slider', {images})
  // 自动轮播
  slider.registerPlugins(pluginController, pluginPrevious, pluginNext)
  slider.start()

现在将插件模板化后,当不引用某个插件时界面上也不显示了。现在通过组件模型抽象来进一步改进,其类图结构如下。
在这里插入图片描述
具体实现:

  class Component {
    constructor(id, options = {data:[]}) {
      this.container = document.getElementById(id)
      this.options = options
      this.container.innerHTML = this.render(options.data)
    }
    registerPlugins(...plugins) {
      plugins.forEach(plugin => {
        const pluginContainer = document.createElement('div')
        pluginContainer.className = '.slider-list__plugin'
        // 渲染插件html结构
        pluginContainer.innerHTML = plugin.render(this.options.data)
        this.container.appendChild(pluginContainer)
        // 添加事件和行为
        plugin.action(this)
      })
    }
    render(data) {
      /* 抽象函数 */
      return ''
    }
  }
  class Slider extends Component {
    constructor(id, options={name: 'slider-list',data: [], cycle: 3000}) { 
      super(id,options)
      // 获取轮播图中的图片列表
      this.items = this.container.querySelectorAll('.slider-list__item,.slider-list__item--selected')
      this.cycle = options.cycle || 3000
      this.slideTo(0)
    }
    /* Slider类的方法 */
    // 渲染html结构
    render (data) {
      const content = data.map(item => `
        <li class="slider-list__item">
          <img src="${item}"/>
        </li> 
      `.trim())
      return `<ul>${content.join('')}</ul>`
    }
 	/*
 	......
 	此处省略了未改动的函数
	*/
  }
  /* 抽象出来的插件函数 */ 
  // 控制条
  const pluginController = {
    render (data) {
      return `
        <div class="slide-list__control">
          ${data.map(item=>`
            <span class="slide-list__control-buttons"></span>
          `).join('')}
        </div>
      `.trim()
    },
    action (component) {
      // 添加控制流
      const controller = component.container.querySelector('.slide-list__control')
      // 控制轮播图选中控制条显示对应图
      if (controller) {
        const buttons = controller.querySelectorAll('.slide-list__control-buttons,.slide-list__control-buttons--selected')
        controller.addEventListener('mouseover', event => {
          const idx = Array.from(buttons).indexOf(event.target)
          if (idx >= 0) {
          component.slideTo(idx)
          component.stop()
          }
        })
        controller.addEventListener('mouseout', event => {
          component.start()
        })
        // 滑动时修改控制条选中样式
        component.container.addEventListener('slide', event => {
          const idx = event.detail.index
          console.log('索引', idx, buttons)
          const selectedItem = controller.querySelector('.slide-list__control-buttons--selected')
          if(selectedItem) selectedItem.className = 'slide-list__control-buttons'
          if(buttons[idx]) buttons[idx].className = 'slide-list__control-buttons--selected'
        })
      }
    }
  }
  // 上一页按钮
  const pluginPrevious = {
    render (data) {
      return `
        <a class="slide-list__previous"></a>
      `.trim()
    },
    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 pluginNext = {
    render (data) {
      return `
        <a class="slide-list__next"></a>
      `.trim()
    },
    action (slider) {
      const next = slider.container.querySelector('.slide-list__next');
      if(next){
        next.addEventListener('click', evt => {
          slider.stop();
          slider.slideNext();
          slider.start();
          evt.preventDefault();
        })
      }  
    }
  }
  const 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'
  ]
  // 实例化对象
  const slider = new Slider('my-slider', {data:images})
  // 自动轮播
  slider.registerPlugins(pluginController, pluginPrevious, pluginNext)
  slider.start()
  </script>

通过一步步地拆解和抽象出来,减少他们之间的依赖关系,无论是组件还是插件都能独立出来。当我们的界面交互发生一小部分变化时我们只需要去修改所涉及到的插件就行,而不需要去修改整个组件代码结构,增加代码的可维护性。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值