使用?render?函數(shù)封裝高擴展的組件
需求:
后臺管理中常常有如下布局的數(shù)據(jù)展示需求:

像表格又不是表格,像表單又不是表單,實際上樣子像表格,呈現(xiàn)的數(shù)據(jù)是一個對象,和 form 的綁定的值一樣,我將其稱為表單式表格。
樣式深的列是標題,淺的列是標題對應(yīng)的取值,數(shù)據(jù)往往是服務(wù)器返回的,標題往往是定寬的,取值可能各種各樣,比如顯示一張圖片,值為 01,需要顯示是與否,有時候需要添加一個修改按鈕,讓用戶能修改某些值,還需要設(shè)置某一列跨越幾列。
先來看看一個基于 element ui 的實現(xiàn)
不好的實現(xiàn):
在接手的項目看到一個實現(xiàn),先看使用方式
<FormTable :data="lessonPackageArr" :fleldsInfo="lessonPackageInfo" :maxColumn="3" label-width="120px">
<template #presentedHours="{ data }">
<div class="flex-box between">
<span>
{{ data.presentedHours }}
</span>
<span class="column-btn" @click="editPresentedHours(data)">修改</span>
</div>
</template>
<template #gifts="{ data }">
<div class="flex-box between">
<span>
{{ data.gifts }}
</span>
<span class="column-btn" @click="editPresentedHours(data)">修改</span>
</div>
</template>
</FormTable>
lessonPackageInfo 對象如下結(jié)構(gòu):
// 一個對象,用于配置標題列和標題列對應(yīng)的字段
// type 指定值的類型,現(xiàn)在組件內(nèi)部設(shè)置可能顯示哪些類型的值了
// 對于服務(wù)其返回 1 0 需要顯示 是否的數(shù),提供一個 map_data 來映射
// column 屬性設(shè)置跨列
// 需要自定義顯示內(nèi)容 提供 slot
lessonPackageInfo: {
orderType: { type: 'option', desc: '課時包類別', map_data: { 1: '首單', 2: '續(xù)費', 5: '贈課' } },
combo: { type: 'text', desc: '套餐名稱' },
presentedHours: { type: 'text', desc: '贈送課時', slot: true },
price: { type: 'text', desc: '標準價格' },
gifts: { type: 'text', desc: '贈送禮物', column: 3, slot: true },
}
- props 不夠直觀,配置項多
- 不是完全數(shù)據(jù)驅(qū)動
為何組件的配置項多不好?
對于這種需求很固定,組件的輸入即 props 應(yīng)該要最小化,組件功能要最大化,盡量給 props 提供默認值,這樣才能提高團隊的開發(fā)效率。
為何不是完全的數(shù)據(jù)驅(qū)動不好?
這個組件不是完全數(shù)據(jù)驅(qū)動的,需要自定義顯示列是,需要編寫模板。
如果需要自定義的列很多,就要寫很多模板代碼,想要再提取,只能再次封裝組件,不提取,模板代碼可能會膨脹,你可能經(jīng)??吹絼虞m 500 行一行的 template ?而膨脹的模板代碼,讓組件維護變得困難,需要 template 和 js 代碼之間來回切換。再者,增加一列自定義的數(shù)據(jù),起碼要修改兩個地方。
為何需要完全的數(shù)據(jù)驅(qū)動?
雖然有 slot 來擴展組件,但是我們在寫業(yè)務(wù)組件時候應(yīng)該少用,而是盡量使用數(shù)據(jù)驅(qū)動模板。因為數(shù)據(jù)是 js 代碼,當組件代碼膨脹時,很容易把 js 代碼提取成單獨的文件, 而想要提取 slot 的代碼,只能再封裝組件。
三大前端框架的設(shè)計理念都是數(shù)據(jù)驅(qū)動模板,這是它們區(qū)別于 jQuery 的重要特征,也是我們封裝業(yè)務(wù)組件時優(yōu)先遵循的原則。
看了組件使用的問題,再看組件的代碼:
<template>
<div v-if="tableData.length" class="form-table">
<div v-for="(data, _) in tableData" :key="_" class="table-border">
<el-row v-for="(row, index) in rows" :key="index">
<el-col v-for="(field, key) in row" :key="key" :span="getSpan(field.column)">
<div v-if="(field.disabled && data[key]) || !field.disabled" class="column-content flex-box between">
<div class="label" :style="'width:' + labelWidth">
<span v-if="field.required" class="required">*</span>
{{ field.desc }}
</div>
<div class="text flex-item" :title="data[key]">
<template v-if="key === 'minAge'">
<span>{{ data[key] }}</span>
-
<span>{{ data['maxAge'] }}</span>
</template>
<template v-else-if="key === 'status'">
<template v-if="field.statusList">
<span v-if="data[key] == 0" :class="field.statusList[2]">{{ field.map_data[data[key]] }}</span>
<span v-else-if="data[key] == 10 || data[key] == 34" :class="field.statusList[1]">
{{ field.map_data[data[key]] }}
</span>
<span v-else :class="field.statusList[0]">{{ field.map_data[data[key]] }}</span>
</template>
<span v-else>{{ field.map_data[data[key]] }}</span>
</template>
<slot v-else :name="key" v-bind:data="data">
<TableColContent
:dataType="field.type"
:metaData="data[key]"
:mapData="field.map_data"
:text="field.text"
/>
</slot>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
<div v-else class="form-table empty">暫無數(shù)據(jù)</div>
</template>
<script>
import TableColContent from '@/components/TableColContent'
export default {
name: 'FormTable',
components: {
TableColContent,
},
props: {
// 數(shù)據(jù)
data: {
required: true,
type: [Object, Array, null],
},
// 字段信息
fleldsInfo: {
required: true,
type: Object,
// className: { type: "text", desc: "班級名稱", column: 3 },
},
// 最多顯示列數(shù)
maxColumn: {
required: false,
type: Number,
default: 2,
},
labelWidth: {
required: false,
type: String,
default: '90px',
},
},
data() {
return {}
},
computed: {
tableData() {
if (!this.data) {
return []
}
if (this.data instanceof Array) {
return this.data
} else {
return [this.data]
}
},
rows() {
const returnArray = []
let total = 0
let item = {}
for (const key in this.fleldsInfo) {
const nextTotal = total + this.fleldsInfo[key].column || 1
if (nextTotal > this.maxColumn) {
returnArray.push(item)
item = {}
total = 0
}
total += this.fleldsInfo[key].column || 1
item[key] = this.fleldsInfo[key]
if (total === this.maxColumn) {
returnArray.push(item)
item = {}
total = 0
}
}
if (total) {
returnArray.push(item)
}
return returnArray
},
},
methods: {
getSpan(column) {
if (!column) {
column = 1
}
return column * (24 / this.maxColumn)
},
},
}
</script>
有哪些問題?
- 模板有太多的條件判斷,不優(yōu)雅
- 自定義顯示列,還需要在引入
TableColContent,增加了組件復(fù)雜性
TableColContent 內(nèi)部還是對配置項的 type 進行條件判斷
部分代碼:
<span v-else-if="dataType === 'image' || dataType === 'cropper'" :class="className">
<el-popover placement="right" title="" trigger="hover">
<img :src="metaData" style="max-width: 600px;" />
<img slot="reference" :src="metaData" :alt="metaData" width="44" class="column-pic" />
</el-popover>
</span>
分析完以上實現(xiàn)的問題,看看好的實現(xiàn):
好的實現(xiàn):
先看使用方式:
<template>
<ZmFormTable :titleList="titleList" :data="data" />
</template>
<script>
export default {
name: 'Test',
data() {
return {
data: {}, // 從服務(wù)器獲取
titleList: [
{ title: '姓名', prop: 'name', span: 3 },
{
title: '課堂作品',
prop: (h, data) => {
const img =
(data.workPic && (
<ElImage
style='width: 100px; height: 100px;'
src={data.workPic}
preview-src-list={[data.workPic]}
></ElImage>
)) ||
''
return img
},
span: 3,
},
{ title: '作品點評', prop: 'workComment', span: 3 },
],
}
},
}
</script>
組件說明: titleList是組件的列配置,一個數(shù)組,元素 title 屬性是標題,prop 指定從 data 里取值的字段,span 指定這列值跨越的行數(shù)。
prop 支持 string ,還支持函數(shù),這是實現(xiàn)自定義顯示的方式,當這個函數(shù)很大時,可提取到獨立的 js 文件中,也可以把整個 titleList 提取單獨的 js 文件中。
參數(shù) h 和 data 是如何傳遞進來的?或者 這函數(shù)在哪調(diào)用呢?
h 是 createElement 函數(shù),data 是從組件內(nèi)部的 data,和父組件傳入的 data 是同一個值。
當普通函數(shù)的第一個參數(shù)是 h 是,它就是一個 render 函數(shù)。
這種方式使用起來簡單多了。
看看內(nèi)部實現(xiàn):
<template>
<div class="form-table">
<ul v-if="titleList.length">
<!-- titleInfo 是經(jīng)過轉(zhuǎn)化的titleList-->
<li
v-for="(item, index) in titleInfo"
:key="index"
:style="{ width: ((item.span || 1) / titleNumPreRow) * 100 + '%' }"
>
<div class="form-table-title" :style="`width: ${titleWidth}px;`">
<Container v-if="typeof item.title === 'function'" :renderContainer="item.title" :data="data" />
<span v-else>
{{ item.title }}
</span>
</div>
<div class="form-table-key" :style="`width:calc(100% - ${titleWidth}px);`">
<Container v-if="typeof item.prop === 'function'" :renderContainer="item.prop" :data="data" />
<span v-else>
{{ ![null, void 0].includes(data[item.prop] && data[item.prop]) || '' }}
</span>
</div>
</li>
</ul>
<div v-else class="form-table-no-data">暫無數(shù)據(jù)</div>
</div>
</template>
<script>
import Container from './container.js'
export default {
name: 'FormTable',
components: {
Container,
},
props: {
titleWidth: {
type: Number,
default: 120,
},
titleNumPreRow: {
type: Number,
default: 3,
validator: value => {
const validate = [1, 2, 3, 4, 5, 6].includes(value)
if (!validate) {
console.error('titleNumPreRow 表示一行有標題字段對,只能時 1 -- 6 的偶數(shù),默認 3')
}
return validate
},
},
titleList: {
type: Array,
default: () => {
return []
},
validator: value => {
const validate = value.every(item => {
const { title, prop } = item
return title && prop
})
if (!validate) {
console.log('傳入的 titleList 屬性的元素必須包含 title 和 prop 屬性')
}
return validate
},
},
data: {
type: Object,
default: () => {
return {}
},
},
},
}
</script>
<!-- 樣式不是關(guān)鍵,省略 -->
實現(xiàn)自定義顯示的方式,沒有使用動態(tài)插槽,而是用一個函數(shù)組件Container,該組件接收一個 render 函數(shù)作為 prop。
export default {
name: 'Container',
functional: true,
render(h, { props }) {
return props.renderContainer(h, props.data)
},
}
在 Container 內(nèi)部調(diào)用 titleList 傳入的函數(shù)。
總結(jié):
- 封裝組件時優(yōu)先考慮數(shù)據(jù)驅(qū)動
- 普通函數(shù)的第一個參數(shù)是 h,就是渲染函數(shù)
- 可能有一些人不習慣寫 JSX, 可兼容兩種寫法
到此這篇關(guān)于使用 render 函數(shù)封裝高擴展的組件的文章就介紹到這了,更多相關(guān) render 函數(shù)封裝高擴展組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Web?Animations?API實現(xiàn)一個精確計時的時鐘示例
這篇文章主要為大家介紹了Web?Animations?API實現(xiàn)一個精確計時的時鐘示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07

