轉載https://juejin.im/post/598965bd5188256da941872c
現在前端的快速發展,已經讓組件這個模式變的格外重要。對於市面上的組件庫,雖然能滿足大部分的項目,但是一些小型細節方面和使用方面,或者UI庫存在的一些bug,會讓人很頭疼。
那我們應該如何面對解決這些問題。俗話說自己動手豐衣足食。有些組件不用刻意去造。應該考慮如何去打造一個快速,兼容性好,功能齊全的組件庫。
-
先到github上和一些大公司開源的組件庫官網上去看看你所需組件庫的demo例子,Prop和event暴露出來了哪些接口。
-
貨比三家。別人分別用了哪些模式設計,哪種模式最簡便,更合理。
-
一般成熟的UI庫兼容性是經過大量測試和用戶使用後沒有問題後的結果。省去這一部分,進行樣式的借鑑。
-
不要太急於進行組件的大量造輪子。因爲一個人的戰鬥是有限的。根據需要和場景項目進行一個個定位,積少成多。
Crib-zk也是我個人目前針對自己項目的需求,額外進行的組件。雖然不能用於開源市場的使用,但是可以用於大家的學習。使用和學習是兩種模式。會使用不代表你懂,一旦有些需求不在開源項目組件的範圍之內。此時又不能清楚內部的原理,就會措手不及。接下來進行一步一步分析。
大綱:分析組件
-
alert 插件/組件
-
backtop 組件
-
sms-countDown組件(短信倒計時)
-
search 組件
-
infinitscroll 組件
-
actionSheet 組件
-
accordion 組件(手風琴)
注意 : 看這篇文章最好結合我github上發佈的組件,進行比對式觀看。
github:github.com/ziksang/cri…
demo:http://39.108.49.237:3000/dist/
一、alert組件/插件
如果在alert這種彈出式組件裏,首先要加一些背景layout的背景層動畫化,可以簡稱爲dialog動化,進行一個包裹。
對於alert組件/插件的區別使用性是在那裏?
一般來說,先會定義一個.vue文件的alert模板。
enter image description here
<template>
<div class="vux-alert">
<x-dialog :value='alertShow' :isClose='false'>
<div class="crib-confirm_hd" :style='[titleStyle]'>
<strong class="crib-confirm_title">{{title}}</strong>
</div>
<div class="crib-confirm_bd">
<slot>
<div v-html="content"></div>
</slot>
</div>
<div class="crib-confirm_ft">
<a href="javascript:;" class="crib-confirm_btn crib-confirm_btn_primary" @click="_onSubmit" :style='[buttonStyle]'>{{buttonText}}</a>
</div>
</x-dialog>
</div>
</template>
仔細看上面模板。
-
我們發現唯一特別的是對content體中的定義了一個solt。這個slot就是組件模式和插件模式的區分。如果我們想對slot裏面定義的是一個額外的展示模板或其它組件插入的話,此時只能用組件模式。
-
如果只是我們對content這個數據進填充的話,插件模式也是最方便的。
props接口的暴露
-
value 顯示消息
-
title (標題)
-
content 內容最好支持html格式
-
buttonText底部的按鈕文案
-
titleStyle 標題樣式
-
buttonStyle button樣式
event接口暴露
-
onsubmit 點擊時向外暴露事件
-
onshow 顯示時向外暴露的顯示事件
-
onhide 顯示時向暴露事件
data() {
return {
alertShow: this.value
}
},
watch: {
value(val) {
this.alertShow = val
}
},
methods: {
_onSubmit() {
this.alertShow = false
this.$emit('update:value', false)
this.$emit('on-submit')
}
}
對於value這個值來說,可以用.sync來進行簡便的操作。不需要通過$emit來進行通知。在聲明組件的時候用$on去進行監聽事件,省去了開發者這一步的事。
插件模式
首先要把原本的alert的.vue的模板給引處進來,然後用Vue.extend繼承一下。
$vm = new Alert({
el: document.createElement('div')
})
我們自己要手動進行一個創建掛載點。
//此方法是用來把confirm上的prop屬性合併到調用時的參數上
const mergeOptions = function($vm,options) {
//聲明一個默認的對象,就是comfirm上props屬性的default的值
const defaults = {}
//循環confirm屬性上的props值
for (let i in $vm.$options.props){
//不把value的值算上去,顯示改變通過watch或者改變data代理的屬性上去監聽
if(i !== 'value'){
defaults[i] = $vm.$options.props[i].default
}
}
//把confrim組件原本的值和插件傳入的options合併
const _options = Object.assign({},defaults,options)
//把confirm組件生成的實列對象再次替換成合並的屬性
for(let i in _options) {
$vm[i] = _options[i]
}
}
同時要把value顯示操作的默認定義的屬性除外,進行自己定義後覆蓋默認屬性,進行顯示。
$watcher = $vm.$watch('alertShow', (val) => {
if (!val && options && options.onHide) {
options.onHide()
}
})
同時對alertshow進行監聽,當點擊submit的時候會自己動觸發事件,然後會改變alertshow的值,然後進行你所想要的操作。
二、backtop 組件
對於backtop組件的話,要理解幾點屬性。
-
scrollTop
-
offsetHeight
let offsetTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop
let offsetHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight
scrollTop 是距離頂部的高度。
offsetHeight 元素的高度包括邊框。
那如何去判斷什麼時候顯示返回頂部按鈕呢?
this.show = offsetTop >= offsetHeight / 2;
只要通過滾動的高度大於滾動元素的高度/2來進行一個適配是最好的。
對於如何進行那方面優化。
可以進行函數節流。節流是個什麼?因爲進行滾動監聽的時候,scroll事件觸發的太頻繁了。這會影響到整個性能的問題。如果對於上下滾動也要頻繁監聽。用節點,不適用於防抖操作。
throttle(func, wait) {
var context, args;
var previous = 0;
return function () {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
},
通過時間戳來進行對比,來進行函數節流。但是有一點需要注意,在節流的同時,不要節流的時節太長。因爲mobile上面節流滾動的話,有一個自行滑動的時長。
const getScrollview = function (el) {
//拿到當前節點
let currentNode = el;
//如果有節點,並且節點不等於html ,body 並且節點類型是元素節點
while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
//拿到節點的overflowy的屬性
let overflowY = document.defaultView.getComputedStyle(currentNode).overflowY;
//如果此時屬性是scroll或者atuo 就返回此節點
if (overflowY === 'scroll' || overflowY === 'auto') {
return currentNode;
}
//否則就繼續向父節點上找
currentNode = currentNode.parentNode;
}
//一但while語句爲false的時候就直接返回window對像
return window;
};
export {getScrollview}
在外層要進行一個包裹,通overflow屬性向來進行推測, 是全局滾動還是window 下的滾動,通過while來進行判斷遞歸,來查找所對應的元素 。
三、sms-countDown 短信倒計時組件
enter image description here
對短信倒計時的認知
對於短信倒計時最重要的一點就是從父組件向sms組件通知倒計時開始的一個prop參數,用start替代。
watch : {
start (value) {
if(value === true){
this.countDown()
}
}
}
同時對start在內部進行監聽。一旦從外部傳入開始的時候,則內部進行倒計時。
countDown () {
this.myTime = 60;
let time = setInterval(()=>{
this.myTime --;
if(this.myTime === 0){
this.$emit('update:start', false);
this.myTime = this.initText;
this.flag = true;
clearInterval(time)
}
},100)
}
在這裏同樣要進行一個倒計時停止之後的向外通知。還是用.sync的雙向綁定方法,用於簡便操作。
在對於第一次倒計時和第二次倒計時的時候,也要對文案這方面進行一個設定。
firstCkText : {
type : String,
default : ''
},
secondCKText : {
type : String,
default : '重新獲取'
},
第一次點擊和第二次點擊的按鈕,也是對主要的文案的一種設計,所以對文案的變化也是要很關注的。
四、search組件
對於search組件通常能想到哪些對應的功能和想法呢?比如首次進來的時候,要進行自動獲取Input的焦點。同時要向外面暴露是否要獲取Input獲點事的Prop :autoFocus。同時也要注意,一定要在Mounted的時候才能拿 到dom元素。
mounted() {
this.autoFocus && this.$refs.input.focus()
}
一般想知道input裏面的value值是否改動的時候,通常都會用keydown或者是keyup事件。但是這裏不需要,可以時時把value的值給暴露出去,讓外層父組件可以去進行watch監聽來進行進所需要的事件操作。
watch: {
inputValue (val) {
if(val == ''){
this.value = ""
}
},
value: {
handler(val, oldvalue) {
//當值改變的時候,觸發事件
this.$emit('update:inputValue', val)
this.$emit('change-val')
},
immediate: true
}
},
同時這裏用到了.sync ,在頁面一加載的時候,立馬執行了。immediate使得value這個值立馬值行了監聽。
五、infinite-scroll 組件 (無限滾動組件)
無限滾動最關鍵的三個地方。第一滾動動底部觸發事件;第二如果有二次加載則顯示 loading;第三如何沒有二次加載則結束文案。
import { getScrollview } from '../../libs/getScrollview.js';
這個不用說,繼續尋找需要滾動範圍的元素 。
data() {
return {
isLoading: false, //是否正在加載
isDone: false, //是否加載完畢
}
},
data裏面進行之前說的兩種模式的狀態進行定義。往下看,這一處定義之後對後面有什麼好處。
this.$on('Load', () => {
this.isLoading = false;
});
this.$on('loadDone', () => {
this.isLoading = false;
this.isDone = true;
});
需要監聽兩個事件:
-
二次加載load事件。一旦進行二次加載的時候,馬上進行isloading等於false 防止重複加載。
-
通過loadDone對是否監聽完畢。如果加載完畢的話,同樣的關閉isloading 對isDone進行true的設置。
isloading和isDone分別對應的那個html 的template部分。
<div class="list-donetip" v-show='!isLoading&& isDone'>
<slot name='doneTip'>沒有更多數據了</slot>
</div>
<div class="list-loading" v-show='isLoading'>
<slot name='loadingTip'>加載中...</slot>
</div>
當isloading 爲true的時候顯示“加載中... ”當isloading 爲false的時候,isDone爲true的時候才顯示 “沒有更多數據”,這也是一個標準的無限滾動。
什麼時候對isloading和isDone設置爲true?
scrollHandler() {
if (this.isLoading || this.isDone) return;
let baseHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight
let moreHeight = this.scrollview == window ? document.body.scrollHeight : this.scrollview.scrollHeight;
let scrollTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop
if (baseHeight + scrollTop + this.distance > moreHeight) {
this.isLoading = true;
this.onInfinite()
}
if (!this.scrollview) {
console.warn('Can\'t find the scrollview!');
return;
}
},
當滾動到底部的時候,對isloading進行爲true的設置。外部組件可以調用onInfinite,進行ajax請求等操作。
在外部如何調用呢?
this.$refs.infinite.$emit('loadDone')
對組件進行 ref的設置,然後進行觸發loadDone或者load。
比對餓麼的組件,它使用的是指令的模式,內部實現還是太複雜。
如何進行一個優化
這裏就用到了函數防抖,同上面不用函數節流,用防抖。防抖跟節流的有什麼區別?防抖比較更節省性能。如果我們在設置的時間內一直滑動,則不會進行加載,只有滑動到指定的地方,則可以進行檢測,通過定時器來實現。
debounce (func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
},
六、actionSheet 組件
enter image description here
actionSheet 這裏亮點就是巧用了。call方法來改變了this的指向。這個有什麼好處?往下面看。
prpps : model 我通過Model這個數據進行遞,把文案的改變,點擊後所執行的方法一併封裝到Model數據裏來進行操作。
如果在父組件引入這個組件的時候,看下面代碼。
model: [
{ name: this.name, method(name, index) { location.href = 'tel:110' } }
],
如果進行this.指向的話,指向的是父組件。這裏就不能直接在data裏面聲明瞭。如果是異步的話,只有在ajax請求的異步裏進行聲明,把值傳入,是如何做的呢?
ff (item,index,method) {
this.$emit('update:show', false)
method.call(this.$parent,item,index)
}
在這裏通過.call來把this.的指向到父組件,就能成功的方便的調用了。
七、accordion 手風琴組件
enter image description here
對accordion組件要進行定義兩個組件合併成一個組件的模式。
-
一個最外層的包裹組件。
-
第二個是每一個item的組件。
每說 accordion-item裏面的組件,通過
this.height = (this.show ? this.$refs.content.offsetHeight: 0) + 'px';
如果需要顯示的話,讓每一個item的元素來計算高度,展現出來。
通過_uid來進行 每個item的識別。
methods : {
open(uid) {
this.$children.forEach (item => {
console.log(item._uid)
if(item._uid == uid){
item.show = !item.show
}else{
if(!this.repeat){
item.show = false
item.height = 0;
}
}
})
}
}
能夠收起的是那個item組件,則向收起的那個item組件進行一個傳遞。本質上通過index找到子組件對應的項也可以實現。因爲_uid是唯一的。這一步也是省了一些簡便的操作。
在這裏把一些突出的組件,來開闊我們的思想,來進行一其它組件的封裝 ,也可以基於這些組件對自己所需要的項目根據不同的需求來封裝。
最後,尤大說了一句話,我最喜歡的就是看別人代碼。記住這句話,你的組件能寫的又快又好。
這是我最近在Gitchat上交流的一篇文章。如果大家對Vue感興趣的話,可以加入我的Vue討論微信羣。有什麼問題可以在羣裏交流。