上週寫了一篇如何寫好一個 vue 組件,算是理論篇吧,可是知易行難,這周我就本身寫一個組件,來實踐一下本身的文章css
本文章以抽屜組件爲例子,有很差或者不完善的地方,歡迎在評論區指出或者提 PR,項目地址,項目 demo 地址html
咱們想象一下用戶會如何使用咱們的組件,它可能須要哪些自定義的功能,好比內容的寬度,控件的位置,抽屜的位置,控件樣式自定義等等,可能的交互好比:點擊控件/鼠標懸浮打開抽屜,點擊抽屜外部收起抽屜等等,接着咱們判斷一下哪些是須要暴露給外部組件的,哪些是屬於組件內部的狀態,儘量的作到這個組件職責單一,且遵循最少知識原則。從這些個角度出發,來編寫咱們的代碼前端
這個組件有個通用的名字,叫抽屜(Drawer),組件結構分爲控件和內容兩部分。如圖:vue
+-----------------------+
| |
+-------+ |
| | |
| | |
| | content |
controls | |
| | |
| | |
| | |
+-------+ |
| |
+-----------------------+
複製代碼
不以規矩,不成方圓。HTML 有語義化標籤,CSS 有 BEM 規範,這些幫助咱們寫出結構清晰的 HTML 架構(ps:佈局部分使用語義化標籤還挺適合的,這種局部小組件仍是 div 一把梭了)。組件 HTML 結構以下:git
<div class="drawer-container">
<div class="drawer">
<div class="controls__container" ref="controls__container">
<ul class="controls">
<li>xxx</li>
</ul>
</div>
<div class="content"></div>
</div>
</div>
複製代碼
咱們拿貼在右側的抽屜舉例(實際代碼與它不徹底相同):github
咱們定義好抽屜的大小,並將其 postion 設置爲 fixed,使用 top,right 屬性,將其固定在右側。由於抽屜默認是收起的,而後經過 translate 將其移除可視區。數組
.drawer {
width: '300px';
height: '100vh';
position: fixed;
top: 0;
right: 0;
transform: 'translate(100%,0)';
}
複製代碼
抽屜展開的代碼也很簡單,在經過 translate 將其移回來微信
.drawer__container--show .drawer {
transform: translate(0);
}
複製代碼
經過負值將控件從抽屜內容區移出來架構
.controls__container {
position: absolute;
left: -40px;
}
複製代碼
抽屜組件支持了 mouseover 和 click 事件,開發的時候,遇到一個比較麻煩的問題:當抽屜以 mouseover 觸發,鼠標移到控件上的時候,抽屜會很鬼畜的打開收起打開收起。(由於鼠標在控件上,mouseover 事件不斷的被觸發,致使抽屜的打開和收起)異步
面對這種狀況,我一開始就想到了防抖和節流。但其實直接拿來用是不適合的
防抖的原理:你儘管觸發事件,可是我必定在事件觸發 n 秒後才執行,若是你在一個事件觸發的 n 秒內又觸發了這個事件,那我就以新的事件的時間爲準,n 秒後才執行,總之,就是要等你觸發完事件 n 秒內再也不觸發事件,我才執行。
防抖因爲是在一個事件觸發 n 秒以後才執行,致使組件有一種反應慢的感受。
節流的原理:若是你持續觸發事件,每隔一段時間,只執行一次事件。
其執行事件是異步的,那麼當我打開抽屜,而後將鼠標移到抽屜外(移到抽屜外會關閉抽屜),由於抽屜的打開和關閉都是由show
變量控制。若是使用節流,會致使異步執行打開抽屜的函數,致使抽屜關閉以後又開起。
節流通常是指事件在一段時間內執行。咱們這裏不妨換一種思路,對show
值進行節流,你也能夠把它理解成一種鎖。那麼當show
值變化後,咱們鎖住show
值,n 秒內不容許修改,n 秒後才能夠修改。即控制住了抽屜不會在短期內迅速開合。咱們使用計算屬性實現以下:
// this.lock 初始值爲undefine
// 開閉抽屜的函數經過對lockedShow進行賦值,不會直接操做show
lockedShow: {
get() {
return this.show;
},
set(val) {
if (this.lock) {
return;
} else {
this.lock = setTimeout(() => {
// 200毫秒以後解除鎖
this.lock = undefined;
}, 200);
this.show = val;
}
}
}
複製代碼
這裏咱們經過 Element.closest() 方法用來獲取點擊的祖先元素(Element.closest:匹配特定選擇器且離當前元素最近的祖先元素,也能夠是當前元素自己)。若是匹配不到,則返回 null。
closeSidebar(evt) {
const parent = evt.target.closest(".drawer");
// 點擊抽屜之外部分,即匹配不到,parent值爲null
if (!parent) {
this.show = false;
}
}
複製代碼
全局監聽點擊事件
window.addEventListener('click', this.closeSidebar)
複製代碼
我一開始的作法是,組件掛載的時候,全局監聽點擊事件,組件銷燬時移除點擊事件。但咱們能夠作的更好,當 controls 被點擊時,添加點擊事件,收起抽屜的時候,移除點擊事件。減小全局監聽的 click 事件。
除了點擊事件,咱們也順便支持一下 hover 的操做。鼠標移出收起的操做和點擊抽屜外部分收起的代碼相同。
經過e.type
判斷是點擊事件仍是鼠標移入事件。
toggleDrawerShow(e) {
if (e.type === "mouseover" && this.triggerEvent === "mouseover") {
// do some thing
}
if (e.type === "click" && this.triggerEvent === "click") {
// do some thing
}
}
複製代碼
使得控件徹底貼合內容區,不會由於控件的內容變化,好比控件內容爲 show 和 hidden,因爲切換的時候,兩個單詞長度不同,而使得控件顯示不徹底,或者脫離內容區。
這種狀況咱們可使用 JavaScript 動態計算。由於常常用到,仍是封裝成一個函數吧。仍是拿右側抽屜舉例子:
updateControlLayout() {
// 獲取控件的寬高
const rect = this.$refs['controls'].getBoundingClientRect()
if (this.position === 'right') {
// 從新設置偏移量
this.$refs['controls'].style['left'] = `-${rect.width}px`
}
}
複製代碼
主要是蒙層的顯影,以及抽屜的開合。CSS動畫貝塞爾曲線瞭解一下,筆者本身也瞭解很少,感興趣能夠本身去看。
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
複製代碼
當內容過長的時候,打開抽屜的時候,滾動條還在。所以咱們須要在抽屜打開的時候打開滾動條。代碼也很好寫,給document.body
添加overflow:hidden
屬性。
這裏有一個小小的坑。原先的 css 是置於 scope 裏面的,若是想要把這個屬性添加到 body 上,是不成功的。把 scoped 去了便可。
<style>
.hidden_scoll_bar{
overflow: hidden;
}
</style>
複製代碼
每一個人都有本身獨特的審美,否則也不會出現那麼多的 UI 庫了。做爲一個組件的設計者,很難預設不少種樣式讓每個使用組件的人都滿意。不如把本身定義的控件做爲插槽的後備內容,用戶能夠很方便的使用control
的具名插槽覆寫控件。
<li v-for="(control,idx) in controlItems" class="control" :class="'control-'+idx" :key="idx" >
<template v-if="show">
// 提供用戶自定義插槽所須要的信息(控件是否展現,控件的信息)
<slot name="control" v-bind:drawer="{drawerShow:show,control}" >{{control.hidden}}</slot >
</template>
<template v-else>
<slot name="control" v-bind:drawer="{drawerShow:show,control}" >{{control.show}}</slot >
</template>
</li>
複製代碼
由於抽屜支持在上下左右四個方向上放置,不一樣方向上定義的偏移方向都不一樣。所以須要定義不一樣的 css 類。經過傳入的 position 值,利用 css 的級聯特性應用樣式
<div class="drawer__container" :class="[positionClass,{'drawer__container--show':show}]" ></div>
複製代碼
data() {
return {
show: false,
positionClass: this.position
};
},
複製代碼
// 定義右側的drawer,其他方向上的同理
// 經過css的級聯,對不一樣方向上的drawer添加不一樣的樣式
.right .drawer {
height: 100vh;
width: 100%;
transform: translate(100%, 0);
top: 0;
right: 0;
}
複製代碼
抽屜組件內部的狀態沒有被暴露出去,用戶可能有點擊控件,不打開抽屜而去作其餘事情的需求。所以咱們須要提供一個鉤子,經過 prop 將函數openDrawer
傳入,openDrawer
控制是否抽屜被打開。
點擊控件,開合抽屜的實現,利用了事件委託,將 click 事件,mouseover 事件直接掛載到了class=controls
的 ul 元素上,爲了方便識別目標li
元素,給每個 li 元素添加 :class="'control-'+idx"
<ul class="controls" @click="toggleDrawerShow" @mouseover="toggleDrawerShowByMouseover" >
<li v-for="(control,idx) in controlItems" class="control" :class="'control-'+idx" :key="idx" >
<!-- xxx -->
</li>
</ul>
複製代碼
// 開合抽屜的函數
openDrawerByControl(evt) {
const onOpenDraw = this.openDrawer;
if (!onOpenDraw) {
this.lockedShow = true;
return;
}
// 獲取到目標階段指向的函數
const target = evt.target;
//獲取到代理事件的元素
const currentTarget = evt.currentTarget;
// 咱們給openDraw傳入target,currentTarget兩個參數,具體由父組件決定onOpenDraw如何實現
this.lockedShow = onOpenDraw(target, currentTarget);
}
複製代碼
父組件傳入的函數以下,關於事件委託的知識感受能夠應用在這裏,筆者作一個示例,讓class='control-0'
的元素不能點擊。
咱們使用 Element.matches 匹配.control-0
類,其能夠像 CSS 選擇器作更加靈活的匹配。但由於 li 元素裏面可能還有其餘元素,因此須要不斷向上尋找其父元素,直到匹配到咱們事件委託的元素爲止
openDrawer(target) {
let shouldOpen = true;
// 僅遍歷到最外層
while (!target.matches(".controls")) {
// 判斷是否匹配到咱們所須要的元素上
if (target.matches(".control-0")) {
shouldOpen = false;
break;
} else {
// 向上尋找
target = target.parentNode;
}
}
return shouldOpen;
}
複製代碼
一個一年小前端,關注個人微信公衆號,和我一塊兒交流,我會盡我所能,而且看看我能成長成什麼樣子吧。交個朋友吧!