美文网首页我们就爱程序媛
vue移动助手实践(四)——分分钟自定义一个属于自己的vue组件

vue移动助手实践(四)——分分钟自定义一个属于自己的vue组件

作者: Katherine的小世界 | 来源:发表于2017-10-08 18:37 被阅读0次

    最近都是用element-ui 在协助自己的项目开发,用着用着就想看看饿了么是怎么实现组件的使用的,于是就想自己动手也来写,当然,我是要循序渐进的,从最开始最简单的组件定义开始。总的写了三个小组件,我按照我自己觉得难度等级,分别定义为基础版,打怪版,终极版。

    项目在线demo

    项目在线演示demo(切换到移动端调试模式哦)

    项目github地址

    项目github地址

    嗯,在写之前,我先说一下,我会这篇文章中写下面三个小组件中的其中两个。分别如下所示:

    • 基于vue的backTop 返回顶部小组件 --------- 普通版
    • 基于vue的下拉菜单小组件 ----------打怪版
    • 基于vue的侧边栏导航菜单的小组件 ------------------终极版

    在编写组件的时候,复用组件是很有好处的。可复用组件应该有一个清晰的公共接口。
    Vue组件的API来自三个部分:props events slots

    • props: 允许外部环境传递数据给组件。
    • Events: 允许组件对外部环境产生副作用。
    • Slots: 允许外部环境将额外的内容组合在组件内部。

    那么,其实我们可以用v-bindv-on的简写语法,来使得模板清楚简洁:

    // 这是我写的backTop 组件的调用方法。
    <back-top :scrollmyself = 'true'></back-top>   // 用props将外部环境数据传递进去。
    

    额,我觉得还是直接动手开始做吧,会比较实在一点,那就从最简单的backTop组件开始吧。

    一 基础版: backTop返回顶部组件。

    1.1最终的实现效果:

    页面右下角就是自己封装的backtop组件。

    test.gif
    1.2 外部组件调用的方式:
    <back-top :scrollmyself = 'true'></back-top>   //这个scrollmyself是传进去组件的props值,
    
    1.3 组件自定义方式

    先说一下功能情况,这个backTop组件的作用就是,当页面存在滚动条,或者页面中某个局部有存在滚动条,当页面滚动到一定位置之后,页面就会出现点击返回顶部的按钮,点击之后就会返回顶部,此时返回顶部的icon消失。

    • 1 .定义组件的基本功能结构
    • 2 . 定义组件的install将组件export出去
    • 3 在项目的main.js中使用调用组件。

    首先看一下文件结构:

    image.png

    1 backtop内部的main.vue文件
    -------- template模板
    ----------组件名: name: BackTop
    ----------props: 定义props数据格式,默认为false;true的时候当前发生滚动的对象就是内部引用该组件的父组件,为false的时候就是window对象。

    <template>
    <transition name='slide-fade'>
        <div class='page-component-up' v-show='isShow' @click='getTop'>
        <i class='tri'></i>
      </div>
    </transition>
    </template>
    <script>
    export default {
      name: 'BackTop',  // 这个是export出去的组件名,我定义为BackTop
      props: {
        scrollmyself: {
          type: Boolean,  // 这是选择滚动对象的props值,如果滚动的对象是当前组件的父元素,就设置scrollObj为true.如果没有设置就默认为window对象
          default: false
        }
      },
      data () {
        return {
          isShow: false,
          target: ''
        }
      },
      methods: {
    //  添加样式,鼠标hover上去,改变颜色
        addhoverClass (e) {
          if (e.type === 'mouseover') {
            this.$el.classList.add('hover')
          } else if (e.type === 'mouseout') {
            this.$el.classList.remove('hover')
          }
        },
        showIcon () {
      //  根据scrollTop的值来判断是否显示返回顶部的icon
          if (this.target.scrollTop > 100) {
            this.isShow = true
            this.$el.addEventListener('mouseover', this.addhoverClass)
            this.$el.addEventListener('mouseout', this.addhoverClass)
          } else if (this.target.scrollTop < 100) {
            this.isShow = false
          }
        },
        getTop () {
    // 点击icon之后自动返回顶部的函数
          let timer = setInterval(() => {
            let top = this.target.scrollTop
            let speed = Math.ceil(top / 5)
            this.target.scrollTop = top - speed
            if (top === 0) {
              clearInterval(timer)
            }
          }, 20)
        }
      },
      mounted () {
        // 通过这个target来判断当前的滚动监听对象是谁
        if (this.scrollmyself) {
          this.target = this.$el.parentNode
        } else {
          this.target = document.body
        }
        this.target.addEventListener('scroll', this.showIcon)
      },
      beforeDestroy () {
       //  组件销毁的时候,需要删除scroll的监听事件。
        this.target.removeEventListener('scroll', this.showIcon)
      }
    }
    </script>
    
    // CSS部分:
    
    <style lang="scss" rel="stylesheet/scss">
      .slide-fade-enter-active {
         transition: all .1s ease;
      }
      .slide-fade-leave-active {
        transition: all .1s cubic-bezier(1.0, 0.3, 0.8, 1.0);
        opacity: 0;
      }
      .slide-fade-enter, .slide-fade-leave-to
       /* .slide-fade-leave-active 在低于 2.1.8 版本中 */ {
      // transform: translateY(-20px);
        opacity: 0;
      }
      .page-component-up {
        background-color: #4eb1fb;
        position: fixed;
        right: 3rem;
        bottom: 12rem;
        width: 50px;
        height: 50px;
        border-radius: 25px;
        cursor: pointer;
        opacity: .4;
        transition: .3s;
        text-align: center;
        z-index: 999;
      }
      .tri {
        width: 0;
        height: 0;
        border: 12px solid transparent;
        border-bottom-color: #dfe6ec;
        text-align: center;
      }
      .hover {
        background-color: red;
      }
    </style>
    
    

    2 引出组件:

    在我们的component的内部的index.js文件中,我们需要将组件引出;

    import BackTop from './backtop/src/main';
    
    /* istanbul ignore next */
    BackTop.install = function(Vue) {
      Vue.component(BackTop.name, BackTop);
    };
    
    export default BackTop;
    

    3 在main.js内部引用

    import backTop from './myComponent/backtop'
    Vue.use(backTop)
    

    总结一下: 在上面这个backtop组件中,用props进行数据的传递,将数据传递给内部组件。
    接下来这个侧边栏多级下拉导航侧边栏,实现的最终效果如下所示。

    二 终极版本: sideBar侧边栏组件。

    2.1 最终的实现效果:

    侧边栏组件可以实现多级下拉菜单,同时也可以实现路由的跳转,只要设置相应的route值就可以。

    test.gif
    2.2 组件的基本结构

    因为这个组件是侧边栏组件,有单个的子菜单,也有包含有下拉子菜单的菜单,同时,所有我分成三个小的组件来实现。

    同时也会使用slot来进行内容的分发。

    基本的结构如下所示:

    image.png

    其实这个组件对于我来说,存在几个难点。

    • 1 首先这是一个可以多级下拉菜单的组件,那么基本的结构和样式就很重要,如何让子菜单下的子菜单每次都依次往右边移动大概20px的距离,可以凸显出菜单之间的级别关系。
    • 2 其次是点击每一个含有子菜单的标题,如何让其显示下拉菜单,而且是下拉的样式来显示的,同时要保证再深一层次的下拉菜单不会显示出来。
    • 3 我会用一个props来从父组件向子组件传递数据,通过 props myVisible来控制导航侧边栏的出现与消失。同时你也会发现,通过点击蒙板(在组件内部定义)也可以实现侧边栏的消失,如何实现双向数据传递呢?

    待会我会提到这两个问题,不过我们可以先来看一下这个组件引入(怎么引入我待会说,跟上面的一样)之后的使用范例:

     <my-menu :my-visible.sync = "visible">
          <!-- 这里的按钮可以自己去封装定义 -->
          <!-- <p slot='toggleBtn'>点我点我</p> -->
          <template slot="menu-title">我的个人助手小系统呀</template>
          <menu-item route='/'><i slot='icon' class=' iconfont icon-403010'></i>首页</menu-item>
          <menu-item route='/DatePlan'><i slot='icon' class=' iconfont icon-403010'></i>DatePlan</menu-item>
          <menu-item route='/EatWhat'><i slot='icon'  class=' iconfont icon-chi'></i>今天吃什么</menu-item>
          <menu-item route='/memo'><i slot='icon'  class=' iconfont icon-beiwanglu'></i>备忘录</menu-item>
          <menu-item route='/when'><i slot='icon'  class=' iconfont icon-fangjia'></i>什么时候放假</menu-item>
          <menu-item route='/icon'><i slot='icon'  class=' iconfont icon-pinrenpinkongxin'></i>抛硬币</menu-item>
          <menu-item route='/mirror'><i slot='icon'  class=' iconfont icon-jingzi'></i>照镜子</menu-item>   
          <my-submenu>
            <i slot="icon" class=' iconfont icon-jizhang'></i><template slot="submenu-title"></i>记账</template>
            <menu-item route='/money'><i slot="icon" class=' fa fa-circle-o'></i>记账首页</menu-item>
            <menu-item route='/moneyRecord'><i slot="icon" class='fa fa-circle-o'></i>添加记账</menu-item>
            <my-submenu>
              <i slot="icon" class='fa fa-circle-o'></i><template slot="submenu-title">这是有下拉菜单</template>
              <menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第一个</menu-item>
              <menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第二个</menu-item>
            </my-submenu>
             <my-submenu>
              <i slot="icon" class='fa fa-circle-o'></i><template slot="submenu-title">这是有下拉菜单</template>
              <menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第一个</menu-item>
              <menu-item><i slot="icon" class='fa fa-circle-o'></i>我是第二个</menu-item>
              <my-submenu>
                <i slot="icon" class='fa fa-circle-o'></i><template slot="submenu-title">这是有下拉菜单</template>
                <menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第一dddddd个</menu-item>
                <menu-item><i slot="icon" class=' fa fa-circle-o'></i>我是第二ddddddddddddd个</menu-item>
              </my-submenu>
            </my-submenu>
          </my-submenu>
        </my-menu>
    
    

    基本的使用结构,可以认真看例子代码。具体的细节我就不说啦。

    参数
    [my-menu 组件] myVisible : 默认为false,控制侧边栏的显示与消失。
    [menu-item组件] route : 默认为空,控制路由的跳转。
    slot
    [my-menu 组件] menu-title: 控制菜单的标题显示
    [menu-item组件] icon: icon图标的显示。
    [my-submenu组件] submenu-title: 子级菜单的标题显示。icon: icon图标的显示。

    2.3 组件的代码结构

    menu-item的结构

    难点实现:这个渲染之后是每一个不含有子菜单的菜单,那么问题来了,当有绑定路由对象的时候,点击某个菜单的时候,侧边栏菜单是要消失的,那么如何,去告诉引用了menu-item组件的my-menu父组件去关闭呢?
    解决方法:这里参考了饿了么组件的dispatch方法,(dispatch文件就不po出来了),向父组件传递事件。
    引入了dispatch文件之后:
    子组件中使用:this.dispatch('my-menu', 'closeByRoute')
    监听的my-menu父组件:this.$on('closeByRoute', this.toggleShow)

    <template>
        <li >
            <router-link href="#" style='color:white' :to='route' @click.native='handleRoute'>
              <slot name='icon'></slot>
              <span class='menutitle'><slot></slot></span></router-link>
          </li>
    </template>
    <script>
    import dispatch from '../../utils/dispatch'
    export default {
      name: 'menu-item',
      mixins: [dispatch],
      props: {
        route: {
          type: String,
          default: ' '
        }
      },
      methods: {
        handleRoute () {
          if (this.route) {
            this.dispatch('my-menu', 'closeByRoute') 
            // 使用dispatch进行传递 this.dispatch(组件名, 触发的事件名)
          }
        }
      }
    }
    </script>
    

    my-menu的结构

    前面提到,my-menu组件是用props myVisible来实现侧边栏的显示与消失。因为vue是不可以直接修改prop属性的,但是新版的vue可以使用sync来实现父子组件的双向数据绑定。
    只要在调用的时候,使用 <my-menu :my-visible.sync = 'visible'></my-menu>
    而在组件内部,想要变更props值的时候,只需要添加 this.$emit('update:myVisible', !this.myVisible)来更新props属性值就可以。

    http://www.cnblogs.com/penghuwan/p/7473375.html#_label1 大家可以参考一下这篇文章。

    这个组件的主要就是用props来控制显示和隐藏。最主要的是toggleShow方法,控制侧边栏的显示和隐藏,通过添加一个togglehide 的类来判断当前的侧边栏是否是显示的状态。

    <template>
      <div class='sideBar togglehide' ref='barPart'>
        <div class='menuCover' @click='toggleMenu' ref='cover'></div>
        <ul class='menu'>
          <li class='list-title'><slot name="menu-title"></slot></li>
          <slot></slot>  
        </ul>
      </div>
    </template>
    <script>
    export default {
      name: 'my-menu',
      props: {
        myVisible: {
          type: Boolean,
          default: false
        }
      },
      watch: {
        myVisible () {
          this.toggleShow()
        }
      },
      methods: {
        toggleMenu () {
          this.$emit('update:myVisible', !this.myVisible)
        },
        toggleShow () {
          let target = this.$refs.barPart
          let test = target.classList.contains('togglehide')
          if (!test) {
            target.classList.add('togglehide')
            this.$emit('closeBar') // 关闭导航标签的回调
            let OpenMenu = target.querySelectorAll('.openMenu')
            let OpenIcon = target.querySelectorAll('.openIcon')
            this.$refs.barPart.style.left = -this.$refs.barPart.offsetWidth + 'px'
            for (let i = 0; i < OpenMenu.length; i++) {
              OpenMenu[i].classList.remove('openMenu')
              OpenMenu[i].style.display = 'none'
            }
            for (let i = 0; i < OpenIcon.length; i++) {
              OpenIcon[i].classList.remove('openIcon')
            }
          } else {
            target.removeAttribute('style')
            target.classList.remove('togglehide')
            this.$emit('openBar') // 打开导航标签的回调
            this.$refs.barPart.style.left = 0 + 'px'
          }
        }
      },
      mounted () {
        this.$refs.barPart.style.left = -this.$refs.barPart.offsetWidth + 'px'  //初始化通过left值来隐藏侧边栏组件
        this.$on('closeByRoute', this.toggleShow)
      }
    }
    </script>
    
    my-submenu 组件。

    含有子菜单的菜单引用,就需要引用my-submenu的组件。关于如何实现子菜单的下拉和收起的效果,这是这个组件的主要实现难点。

    image.png

    基本思路如下:
    1 一开始先设置.treeview同级的treeview-menu菜单的display为none
    2 当包含有子菜单的菜单即记账标签被点击之后,设置treeview-menu的样式height为0,首先设置为display:block,而且over-flow为hidden。然后获取当前的子菜单下的li个数,即可以获取所有子元素的高度,然后再设置treeview-menu的高度为该高度。
    结合transition就可以实现下拉效果了。具体可以看代码。

    这里设置display:block和设置高度不能同时设置,不然transition不会生效,可以设置一个小延时,设置为display:block之后,再设置高度。
    <template>
        <div>
         <li class='treeview'  @click='toggleShowMenu'>
              <a href="#" data-show = false  style='color:white'>
              <slot name='icon'></slot>   
              <span class='menutitle'><slot name="submenu-title"></slot></span>
              <span class='pull-right-container'><i class='fa fa-angle-left pull-right' style='color:white'></i></span>
            </a>
            </li>    
            <ul class='treeview-menu' style='display:none'>
              <slot></slot>
            </ul>
        </div>
    </template>
    <script>
    export default {
      name: 'my-submenu',
      methods: {
        toggleShowMenu (e) {
          let setTarget = e.currentTarget.nextElementSibling
          if (setTarget !== null) {
            let showCon = setTarget.classList.contains('openMenu')
            let childLi = setTarget.children
            var totalHeight = 0
            let h = e.currentTarget
            var targetIcon = h.querySelectorAll('.pull-right')[0]  // todo: h是当前的元素
            let nodeListArr = Array.prototype.slice.call(childLi)
            if (!showCon) {
              setTarget.style.height = 0
              setTarget.classList.add('openMenu')
              targetIcon.classList.add('openIcon')
              setTarget.style.overflow = 'hidden'
              setTarget.style.display = 'block'
              for (let i = 0; i < nodeListArr.length; i++) {
                totalHeight = totalHeight + nodeListArr[i].offsetHeight
              }
              setTimeout(() => {
                setTarget.style.height = totalHeight + 'px'
                setTimeout(() => {
                  setTarget.removeAttribute('style')
                  setTarget.style.display = 'block'
                }, 300)
              }, 40)
            } else {
              targetIcon.classList.remove('openIcon')
              setTarget.style.height = setTarget.offsetHeight + 'px'
              setTarget.style.overflow = 'hidden'
              setTarget.classList.remove('openMenu')
              setTimeout(() => {
                setTarget.style.height = 0 + 'px'
                setTimeout(() => {
                  setTarget.removeAttribute('style')
                  setTarget.style.display = 'none'
                }, 300)
              }, 40)
            }
          }
        }
      }
    }
    </script>
    

    在index.js中export组件

    import mymenu from './sidebar//src/my-menu.vue'
    import menuitem from './sidebar/src/menu-item.vue'
    import mysubmenu from './sidebar/src/my-submenu.vue'
    import BackTop from './backtop/src/main'
    /* istanbul ignore next */
    
    const components = [
      mymenu,
      menuitem,
      mysubmenu,
      BackTop
    ]
    
    const install = (Vue, OPts) => {
      if (install.installed) {
        return
      }
      components.map(component => {
        Vue.component(component.name, component)
      })
    }
    
    export default {
      version: '0.0.1',
      author: 'katherine',
      install,
      mymenu,
      menuitem,
      mysubmenu,
      BackTop
    }
    
    这里我将所有的组件都在这里export出去了。引用的时候,只要直接import整个文件就可以了。

    main.js

    import globalUI from './myComponent'
    Vue.use(globalUI)
    

    一个小技巧

    image.png image.png
    关于怎么实现这些子菜单中的等级关系,就是越往下的子菜单就会依次增多一个padding-left:20px;的属性值。
    其实可以看一下我的 my-submenu.vue 的结构 image.png

    而当有多级下拉菜单的时候,我在外部引用的时候都是这样子去引用调用的。
    而含有多级子菜单的,我都是直接在my-submenu内部再去嵌套添加,细心的你会发现,我的子菜单list都是存在一个这样的ul里面。

    image.png

    所以,我就在CSS样式中设置如下,这个很关键,这样的话,只要你不管嵌套多少层下拉菜单,都会依次增加一个padding-left:20px;的属性值。

    .treeview-menu {
      .treeview-menu {
       padding-left:20px; 
    }
    }
    

    呼呼,感觉自己讲得太细节了,会不会反而会更混乱,但是这些是我在做的过程中遇到的问题,毕竟自己是小白,觉得很多东西如果不讲细节一点,一开始学肯定觉得很吃力,不知道要怎么深入。所以我都会希望自己能够详细地分享自己学习过程中所遇到的问题。也希望通过分享,自己也能从别人身上学到新的知识。

    项目在线demo

    项目在线演示demo(切换到移动端调试模式哦)

    项目github地址(喜欢的话欢迎start~~~~)

    项目github地址

    相关文章

      网友评论

        本文标题:vue移动助手实践(四)——分分钟自定义一个属于自己的vue组件

        本文链接:https://www.haomeiwen.com/subject/zgxhyxtx.html