vue移動助手實踐(四)——分分鐘自定義一個屬於本身的vue組件(基於Vue的側邊欄和返回頂部組件)

最近都是用element-ui 在協助本身的項目開發,用着用着就想看看餓了麼是怎麼實現組件的使用的,因而就想本身動手也來寫,固然,我是要按部就班的,從最開始最簡單的組件定義開始。總的寫了三個小組件,我按照我本身以爲難度等級,分別定義爲基礎版,打怪版,終極版。css

項目在線demo

項目在線演示demo(切換到移動端調試模式哦)html

項目github地址

項目github地址vue

嗯,在寫以前,我先說一下,我會這篇文章中寫下面三個小組件中的其中兩個。分別以下所示:node

  • 基於vue的backTop 返回頂部小組件 --------- 普通版
  • 基於vue的下拉菜單小組件 ----------打怪版
  • 基於vue的側邊欄導航菜單的小組件 ------------------終極版

在編寫組件的時候,複用組件是頗有好處的。可複用組件應該有一個清晰的公共接口。
Vue組件的API來自三個部分:props events slotsgit

  • props: 容許外部環境傳遞數據給組件。
  • Events: 容許組件對外部環境產生反作用。
  • Slots: 容許外部環境將額外的內容組合在組件內部。

那麼,其實咱們能夠用v-bindv-on的簡寫語法,來使得模板清楚簡潔:github

// 這是我寫的backTop 組件的調用方法。
<back-top :scrollmyself = 'true'></back-top>   // 用props將外部環境數據傳遞進去。複製代碼

額,我以爲仍是直接動手開始作吧,會比較實在一點,那就從最簡單的backTop組件開始吧。element-ui

一 基礎版: backTop返回頂部組件。

1.1最終的實現效果:

頁面右下角就是本身封裝的backtop組件。bash

test.gif
test.gif

1.2 外部組件調用的方式:

<back-top :scrollmyself = 'true'></back-top>   //這個scrollmyself是傳進去組件的props值,複製代碼

1.3 組件自定義方式

先說一下功能狀況,這個backTop組件的做用就是,當頁面存在滾動條,或者頁面中某個局部有存在滾動條,當頁面滾動到必定位置以後,頁面就會出現點擊返回頂部的按鈕,點擊以後就會返回頂部,此時返回頂部的icon消失。ide

  • 1 .定義組件的基本功能結構
    • 2 . 定義組件的install將組件export出去
    • 3 在項目的main.js中使用調用組件。

首先看一下文件結構:函數

image.png
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
test.gif

2.2 組件的基本結構

由於這個組件是側邊欄組件,有單個的子菜單,也有包含有下拉子菜單的菜單,同時,全部我分紅三個小的組件來實現。

同時也會使用slot來進行內容的分發。

基本的結構以下所示:

image.png
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組件的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屬性值就能夠。

www.cnblogs.com/penghuwan/p… 你們能夠參考一下這篇文章。

這個組件的主要就是用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
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

image.png
image.png

關於怎麼實現這些子菜單中的等級關係,就是越往下的子菜單就會依次增多一個 padding-left:20px;的屬性值。
其實能夠看一下個人 my-submenu.vue 的結構

image.png
image.png

而當有多級下拉菜單的時候,我在外部引用的時候都是這樣子去引用調用的。
而含有多級子菜單的,我都是直接在my-submenu內部再去嵌套添加,細心的你會發現,個人子菜單list都是存在一個這樣的ul裏面。

image.png
image.png

因此,我就在CSS樣式中設置以下,這個很關鍵,這樣的話,只要你無論嵌套多少層下拉菜單,都會依次增長一個padding-left:20px;的屬性值。

.treeview-menu {
  .treeview-menu {
   padding-left:20px; 
}
}複製代碼

呼呼,感受本身講得太細節了,會不會反而會更混亂,可是這些是我在作的過程當中遇到的問題,畢竟本身是小白,以爲不少東西若是不講細節一點,一開始學確定以爲很吃力,不知道要怎麼深刻。因此我都會但願本身可以詳細地分享本身學習過程當中所遇到的問題。也但願經過分享,本身也能從別人身上學到新的知識。

項目在線demo

項目在線演示demo(切換到移動端調試模式哦)

項目github地址(喜歡的話歡迎start~~~~)

項目github地址

相關文章
相關標籤/搜索