乾貨--手把手擼vue移動UI框架:側邊菜單

前言

最近面試發現不少前端程序員都歷來沒有寫過插件的經驗,基本上都是網上百度。因此打算寫一系列文章,手把手的教一些沒有寫過組件的兄弟們如何去寫插件。本系列文章都基於VUE,核心內容都同樣,會了以後你們能夠快速的改寫成react、angular或者是小程序等組件。這篇文章是第一篇,寫的是一個相似QQ的側邊菜單組件。javascript

效果展現

先讓你們看個效果展現,知道我們要作的東西是個怎麼樣的樣子,圖片有點模糊,你們先將就點:
圖片描述css

開始製做

DOM結構

總體結構中應該存在兩個容器:1. 菜單容器 2. 主頁面容器;所以當前DOM結構以下:前端

<template>
  <div class="r-slide-menu">
    <div class="r-slide-menu-wrap"></div>
    <div class="r-slide-menu-content"></div>
  </div>
</template>

爲了使得菜單內容和主題內容可以定製,咱們再給兩個容器中加入兩個slot插槽:默認插槽中放置主體內容、菜單放置到menu插槽內:java

<template>
  <div class="r-slide-menu">
    <div class="r-slide-menu-wrap">
      <slot name="menu"></slot>
    </div>
    <div class="r-slide-menu-content">
      <slot></slot>
    </div>
  </div>
</template>

css樣式

我項目中使用了scss,代碼以下:react

<style lang="scss">
@mixin one-screen {
  position: absolute;
  left:0;
  top:0;
  width:100%;
  height:100%;
  overflow: hidden;
}

.r-slide-menu{
  @include one-screen;
  &-wrap, &-content{
    @include one-screen;
  }
  &-transition{
    -webkit-transition: transform .3s;
    transition: transform .3s;
  }
}
</style>

此時咱們就獲得了兩個絕對定位的容器程序員

javascript

如今開始正式的代碼編寫了,首先咱們理清下交互邏輯:web

  1. 手指左右滑動的時候主體容器和菜單容器都跟着手指運動運動
  2. 當手指移動的距離超過菜單容器寬度的時候頁面不能繼續向右滑動
  3. 當手指向左移動使得菜單和頁面的移動距離歸零的時候頁面不能繼續向左移動
  4. 當手指釋放離開屏幕的時候,頁面滑動若是超過必定的距離(整個菜單寬度的比例)則打開整個菜單,若是小於必定距離則關閉菜單

因此如今我們須要在使用組件的時候可以入參定製菜單寬度以及觸發菜單收起關閉的臨界值和菜單寬度的比例,同時須要給主體容器添加touch事件,最後咱們給菜單容器和主體容器添加各自添加一個控制他們運動的style,經過控制這個style來控制容器的移動面試

<template>
  <div class="r-slide-menu">
    <div class="r-slide-menu-wrap" :style="wrapStyle">
      <slot name="menu"></slot>
    </div>
    <div class="r-slide-menu-content" :style="contentStyle"
    @touchstart="touchstart"
    @touchmove="touchmove"
    @touchend="touchend">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    width: {
      type: String,
      default: '250'
    },
    ratio: {
      type: Number,
      default: 2
    }
  },
  data () {
    return {
      isMoving: false,
      transitionClass: '',
      startPoint: {
        X: 0,
        y: 0
      },
      oldPoint: {
        x: 0,
        y: 0
      },
      move: {
        x: 0,
        y: 0
      }
    }
  },
  computed: {
    wrapStyle () {
      let style = {
        width: `${this.width}px`,
        left: `-${this.width / this.ratio}px`,
        transform: `translate3d(${this.move.x / this.ratio}px, 0px, 0px)`
      }
      return style
    },
    contentStyle () {
      let style = {
        transform: `translate3d(${this.move.x}px, 0px, 0px)`
      }
      return style
    }
  },
  methods: {
    touchstart (e) {},
    touchmove (e) {},
    touchend (e) {}
  }
}

接下來,咱們來實現咱們最核心的touch事件處理函數,事件的邏輯以下:小程序

  1. 手指按下瞬間,記錄下當前手指所觸摸的點,以及當前主容器的位置
  2. 手指移動的時候,獲取到移動的點的位置
  3. 計算當前手指所在點移動的X、Y軸距離,若是X移動的距離大於Y移動的距離則斷定爲橫向運動,不然爲豎向運動
  4. 若是橫向運動則判斷當前移動的距離是在合理的移動區間(0到菜單寬度)移動,若是是則改變兩個容器的位置(移動過程當中阻止頁面中其餘的事件觸發)
  5. 手指離開屏幕:若是累計移動距離超過臨界值則運用動畫打開菜單,不然關閉菜單
