本文是Vue實戰(zhàn)系列的第六篇文章,主要介紹Falcon項目中通用 Table 組件的開發(fā)和使用 。Falcon項目地址:https://github.com/thierryxing/Falcon
通用 Table 組件 TableBox
隨著業(yè)務(wù)的發(fā)展和功能的增多,我們發(fā)現(xiàn)不少頁面都具備相似的功能,這里舉幾個比較俗的例子:可以多選的下拉菜單,帶輸入的對話框雄可,日期選擇器等等,于是我們會想辦法將這些共有的功能抽取成一個個公共組件缠犀,以便能夠在不同的頁面或業(yè)務(wù)中使用数苫。
對于一個中后臺類的項目,一個比較常見的展示形式就是Table了夭坪,相信大家都不陌生文判,如下圖所示:
一個Table通常由如下幾個部分構(gòu)成:
- 標(biāo)題
- 表頭
- 表格內(nèi)容
- 分頁控件
除此之外,由于 Table 中的數(shù)據(jù)往往都是從后端獲取的室梅,所以這個包含 Table 的頁面還需要發(fā)起一個請求戏仓,并且將最終的內(nèi)容渲染在表格之內(nèi),請求的過程由于是異步的亡鼠,所以需要給用戶展示一個 Loading 動畫赏殃;當(dāng)請求數(shù)據(jù)為空時,需要顯示一個占位的空元素控件间涵。
在 Falcon 項目的實踐中仁热,我們發(fā)現(xiàn),每一個頁面中的 Table 除了行數(shù)勾哩,列數(shù)抗蠢,及單元格的內(nèi)容不同之外举哟,其它的地方,包括樣式迅矛,分頁及數(shù)據(jù)處理邏輯都是一樣的妨猩,每次新增一個這樣的頁面無非就是拷貝粘貼了,那么在這種情況下秽褒,我們抽取出了一個通用的 Table 組件壶硅,取名為:TableBox。
說到這里插一個題外話:
我們在編寫前端代碼的過程中有時會感到困惑销斟,究竟什么時候應(yīng)該將模塊抽取出來庐椒,而不是寫死在頁面中呢?
關(guān)于這個問題我認(rèn)為蚂踊,如果一個功能只出現(xiàn)在了一個或兩個頁面中约谈,往往是沒有必要處理的,因為一兩個功能的重復(fù)還不足以說明問題悴势,也很難看出其中的共性窗宇,如果強(qiáng)行抽取的話,反而會增加維護(hù)的負(fù)擔(dān)特纤;如果出現(xiàn)的地方超過了兩處,那么我們就需要考慮將這個功能進(jìn)行抽取了侥加,我也常常和 Team 的人說:“如果一個功能你拷貝粘貼了1次捧存,沒關(guān)系,不用糾結(jié)担败;2次的話昔穴,就得考慮其復(fù)用性和組件化了”。
當(dāng)然提前,以上內(nèi)容只適用于那些初期開發(fā)過程中無法預(yù)測未來變化的項目吗货,如果剛開始產(chǎn)品設(shè)計的時候,就能夠充分的預(yù)見和考慮未來的業(yè)務(wù)發(fā)展狈网,并且給出詳細(xì)的產(chǎn)品及UI設(shè)計方案宙搬,那么就另當(dāng)別論了。
回到我們的主題拓哺,抽取這個 TableBox 其實并不是一氣呵成的勇垛,而是在業(yè)務(wù)迭代中,不斷地發(fā)現(xiàn)新的場景士鸥,新的問題闲孤,帶著這些問題我們不斷的優(yōu)化 TableBox,最終達(dá)到一個較為完整的狀態(tài)烤礁。這也符合 Vue 本身漸進(jìn)式的理念讼积。接下來我們花些時間肥照,一起探討一下這些場景和問題:
場景&問題1:Table 數(shù)據(jù)獲取和渲染
我們發(fā)現(xiàn),對于不同的頁面勤众,只要帶有 Table 的舆绎,其數(shù)據(jù)都需要從遠(yuǎn)端服務(wù)器獲取,一般情況下决摧,我們會在每個業(yè)務(wù)中都去寫一下這個網(wǎng)絡(luò)獲取數(shù)據(jù)的邏輯亿蒸,但是,如果仔細(xì)想想掌桩,你就會發(fā)現(xiàn)边锁,其實這類列表數(shù)據(jù)獲取和處理的邏輯都是一樣的。所以針對這個情況波岛,我們只要和后端協(xié)商好列表相關(guān)的統(tǒng)一 API 數(shù)據(jù)結(jié)構(gòu)茅坛,如:
{
"status": 0,
"message": "",
"data": {
"list": [
...
],
"total": 30 //總個數(shù)
}
}
那么數(shù)據(jù)獲取,渲染则拷,Loading 動畫展示隱藏贡蓖,分頁加載等操作都可以在 TableBox 中完成。
這個組件需要的只是向外暴露出數(shù)據(jù)請求的 API 地址及各種參數(shù):
props: {
// API URL
url: {
type: String,
default: ''
},
// URL路徑中的參數(shù)煌茬,如:/posts/#{id}/comments
pathParams: {
type: Object,
default: () => {}
},
// Query參數(shù)
options: {
type: Object,
default: function () { return {params: {}} }
}
}
然后寫好對應(yīng)的獲取數(shù)據(jù)的 fetchData 方法:
fetchData (page) {
this.showLoading()
this.options.params.page = page >= 1 ? page : 1
NetWorking
.doGet(this.url, this.pathParams, this.options)
.then(response => {
this.items = response.data.list
this.paginate.totalRows = response.data.total
this.hideLoading()
}, () => {
this.hideLoading()
})
}
這樣對于調(diào)用者來說斥铺,只需要簡單的傳入相關(guān) API 地址及參數(shù)就可以了,數(shù)據(jù)加載的事情讓 TableBox 去處理就好了坛善,非常的方便晾蜘。
場景&問題2:適配不同的表格列
因為 TableBox 組件本身是和業(yè)務(wù)無關(guān)的,所以其肯定無法知道我的這個 Table 的表頭是什么眠屎,有多少行剔交,也無法知道每一行展示什么數(shù)據(jù),這些內(nèi)容全部應(yīng)該由父組件告知 TableBox改衩。
要實現(xiàn)以上的功能岖常,我們可以借助于 Vue 本身提供的強(qiáng)大的工具 Slot,如果簡單點說葫督,大家可以把 Slot 理解為一個坑位竭鞍,因為大多數(shù)情況下,組件自己無法預(yù)先知道某塊區(qū)域放置什么內(nèi)容候衍,那么組件可以先將個區(qū)域放置一個 Slot笼蛛,就是挖個坑,當(dāng)父組件引入子組件時蛉鹿,會告訴子組件往這個坑位中填充什么樣的內(nèi)容滨砍。
回到我們的 TableBox 組件,我們首先挖兩個坑(放置兩個 Slot ),命名為 ths 和 item 惋戏,分別用于表頭和行列表內(nèi)容:
<table class="table table-hover table-bordered">
<tbody>
<slot name="ths"></slot>
<slot name="item"
v-for="item in items"
:item="item">
</slot>
</tbody>
</table>
這樣對于表頭的數(shù)據(jù)领追,可以由引入 TableBox 的父組件來指定,用法如下响逢,其中 slot='ths' 就是剛才我們在 TableBox
中放置的 Slot
<tr slot="ths">
<th>ID</th>
<th>Project</th>
<th>Version</th>
...
</tr>
同樣绒窑,對于每一行的內(nèi)容,也是由引入 TableBox 的父組件來指定舔亭,用法如下:
<template slot="item" scope="props">
<tr>
<td>
{{ props.item.id }}
</td>
<td>
{{ props.item.project.name }}
</td>
<td>
<div>
{{ props.item.version }}
</div>
<div>
{{ props.item.number }}
</div>
</td>
</tr>
</template>
問題&場景3:表格數(shù)據(jù)自刷新
在開發(fā)業(yè)務(wù)的過程中些膨,遇到一個場景:當(dāng)頁面數(shù)據(jù)已經(jīng)全部加載完畢后,在某些場景下需要改變 Table 中某些數(shù)據(jù)的狀態(tài)(刪除某列或改變某一列的數(shù)據(jù))钦铺。
這里具體舉個 Falcon 中的實際例子:
我們允許用戶給每個項目分配多個環(huán)境订雾,以區(qū)分測試,生產(chǎn)矛洞,開發(fā)和各種自定義的場景洼哎,在每個環(huán)境下,用戶可以設(shè)置不同的 Git Branch 沼本。用戶點擊 Choose Branch 按鈕后噩峦,會觸發(fā)一個請求到后端,變更當(dāng)前環(huán)境的 Git Branch抽兆, 修改成功后該列表項的按鈕會顯示為 Current Branch识补。
由于以上邏輯都是在引入了 TableBox 的父組件中完成的,其能夠控制數(shù)據(jù)的刷新辫红,由于 場景1 中我們已經(jīng)把數(shù)據(jù)請求的邏輯都封裝在了 TableBox 中李请,所以我們需要讓其向外暴露出一個 Boolean 屬性:reloadData,當(dāng)此屬性為 true 時厉熟,TableBox 會重新請求一次API,并刷新列表较幌。
reloadData: {
type: Boolean,
default: false
}
同理揍瑟,由于操作數(shù)據(jù)是由父組件發(fā)起的,所父組件中也需要有同樣的屬性乍炉,并且和 TableBox 中的 reloadData 保持?jǐn)?shù)據(jù)同步绢片,這里用到了 Vue 2.3 版本增加的一個 .sync 修飾符進(jìn)行處理 。
<table-box :url="url" :pathParams="pathParams" :reloadData.sync="reloadData">
這樣岛琼,當(dāng) reloadData 在數(shù)據(jù)更新完畢后還原為 false 狀態(tài)時底循,我們可以顯示的觸發(fā)一個 emit 事件:
hideLoading () {
this.showOverlay = false
this.$emit('update:reloadData', false)
}
問題&場景4:父組件獲取表格數(shù)據(jù)
由于目前所有的數(shù)據(jù)獲取都是在 TableBox 內(nèi)部處理的,所以父組件本身是無法直接獲取到數(shù)據(jù)的槐瑞。但是在某些情況下熙涤,我們又希望父組件能夠獲取到數(shù)據(jù),以便能夠在頂層進(jìn)行更靈活的處理,這時我們就需要在 TableBox 內(nèi)部將數(shù)據(jù)拋出祠挫。
拋出的方式也很簡單那槽,我們可以使用 emit 方法拋出一個事件。根據(jù)這個思路我們改造一下上文提到的 fetchData 方法:
fetchData (page) {
this.showLoading()
this.options.params.page = page >= 1 ? page : 1
NetWorking
.doGet(this.url, this.pathParams, this.options)
.then(response => {
this.items = response.data.list
this.paginate.totalRows = response.data.total
// 拋出data完整對象等舔,以便父組件使用
this.$emit('afterFetchData', response.data)
this.hideLoading()
}, () => {
this.hideLoading()
})
}
然后在父組件中監(jiān)聽這個事件骚灸,這樣就能獲取到完整的數(shù)據(jù)了。
<table-box :url="url" :pathParams="pathParams" @afterFetchData="afterFetchData" :reloadData="reloadData">
...
methods: {
afterFetchData (data) {
this.slb = data
},
...
總結(jié)
解決了以上4個場景的問題后慌植,我們這個 TableBox 可以說告一段落了甚牲,后續(xù)如果有遇到新的場景,新的問題蝶柿,我們只需要不斷的去優(yōu)化去完善這個組件即可丈钙。
到目前為止,TableBox 已經(jīng)應(yīng)用到了我們內(nèi)部的三個后臺項目約幾十個頁面中只锭,可以說大大節(jié)省了我們的時間著恩,提升了整體效率。
并且隨著這樣的組件越來越多蜻展,甚至我們的后端工程師經(jīng)過簡短的培訓(xùn)喉誊,也可以上手部分前端頁面的開發(fā)了。
最后附上 TableBox 的地址:https://github.com/thierryxing/Falcon/blob/mock/src/components/global/TableBox.vue