上週分享了幾篇關於React組件封裝方面的博文,這周就來分享幾篇關於Vue組件封裝方面的博文,也好讓你們能更好地瞭解React和Vue在組件封裝方面的區別。html
在封裝Vue組件時,我依舊會交叉使用函數式組件的方式來實現。關於函數式組件,咱們能夠把它想像成組件裏的一個函數,入參是渲染上下文(render context),返回值是渲染好的HTML(VNode)。它比較適用於外層組件僅僅是對內層組件的一次邏輯封裝,而渲染的模板結構變化擴展很少的狀況,且它必定是無狀態、無實例的,無狀態就意味着它沒有created、mounted、updated等Vue的生命週期函數,無實例就意味着它沒有響應式數據data和this上下文。vue
咱們先來一個簡單的Vue函數式組件的例子吧,而後照着這個例子來詳細介紹一下。數組
export default { functional: true, props: {}, render(createElement, context) { return createElement('span', 'hello world') } }
Vue提供了一個functional開關,設置爲true後,就可讓組件變爲無狀態、無實例的函數式組件。由於只是函數,因此渲染的開銷相對來講較小。函數
函數式組件中的Render函數,提供了兩個參數createElement和context,咱們先來了解下第一個參數createElement。oop
createElement說白了就是用來建立虛擬DOM節點VNode的。它接收三個參數,第一個參數能夠是DOM節點字符串,也能夠是一個Vue組件,還能夠是一個返回DOM節點字符串或Vue組件的函數;第二個參數是一個對象,這個參數是可選的,定義了渲染組件所需的參數;第三個參數是子級虛擬節點,能夠是一個由createElement函數建立的組件,也能夠是一個普通的字符串如:'hello world',還能夠是一個數組,固然也能夠是一個返回DOM節點字符串或Vue組件的函數。this
createElement有幾點須要注意:url
createElement第一個參數如果組件,則第三個參數可省略,即便寫上去也無效;spa
render函數在on事件中可監聽組件$emit發出的事件excel
在2.3.0以前的版本中,若是一個函數式組件想要接收prop,則props選項是必須的。在2.3.0或以上的版本中,你能夠省略props選項,組件上全部的attribute都會被自動隱式解析爲prop。code
函數式組件中Render的第二個參數是context上下文,data、props、slots、children以及parent均可以經過context來訪問。
在2.5.0及以上版本中,若是你使用了單文件組件,那麼基於模板的函數式組件能夠這樣聲明:<template functional></template>
, 可是若是Vue組件中的render函數存在,則Vue構造函數不會從template選項或經過el選項指定的掛載元素中提取出的HTML模板編譯渲染函數,也就是說一個組件中templete和render函數不能共存,若是一個組件中有了templete,即便有了render函數,render函數也不會執行,由於template選項的優先級比render選項的優先級高。
到這裏,Vue函數式組件介紹的就差很少了,咱們就來看看Element的表格組件是如何經過函數式組件來實現封裝的吧。
效果圖:
一、所封裝的table組件
<template> <el-table :data="config.tableData" style="width: 100%" v-on="cfg.on" v-bind="cfg.attrs"> <el-table-column v-if="cfg.hasCheckbox" 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: [], } }, 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>
二、彙總表格每一列的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}}); } }
三、不一樣於封裝React AntD的table表格組件時將表格的每一列的渲染都集中在了一個table.js中,本次封裝將每一列的渲染單獨分開成多個vue組件,最後再合併在一個components.js文件中一塊兒進行匹配。
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 Loop from './Loop'; import Popover from './Popover'; export { Default, Date, Currency, Enum, Action, Link, Loop, Popover, }
2)日期列Date.vue
<template functional> <span>{{props.value | date(props.format)}}</span> </template>
3)默認列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: '未監控', }, { key: '01', val: '監控中', }, ]), } 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)帶有可跳轉連接的列Link.vue
<template functional> <router-link :to="{ path: props.url, query: {id: props.data.id} }">{{props.value}}</router-link> </template>
8)可循環展現數組數據的列Loop.vue
<template functional> <div v-html="props.Loop(props.value)" /> </template>
9)當內容過多須要省略並在鼠標移入後彈出一個提示窗顯示所有內容的列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函數類型的函數式組件也使用了基於模板的函數式組件,主要是爲了在封裝時的方便,畢竟使用render這個最接近編譯器的函數仍是有點麻煩的,不如基於模板的函數式組件來的方便。
四、使用封裝後的表格table組件:
<template> <div style="margin: 20px;"> <el-button type="primary" v-if="excelExport" @click="download">獲取勾選的表格數據</el-button> <Table :config="config" ref="table" /> </div> </template> <script> import Table from '@/components/table' export default { components: { Table, }, data() { return { config: { headers: [ {prop: 'contractCode', name: '業務編號', attrs: {width: 200, align: 'center'}}, {prop: 'payeeAcctName', name: '收款帳戶名', type: 'Link', url: 'otherElTable', attrs: {width: 260, align: 'right'}}, {prop: 'tradeAmt', name: '付款金額', type: 'Currency'}, {prop: 'status', name: '操做狀態', type: 'Enum', Enum: {name: 'order'}}, {prop: 'statistic', name: '預警統計', type: 'Loop', Loop: (val) => this.loop(val)}, {prop: 'reason', name: '緣由', type: 'Popover'}, {prop: 'payTime', name: '付款時間', type: "Date", format: 'yyyy-MM-dd hh:mm:ss'}, //不設置format的話,日期格式默認爲yyyy/MM/dd {prop: 'monitorStatus', name: '當前監控狀態', type: 'Enum', Enum: {name: 'monitor'}}, ].concat(this.getActions()), tableData: [ {id: 1, contractCode: '', payeeAcctName: '中國銀行上海分行', tradeAmt: '503869', status: '00', payTime: 1593585652530, statistic:[ {level: 3, total: 5}, {level: 2, total: 7}, {level: 1, total: 20}, {level: 0, total: 0} ] }, {id: 2, contractCode: 'GLP-YG-B3-1111', payeeAcctName: '中國郵政上海分行', tradeAmt: '78956.85', status: 'CREATED', payTime: 1593416718317, reason: 'Popover的屬性與Tooltip很相似,它們都是基於Vue-popper開發的,所以對於重複屬性,請參考Tooltip的文檔,在此文檔中不作詳盡解釋。', }, {id: 3, contractCode: 'HT1592985730310', payeeAcctName: '招商銀行上海支行', tradeAmt: '963587123', status: 'PASS', payTime: 1593420950772, monitorStatus: '01'}, {id: 4, contractCode: 'pi239', payeeAcctName: '廣州物流有限公司', tradeAmt: '875123966', status: 'REJECT', payTime: 1593496609363}, {id: 5, contractCode: '0701001', payeeAcctName: '建設銀行上海分帳', tradeAmt: '125879125', status: 'REFUSE', payTime: 1593585489177}, ], hasCheckbox: true, }, 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}, ]} }, loop(val){ let str = ''; val.forEach(t => { str += '<span style="margin-right:5px;">' + t.total + '</span>'; }) return str; }, download(){ console.log(this.$refs.table.getChecked()) }, }, }; </script> <style> .action span{margin-right:10px;color:#359C67;cursor: pointer;} </style>
關於金額千分位和時間戳格式化的實現,這裏就再也不貼代碼了,可自行實現。