touchstart (e) {
  this.oldPoint.x = e.touches[0].pageX
  this.oldPoint.y = e.touches[0].pageY
  this.startPoint.x = this.move.x
  this.startPoint.y = this.move.y
  this.setTransition()
},
touchmove (e) {
  let newPoint = {
    x: e.touches[0].pageX,
    y: e.touches[0].pageY
  }
  let moveX = newPoint.x - this.oldPoint.x
  let moveY = newPoint.y - this.oldPoint.y
  if (Math.abs(moveX) < Math.abs(moveY)) return false
  e.preventDefault()
  this.isMoving = true
  moveX = this.startPoint.x * 1 + moveX * 1
  moveY = this.startPoint.y * 1 + moveY * 1

  if (moveX >= this.width) {
    this.move.x = this.width
  } else if (moveX <= 0) {
    this.move.x = 0
  } else {
    this.move.x = moveX
  }
},
touchend (e) {
  this.setTransition(true)
  this.isMoving = false
  this.move.x = (this.move.x > this.width / this.ratio) ? this.width : 0
},
setTransition (isTransition = false) {
  this.transitionClass = isTransition ? 'r-slide-menu-transition' : ''
}

上面,這段核心代碼中有一個setTransition 函數,這個函數的做用是在手指離開的時候給容器元素添加transition屬性,讓容器有一個過渡動畫,完成關閉或者打開動畫;因此在手指按下去的瞬間須要把容器上的這個transition屬性去除,避免滑動過程當中出現容器和手指滑動延遲的不良體驗。微信

最後提醒下,代碼中使用translate3d而非translate的緣由是爲了啓動移動端手機的動畫3D加速,提高動畫流暢度。最終代碼以下:

<template>
  <div class="r-slide-menu">
    <div class="r-slide-menu-wrap" :class="transitionClass" :style="wrapStyle">
      <slot name="menu"></slot>
    </div>
    <div class="r-slide-menu-content" :class="transitionClass" :style="contentStyle"
     @touchstart="touchstart"
     @touchmove="touchmove"
     @touchend="touchend">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    width: {
      type: String,
      default: '250'
    },
    ratio: {
      type: Number,
      default: 2
    }
  },
  data () {
    return {
      isMoving: false,
      transitionClass: '',
      startPoint: {
        X: 0,
        y: 0
      },
      oldPoint: {
        x: 0,
        y: 0
      },
      move: {
        x: 0,
        y: 0
      }
    }
  },
  computed: {
    wrapStyle () {
      let style = {
        width: `${this.width}px`,
        left: `-${this.width / this.ratio}px`,
        transform: `translate3d(${this.move.x / this.ratio}px, 0px, 0px)`
      }
      return style
    },
    contentStyle () {
      let style = {
        transform: `translate3d(${this.move.x}px, 0px, 0px)`
      }
      return style
    }
  },
  methods: {
    touchstart (e) {
      this.oldPoint.x = e.touches[0].pageX
      this.oldPoint.y = e.touches[0].pageY
      this.startPoint.x = this.move.x
      this.startPoint.y = this.move.y
      this.setTransition()
    },
    touchmove (e) {
      let newPoint = {
        x: e.touches[0].pageX,
        y: e.touches[0].pageY
      }
      let moveX = newPoint.x - this.oldPoint.x
      let moveY = newPoint.y - this.oldPoint.y
      if (Math.abs(moveX) < Math.abs(moveY)) return false
      e.preventDefault()
      this.isMoving = true
      moveX = this.startPoint.x * 1 + moveX * 1
      moveY = this.startPoint.y * 1 + moveY * 1

      if (moveX >= this.width) {
        this.move.x = this.width
      } else if (moveX <= 0) {
        this.move.x = 0
      } else {
        this.move.x = moveX
      }
    },
    touchend (e) {
      this.setTransition(true)
      this.isMoving = false
      this.move.x = (this.move.x > this.width / this.ratio) ? this.width : 0
    },
    // 點擊切換
    switch () {
      this.setTransition(true)
      this.move.x = (this.move.x === 0) ? this.width : 0
    },
    setTransition (isTransition = false) {
      this.transitionClass = isTransition ? 'r-slide-menu-transition' : ''
    }
  }
}
</script>

<style lang="scss">
@mixin one-screen {
  position: absolute;
  left:0;
  top:0;
  width:100%;
  height:100%;
  overflow: hidden;
}

.r-slide-menu{
  @include one-screen;
  &-wrap, &-content{
    @include one-screen;
  }
  &-transition{
    -webkit-transition: transform .3s;
    transition: transform .3s;
  }
}
</style>

寫在最後

第一次寫這樣的乾貨,寫的很差請見諒,若是你們以爲有用,給個賞錢喝杯茶唄,讓我後續更有動力寫完全部移動端經常使用的UI組件的文章(誰能教教我怎麼在把這兩個贊助碼縮小啊,尷尬)
支付寶

微信

相關文章
相關標籤/搜索