3

前言

最近面试发现很多前端程序员都从来没有写过插件的经验,基本上都是网上百度。所以打算写一系列文章,手把手的教一些没有写过组件的兄弟们如何去写插件。本系列文章都基于VUE,核心内容都一样,会了之后大家可以快速的改写成react、angular或者是小程序等组件。这篇文章是第一篇,写的是一个类似QQ的侧边菜单组件。

效果展示

先让大家看个效果展示,知道咱们要做的东西是个怎么样的样子,图片有点模糊,大家先将就点:
图片描述

开始制作

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插槽内:

<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,代码如下:

<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

现在开始正式的代码编写了,首先我们理清下交互逻辑:

  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组件的文章(谁能教教我怎么在把这两个赞助码缩小啊,尴尬)
支付宝

微信


cyq
100 声望3 粉丝

菜鸟的地盘,大牛请蹂躏!!!