Web前端UI组件设计

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

一、如何UI组件设计

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

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

二、轮播图组件具体设计

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

步骤1. 结构设计

  1. 图片结构是一个列表型结构,所以主体用
      ,图片放
    • 使用 css 绝对定位将图片重叠在同一个位置
    • 轮播图切换的状态使用修饰符(modifier),用挂类名的方式修改状态对应样式
    • 轮播图的切换动画使用 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设计如下类图。
    Web前端UI组件设计_第1张图片
  • 具体实现:
    使用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来控制是否自动轮播效果。


<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组件对象作为依赖注入,降低控制流插件与组件的耦合度。
Web前端UI组件设计_第2张图片
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作为依赖注入,从而给组件添加事件和行为。
Web前端UI组件设计_第3张图片
具体实现:

  • 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 => `
        
  • ${item}"/>
  • `
    .trim()) return `
      ${content.join('')}
    `
    } // 引入插件函数 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 `
    ${data.map(item=>` `).join('')}
    `
    .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 ` `.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 ` `.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()

    现在将插件模板化后,当不引用某个插件时界面上也不显示了。现在通过组件模型抽象来进一步改进,其类图结构如下。
    Web前端UI组件设计_第4张图片
    具体实现:

      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 => `
            
  • ${item}"/>
  • `
    .trim()) return `
      ${content.join('')}
    `
    } /* ...... 此处省略了未改动的函数 */ } /* 抽象出来的插件函数 */ // 控制条 const pluginController = { render (data) { return `
    ${data.map(item=>` `).join('')}
    `
    .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 ` `.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 ` `.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>

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

    你可能感兴趣的:(JavaScript)