最近都是用element-ui 在協助自己的專案開發,用著用著就想看看餓了麼是怎麼實現元件的使用的,於是就想自己動手也來寫,當然,我是要循序漸進的,從最開始最簡單的元件定義開始。總的寫了三個小元件,我按照我自己覺得難度等級,分別定義為基礎版,打怪版,終極版。
專案線上demo
專案github地址
嗯,在寫之前,我先說一下,我會這篇文章中寫下面三個小元件中的其中兩個。分別如下所示:
- 基於vue的backTop 返回頂部小元件 ——— 普通版
- 基於vue的下拉選單小元件 ———-打怪版
- 基於vue的側邊欄導航選單的小元件 ——————終極版
在編寫元件的時候,複用元件是很有好處的。可複用元件應該有一個清晰的公共介面。
Vue元件的API來自三個部分:props events slots
- props: 允許外部環境傳遞資料給元件。
- Events: 允許元件對外部環境產生副作用。
- Slots: 允許外部環境將額外的內容組合在元件內部。
那麼,其實我們可以用v-bind和v-on的簡寫語法,來使得模板清楚簡潔:
// 這是我寫的backTop 元件的呼叫方法。
<back-top :scrollmyself = `true`></back-top> // 用props將外部環境資料傳遞進去。複製程式碼
額,我覺得還是直接動手開始做吧,會比較實在一點,那就從最簡單的backTop元件開始吧。
一 基礎版: backTop返回頂部元件。
1.1最終的實現效果:
頁面右下角就是自己封裝的backtop元件。
1.2 外部元件呼叫的方式:
<back-top :scrollmyself = `true`></back-top> //這個scrollmyself是傳進去元件的props值,複製程式碼
1.3 元件自定義方式
先說一下功能情況,這個backTop元件的作用就是,當頁面存在滾動條,或者頁面中某個區域性有存在滾動條,當頁面滾動到一定位置之後,頁面就會出現點選返回頂部的按鈕,點選之後就會返回頂部,此時返回頂部的icon消失。
- 1 .定義元件的基本功能結構
- 2 . 定義元件的install將元件export出去
- 3 在專案的main.js中使用呼叫元件。
首先看一下檔案結構:
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值就可以。
2.2 元件的基本結構
因為這個元件是側邊欄元件,有單個的子選單,也有包含有下拉子選單的選單,同時,所有我分成三個小的元件來實現。
同時也會使用slot來進行內容的分發。
基本的結構如下所示:
其實這個元件對於我來說,存在幾個難點。
-
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屬性值就可以。
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的元件。關於如何實現子選單的下拉和收起的效果,這是這個元件的主要實現難點。
基本思路如下:
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)複製程式碼
一個小技巧
關於怎麼實現這些子選單中的等級關係,就是越往下的子選單就會依次增多一個padding-left:20px;
的屬性值。
其實可以看一下我的 my-submenu.vue 的結構
而當有多級下拉選單的時候,我在外部引用的時候都是這樣子去引用呼叫的。
而含有多級子選單的,我都是直接在my-submenu內部再去巢狀新增,細心的你會發現,我的子選單list都是存在一個這樣的ul裡面。
所以,我就在CSS樣式中設定如下,這個很關鍵,這樣的話,只要你不管巢狀多少層下拉選單,都會依次增加一個padding-left:20px;的屬性值。
.treeview-menu {
.treeview-menu {
padding-left:20px;
}
}複製程式碼
呼呼,感覺自己講得太細節了,會不會反而會更混亂,但是這些是我在做的過程中遇到的問題,畢竟自己是小白,覺得很多東西如果不講細節一點,一開始學肯定覺得很吃力,不知道要怎麼深入。所以我都會希望自己能夠詳細地分享自己學習過程中所遇到的問題。也希望通過分享,自己也能從別人身上學到新的知識。