最近面试发现很多前端程序员都从来没有写过插件的经验, 基本上都是网上百度所以打算写一系列文章, 手把手的教一些没有写过组件的兄弟们如何去写插件本系列文章都基于 vue, 核心内容都一样, 会了之后大家可以快速的改写成 reactangular 或者是小程序等组件这篇文章是第一篇, 写的是一个类似 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
现在开始正式的代码编写了, 首先我们理清下交互逻辑:
手指左右滑动的时候主体容器和菜单容器都跟着手指运动运动
当手指移动的距离超过菜单容器宽度的时候页面不能继续向右滑动
当手指向左移动使得菜单和页面的移动距离归零的时候页面不能继续向左移动
当手指释放离开屏幕的时候, 页面滑动如果超过一定的距离 (整个菜单宽度的比例) 则打开整个菜单, 如果小于一定距离则关闭菜单
所以现在咱们需要在使用组件的时候能够入参定制菜单宽度以及触发菜单收起关闭的临界值和菜单宽度的比例, 同时需要给主体容器添加 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 事件处理函数, 事件的逻辑如下:
手指按下瞬间, 记录下当前手指所触摸的点, 以及当前主容器的位置
手指移动的时候, 获取到移动的点的位置
计算当前手指所在点移动的 XY 轴距离, 如果 X 移动的距离大于 Y 移动的距离则判定为横向运动, 否则为竖向运动
如果横向运动则判断当前移动的距离是在合理的移动区间 (0 到菜单宽度) 移动, 如果是则改变两个容器的位置(移动过程中阻止页面中其他的事件触发)
手指离开屏幕: 如果累计移动距离超过临界值则运用动画打开菜单, 否则关闭菜单
- 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>
总结
以上所述是小编给大家介绍的 vue 移动端 UI 框架实现 QQ 侧边菜单组件, 希望对大家有所帮助, 如果大家有任何疑问请给我留言, 小编会及时回复大家的在此也非常感谢大家对脚本之家网站的支持!
来源: http://www.jb51.net/article/136108.htm