在封裝Vue組件時灸异,我依舊會交叉使用函數(shù)式組件的方式來實現(xiàn)亥至。關(guān)于函數(shù)式組件,我們可以把它想像成組件里的一個函數(shù),入?yún)⑹卿秩旧舷挛?render context)奠支,返回值是渲染好的HTML(VNode)。它比較適用于外層組件僅僅是對內(nèi)層組件的一次邏輯封裝涌献,而渲染的模板結(jié)構(gòu)變化擴(kuò)展不多的情況胚宦,且它一定是無狀態(tài)、無實例的,無狀態(tài)就意味著它沒有created枢劝、mounted井联、updated等Vue的生命周期函數(shù),無實例就意味著它沒有響應(yīng)式數(shù)據(jù)data和this上下文您旁。
我們先來一個簡單的Vue函數(shù)式組件的例子吧烙常,然后照著這個例子來詳細(xì)介紹一下。
export default {
functional: true,
props: {},
render(createElement, context) {
return createElement('span', 'hello world')
}
}
Vue提供了一個functional開關(guān)鹤盒,設(shè)置為true后蚕脏,就可以讓組件變?yōu)闊o狀態(tài)、無實例的函數(shù)式組件侦锯。因為只是函數(shù)驼鞭,所以渲染的開銷相對來說較小。
函數(shù)式組件中的Render函數(shù)尺碰,提供了兩個參數(shù)createElement和context挣棕,我們先來了解下第一個參數(shù)createElement。
createElement說白了就是用來創(chuàng)建虛擬DOM節(jié)點VNode的亲桥。它接收三個參數(shù)洛心,第一個參數(shù)可以是DOM節(jié)點字符串,也可以是一個Vue組件题篷,還可以是一個返回字符串或Vue組件的函數(shù)词身;第二個參數(shù)是一個對象,這個參數(shù)是可選的番枚,定義了渲染組件所需的參數(shù)法严;第三個參數(shù)是子級虛擬節(jié)點,可以是一個由createElement函數(shù)創(chuàng)建的組件葫笼,也可以是一個普通的字符串如:'hello world'渐夸,還可以是一個數(shù)組,當(dāng)然也可以是一個返回字符串或Vue組件的函數(shù)渔欢。
createElement有幾點需要注意:
createElement第一個參數(shù)若是組件,則第三個參數(shù)可省略瘟忱,即使寫上去也無效奥额;
render函數(shù)在on事件中可監(jiān)聽組件$emit發(fā)出的事件
在2.3.0之前的版本中,如果一個函數(shù)式組件想要接收prop访诱,則props選項是必須的垫挨。在2.3.0或以上的版本中,你可以省略props選項触菜,組件上所有的attribute都會被自動隱式解析為prop九榔。
函數(shù)式組件中Render的第二個參數(shù)是context上下文,data、props哲泊、slots剩蟀、children以及parent都可以通過context來訪問。
在2.5.0及以上版本中切威,如果你使用了單文件組件育特,那么基于模板的函數(shù)式組件可以這樣聲明:<template functional></template>
, 但是如果Vue組件中的render函數(shù)存在先朦,則Vue構(gòu)造函數(shù)不會從template選項或通過el選項指定的掛載元素中提取出的HTML模板編譯渲染函數(shù)缰冤,也就是說一個組件中templete和render函數(shù)不能共存,如果一個組件中有了templete喳魏,即使有了render函數(shù)棉浸,render函數(shù)也不會執(zhí)行,因為template選項的優(yōu)先級比render選項的優(yōu)先級高刺彩。
到這里迷郑,Vue函數(shù)式組件介紹的就差不多了,我們就來看看Element的表格組件是如何通過函數(shù)式組件來實現(xiàn)封裝的吧迂苛。
效果圖:
1三热、所封裝的table組件:
<template>
<el-table :data="config.data" style="width: 100%" v-on="cfg.on" v-bind="attrs" v-loading="config.loading">
<el-table-column v-if="cfg.hasCheckbox" v-bind="selectionAttrs" type="selection" width="55" label="xx" />
<el-table-column v-for="n in cfg.headers" :prop="n.prop" :label="n.name" :key="n.prop" v-bind="{...columnAttrs, ...n.attrs}">
<template slot-scope="{row}">
<Cell :config="n" :data="row" />
</template>
</el-table-column>
</el-table>
</template>
<script>
import Cell from './cell'
export default {
components: {
Cell,
},
props: {
config: Object,
},
data(){
return {
columnAttrs: {
align: 'left',
resizable: false,
},
cfg: {
on: this.getTableEvents(),
attrs: {
border: true,
stripe: true,
},
...this.config,
},
checked: [],
}
},
computed: {
selectionAttrs(){
let {selectable, reserveSelection = false} = this.config || {}, obj = {};
// checkBox是否可以被選中
if(selectable && typeof selectable == 'function'){
Object.assign(obj, {
selectable,
})
}
//reserve-selection僅對type=selection的列有效,類型為Boolean三幻,為true則會在數(shù)據(jù)更新之后保留之前選中的數(shù)據(jù)(需指定 row-key)
if(reserveSelection){
Object.assign(obj, {
'reserve-selection': reserveSelection,
})
}
return obj;
},
attrs(){
let {config: {spanMethod, rowKey}, cfg: {attrs}} = this;
// 合并單元格 - spanMethod是父組件傳過來的合并單元格的方法就漾,請參照element合并單元格
if(spanMethod && typeof spanMethod == 'function'){
Object.assign(attrs, {
'span-method': spanMethod,
})
}
// 表格跨頁選中,需要設(shè)置row-key和reserve-selection念搬,reserve-selection只能且必須設(shè)置在type為selection的el-table-column上
if(rowKey && typeof rowKey == 'function'){
Object.assign(attrs, {
'row-key': rowKey,
})
}
return attrs;
},
},
methods: {
getTableEvents(){
let {hasCheckbox = false} = this.config, events = {}, _this = this;
if(hasCheckbox){
// 綁定事件
Object.assign(events, {
'selection-change'(v){
_this.checked = v;
},
});
}
return events;
},
getChecked(){
return this.checked;
},
},
}
</script>
2抑堡、分頁實現(xiàn)pagination.vue:
<template>
<div>
<Table :config="cfg" />
<div class="pagination" v-if="showPage">
<el-pagination
layout="prev, pager, next"
background
:total="page.total"
:page-size="page.pageSize"
:current-page="page.pageIndex"
@current-change="loadPage"
/>
</div>
</div>
</template>
<script>
import Table from './index'
export default {
components: {
Table,
},
props: {
config: Object,
},
data(){
return {
cfg: {
...this.config,
data: [],
loading: true,
},
page: {
pageSize: this.config.pageSize || 10,
pageIndex: 1,
total: 0,
},
}
},
created(){
this.load();
},
computed: {
showPage(){
let {pageSize, total} = this.page;
return pageSize < total;
},
},
methods: {
load(p = {}){
let {pageSize, pageIndex} = this.page, {loadData = () => Promise.resolve({})} = this.config || {};
this.cfg.loading = true;
// 這里loadData的參數(shù)在初始化時只有分頁所需的pageIndex和pageSize,至于接口需要的其他參數(shù)朗徊,是在父組件的config的loadData中傳遞首妖,這里不再接收其他參數(shù)
loadData({...p, pageIndex, pageIndex}).then(({data, total}) => {
this.cfg.data = data;
this.page.pageIndex = index;
this.page.total = total;
this.cfg.loading = false;
});
},
loadPage(index){
this.page.pageIndex = index
this.load();
},
// 一般在點擊查詢按鈕或局部刷新表格列表時,可調(diào)用此方法爷恳,如果不傳參數(shù)有缆,則默認(rèn)從第一頁開始
reload(p = {}){
this.page.pageIndex = 1
this.load(p);
},
},
}
</script>
<style scoped>
.pagination{
margin-top: 16px;
text-align: right;
}
</style>
3、匯總表格每一列的cell.js:
import * as Components from './components';
let empty = '-'
export default {
props: {
config: Object,
data: Object,
},
functional: true,
render: (h, c) => {
let {props: {config = {}, data = {}}} = c, {prop, type = 'Default'} = config, value = data[prop] || config.value, isEmpty = value === '' || value === undefined;
return isEmpty ? h(Components.Default, {props: {value: empty}}) : h(Components[type], {props: {value, empty, data, ...config}});
}
}
4温亲、不同于封裝React AntD的table表格組件時將表格的每一列的渲染都集中在了一個table.js中棚壁,本次封裝將每一列的渲染單獨分開成多個vue組件,最后再合并在一個components.js文件中一起進(jìn)行匹配栈虚。
1)整合文件components.js:
import Date from './Date';
import Default from './Default';
import Currency from './Currency';
import Enum from './Enum';
import Action from './Action';
import Link from './Link';
import Format from './Format';
import Popover from './Popover';
export {
Default,
Date,
Currency,
Enum,
Action,
Link,
Format,
Popover,
}
2)日期列Date.vue
<template functional>
<span>{{props.value | date(props.format)}}</span>
</template>
3)默認(rèn)列Default.vue
<template functional>
<span>{{props.value}}</span>
</template>
4)金額千分位列Currency.vue
<template functional>
<span>{{props.value | currency}}</span>
</template>
5)映射列Enum.js
let mapIdAndKey = list => list.reduce((c, i) => ({...c, [i.key]: i}), {});
let STATUS = {
order: mapIdAndKey([
{
id: 'draft',
key: 'CREATED',
val: '未提交',
},
{
id: 'pending',
key: 'IN_APPROVAL',
val: '審批中',
},
{
id: 'reject',
key: 'REJECT',
val: '審批駁回',
},
{
id: 'refuse',
key: 'REFUSE',
val: '審批拒絕',
},
{
id: 'sign',
key: 'CONTRACT_IN_SIGN',
val: '合同簽署中',
},
{
id: 'signDone',
key: 'CONTRACT_SIGNED',
val: '合同簽署成功',
},
{
id: 'lendDone',
key: 'LENDED',
val: '放款成功',
},
{
id: 'lendReject',
key: 'LOAN_REJECT',
val: '放款駁回',
},
{
id: 'cancel',
key: 'CANCEL',
val: '取消成功',
},
{
id: 'inLend',
key: 'IN_LOAN',
val: '放款審批中',
},
]),
monitor: mapIdAndKey([
{
key: '00',
val: '未監(jiān)控',
},
{
key: '01',
val: '監(jiān)控中',
},
]),
}
export default {
functional: true,
render(h, {props: {value, Enum, empty}, parent}){
let enums = Object.assign({}, STATUS, parent.$store.getters.dictionary),
{name = '', getVal = (values, v) => values[v]} = Enum, _value = getVal(enums[name], value);
if( _value === undefined) return h('span', _value === undefined ? empty : _value);
let {id, val} = _value;
return h('span', {staticClass: id}, [h('span', val)]);
}
}
6)操作列Action.js
const getAcitons = (h, value, data) => {
let result = value.filter(n => {
let {filter = () => true} = n;
return filter.call(n, data);
});
return result.map(a => h('span', {class: 'btn', on: {click: () => a.click(data)}, key: a.prop}, a.label))
}
export default {
functional: true,
render: (h, {props: {value, data}}) => {
return h('div', {class: 'action'}, getAcitons(h, value, data))
},
}
7)帶有可跳轉(zhuǎn)鏈接的列Link.vue
<template>
<router-link :to="{ path, query: params }">{{value}}</router-link>
</template>
<script>
export default {
props: {
data: Object,
value: String,
query: {
type: Function,
default: () => {
return {
path: '',
payload: {}
}
}
},
},
computed: {
// 路由path
path(){
const { path } = this.query(this.data)
return path
},
params(){
const { payload } = this.query(this.data)
return payload
},
},
}
</script>
8)自定義想要展示的數(shù)據(jù)格式Format.vue
<template functional>
<div v-html="props.format(props.value)" />
</template>
9)當(dāng)內(nèi)容過多需要省略并在鼠標(biāo)移入后彈出一個提示窗顯示全部內(nèi)容的列Popover.vue
<template functional>
<el-popover
placement="top-start"
width="300"
trigger="hover"
popper-class="popover"
:content="props.value">
<span slot="reference" class="popover-txt">{{props.value}}</span>
</el-popover>
</template>
<style scoped>
.popover-txt{
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
display: block;
cursor: pointer;
}
</style>
從以上代碼中可以看出袖外,我既使用了基于render函數(shù)類型的函數(shù)式組件也使用了基于模板的函數(shù)式組件,主要是為了在封裝時的方便魂务,畢竟使用render這個最接近編譯器的函數(shù)還是有點麻煩的曼验,不如基于模板的函數(shù)式組件來的方便泌射。
5、使用封裝后的表格table組件:
<template>
<div style="margin: 20px;">
<el-button type="primary" v-if="excelExport" @click="download">獲取勾選的表格數(shù)據(jù)</el-button>
<Table :config="config" ref="table" />
</div>
</template>
<script>
import Table from '@/components/pagination'
export default {
components: {
Table,
},
data() {
return {
config: {
headers: [
{prop: 'contractCode', name: '業(yè)務(wù)編號', attrs: {width: 200, align: 'center'}},
{prop: 'payeeAcctName', name: '收款賬戶名', type: 'Link', query: row => this.query(row), attrs: {width: 260, align: 'right'}},
{prop: 'tradeAmt', name: '付款金額', type: 'Currency'},
{prop: 'status', name: '操作狀態(tài)', type: 'Enum', Enum: {name: 'order'}},
{prop: 'statistic', name: '預(yù)警統(tǒng)計', type: 'Format', format: val => this.format(val)}, //自定義展示自己想要的數(shù)據(jù)格式
{prop: 'reason', name: '原因', type: 'Popover'},
{prop: 'payTime', name: '付款時間', type: "Date", format: 'yyyy-MM-dd hh:mm:ss'}, //不設(shè)置format的話鬓照,日期格式默認(rèn)為yyyy/MM/dd
{prop: 'monitorStatus', name: '當(dāng)前監(jiān)控狀態(tài)', type: 'Enum', Enum: {name: 'monitor'}},
].concat(this.getActions()),
//通過接口獲取列表數(shù)據(jù) - 這里的參數(shù)p就是子組件傳過來的包含分頁的參數(shù)
loadData: p => request.post('permission/list', {...this.setParams(), ...p}),
hasCheckbox: true,
selectable: this.selectable,
reserveSelection: false,
rowKey: row => row.id,
},
status: "01",
permission: ["handle", "pass", "refuse", "reApply", 'export']
}
},
computed: {
handle() {
return this.permission.some(n => n == "handle");
},
pass() {
return this.permission.some(n => n == "pass");
},
reject() {
return this.permission.some(n => n == "reject");
},
refuse() {
return this.permission.some(n => n == "refuse");
},
excelExport(){
return this.permission.some(n => n == "handle") && this.permission.some(n => n == "export");
},
},
methods: {
getActions(){
return {prop: 'action', name: '操作', type: "Action", value: [
{label: "查看", click: data => {console.log(data)}},
{label: "辦理", click: data => {}, filter: ({status}) => status == 'CREATED' && this.handle},
{label: "通過", click: data => {}, filter: ({status}) => status == 'PASS' && this.pass},
{label: "駁回", click: data => {}, filter: ({status}) => status == 'REJECT' && this.reject},
{label: "拒絕", click: data => {}, filter: ({status}) => status == 'CREATED' && this.refuse},
]}
},
setParams(){
return {
name: '測試',
status: '01',
type: 'CREATED',
}
},
query(row){
return {
path: '/otherElTable', // 路由path
payload: {
id: row.id,
type: 'link'
}
}
},
format(val){
let str = '';
val.forEach(t => {
str += '<span style="margin-right:5px;">' + t.total + '</span>';
})
return str;
},
selectable({status}){
return status == "REFUSE" ? false : true
},
download(){
console.log(this.$refs.table.getChecked())
},
},
};
</script>
<style>
.action span{margin-right:10px;color:#359C67;cursor: pointer;}
</style>
關(guān)于金額千分位和時間戳格式化的實現(xiàn)熔酷,這里就不再貼代碼了,可自行實現(xiàn)颖杏。
------------------------------------- 2020年11月17日更新 -------------------------------------
最近又想了一下封裝的這個table組件纯陨,想著說在原來封裝的基礎(chǔ)上還有沒有其他的實現(xiàn)方法,比如我不想在原來定義的headers數(shù)組后邊再concat一個關(guān)于操作列的數(shù)組留储,再比如表格的某一列的數(shù)據(jù)處理方法不包含在我們之前所封裝的那些方法當(dāng)中翼抠,或者說作為第一次使用這個table組件的前端開發(fā)人員,我不太習(xí)慣你的那種寫法获讳,那我可不可以在你封裝的基礎(chǔ)上自己寫一些處理方法呢阴颖,答案是可以的,當(dāng)然我們說既然已經(jīng)封裝好了組件丐膝,那么大家就按照一個套路來量愧,省時又省力,何樂而不為呢帅矗?但有一說一偎肃,我們本著學(xué)習(xí)的態(tài)度,本著藝多不壓身的出發(fā)點來看的話浑此,多學(xué)多思考多動手累颂,總歸是有益于進(jìn)步的。只是在實際的開發(fā)過程中凛俱,我們盡量要選擇一種封裝方式紊馏,然后大家一起遵守這個約定就好了。
其實說了這么多廢話蒲犬,這次變更也是沒有多大力度的朱监,只是在原來封裝的基礎(chǔ)上增加了插槽而已≡#看過本篇博客的你一定還記得我封裝的代碼中有一段專門用來處理每一列數(shù)據(jù)的代碼吧:
<Cell :config="n" :data="row" />
對赫编,就是它。對于它奋隶,我不想再多說了沛慢,上邊已經(jīng)做了介紹了。本次變更达布,我們主要用到的是插槽。
插槽這個API逾冬,VUE的官網(wǎng)和網(wǎng)上的各種文章介紹已經(jīng)講的很清楚了黍聂,它大概分為:默認(rèn)插槽(也有人管它叫匿名插槽)躺苦、具名插槽和作用域插槽。關(guān)于它們的介紹产还,請自行查閱官網(wǎng)或網(wǎng)上的各種文章資料匹厘。本次變更主要用到的就是具名插槽和作用域插槽。其實實現(xiàn)起來很簡單脐区,就是在<Cell :config="n" :data="row" />
的外邊再包一層具名插槽就可以了愈诚。
<slot :name="n.prop" :row="row"><Cell :config="n" :data="row" /></slot>
就醬。
接下來牛隅,我們就可以回答上邊我們提出的那些問題了炕柔。來看答案:
<Table :config="config" ref="table">
<template #payTime="{row}">
{{row.payTime | date('yyyy-MM-dd hh:mm:ss')}}
</template>
<template #customize="{row}">
{{customize(row.customize)}}
</template>
<template #opt="{row}">
<div class="action">
<span>查看</span>
<span v-if="row.status == 'CREATED' && handle">辦理</span>
<span v-if="row.status == 'PASS' && pass">通過</span>
<span v-if="row.status == 'REJECT' && reject">駁回</span>
<span v-if="row.status == 'REFUSE' && refuse">拒絕</span>
</div>
</template>
</Table>
以上就是對某些特殊情況,而你又不想使用我最開始封裝的那些方法來實現(xiàn)媒佣,那么可以匕累,我就再為你提供一個其他的“特殊服務(wù)”。這里要注意默伍,如果你使用插槽來自己渲染數(shù)據(jù)欢嘿,那么在headers數(shù)組中,你需要提供表格頭部的渲染也糊,而不需要再加入type字段即可炼蹦。
比如最開始渲染表格的日期列時我們是這么寫的{prop: 'payTime', name: '付款時間', type: "Date", format: 'yyyy-MM-dd hh:mm:ss'}
那么如果你使用插槽來自己渲染數(shù)據(jù),這里的寫法就要變成了這樣{prop: 'payTime', name: '付款時間'}
還有之前我們定義操作列是在headers數(shù)組的后邊再concat了一個數(shù)組狸剃,如果你使用插槽來自己渲染數(shù)據(jù)掐隐,那么就不需要再concat一個數(shù)組了,而是在headers數(shù)組中再加一個{prop: 'opt', name: '操作'}
就可以了捕捂。
其實瑟枫,這次變更說的是在原來的基礎(chǔ)上重新包裝了一層插槽,那么對于那些不需要我們自行處理數(shù)據(jù)指攒,只需直接展示接口返回的數(shù)據(jù)的情況慷妙,我們在使用這個封裝的table組件時也不需要進(jìn)行什么特殊處理,更不需要像上邊使用插槽那樣去定義允悦,只要還是跟之前一樣在headers數(shù)組中正常定義就可以了膝擂。因為插槽嘛,你不定義具名插槽隙弛,也不定義默認(rèn)插槽架馋,那么插槽中顯示的就是包裹在插槽標(biāo)簽slot中的<Cell :config="n" :data="row" />
明白了吧。
多說一句全闷,你說我不想使用插槽去處理日期叉寂、金額千分位這些列,那么你依舊可以根據(jù)上邊我介紹的插槽的原理总珠,在headers數(shù)組中依舊這樣定義就OK了:
{prop: 'tradeAmt', name: '付款金額', type: 'Currency'},
{prop: 'payTime', name: '付款時間', type: "Date"},
寫到這里屏鳍,其實我想說勘纯,即使加上了插槽,那么對之前的那些使用方法來說钓瞭,基本沒啥影響驳遵,你該怎么用還怎么用,我只是給你提供了更多的選擇而已山涡。