背景介绍
最近在做 vue 高仿网易云音乐的项目,在做的过程中发现音乐表格这个组件会被非常多的地方复用,而且需求比较复杂的和灵活。
预览地址
源码地址
图片预览
- 歌单详情
- 播放列表
- 搜索高亮
需求分析
它需要支持:
- hideColumns 参数,自定义需要隐藏哪些列。
- highLightText,传入字符串,数据中命中的字符串高亮。
首先 看一下我们平常的 table 写法。
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="index"
label=" "
width="180">
</el-table-column>
<el-table-column
prop="name"
label="音乐标题"
width="180">
</el-table-column>
<el-table-column
prop="artistsText"
label="歌手">
</el-table-column>
</el-table>
这是官网的写法,假设我们传入了 hideColumns: [‘index’, ‘name’],我们需要在模板里隐藏的话`
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
+++ v-if="!hideColumns.includes('index')"
prop="index"
label=" "
width="180">
</el-table-column>
<el-table-column
+++ v-if="!hideColumns.includes('name')"
prop="name"
label="音乐标题"
width="180">
</el-table-column>
<el-table-column
+++ v-if="!hideColumns.includes('address')"
prop="artistsText"
label="歌手">
</el-table-column>
</el-table>
这种代码非常笨,所以我们肯定是接受不了的,我们很自然的联想到平常用 v -for 循环,能不能套用在这个需求上呢。
首先在 data 里定义 columns
data() {
return {
columns: [{
prop: 'index',
label: '',
width: '50'
}, {
prop: 'artistsText',
label: '歌手'
}, {
prop: 'albumName',
label: '专辑'
}, {
prop: 'durationSecond',
label: '时长',
width: '100',
}]
}
}
然后我们在 computed 中计算 hideColumns 做一次合并
computed: {showColumns() {const { hideColumns} = this
return this.columns.filter(column => {return !this.hideColumns.find((prop) => prop === column.prop)
})
},
},
那么模板里我们就可以简写成
<el-table
:data="songs"
>
<template v-for="(column, index) in showColumns">
<el-table-column
:key="index"
// 混入属性
v-bind="column"
>
</el-table-column>
</template>
</el-table>
注意 v-bind="column"
这行,相当于把 column 中的所有属性混入到 table-column 中去,是一个非常简便的方法。
script 配合 template 的解决方案
这样需求看似解决了,很美好。
但是我们忘了非常重要的一点,slotScopes
这个东西!
比如音乐时长我们需要 format 一下,
<el-table-column>
<template>
<span>{{$utils.formatTime(scope.row.durationSecond) }}</span>
</template>
</el-table-column>
但是我们现在把 columns 都写到 script 里了,和 template 分离开来了,我暂时还不知道有什么方法能把 sciprt
里写的模板放到 template
里用,所以先想到一个可以解决问题的方法。就是在 template 里加一些判断。
<el-table
v-bind="$attrs"
v-if="songs.length"
:data="songs"
@row-click="onRowClick"
:cell-class-name="tableCellClassName"
style="width: 99.9%"
>
<template v-for="(column, index) in showColumns">
<!-- 需要自定义渲染的列 -->
<el-table-column
v-if="['durationSecond'].includes(column.prop)"
:key="index"
v-bind="column"
>
<!-- 时长 -->
<template v-else-if="column.prop ==='durationSecond'">
<span>{{$utils.formatTime(scope.row.durationSecond) }}</span>
</template>
</el-table-column>
<!-- 普通列 -->
<el-table-column
v-else
:key="index"
v-bind="column"
>
</el-table-column>
</template>
</el-table>
又一次的需求看似解决了,很美好。
高亮文字匹配需求分析
但是新需求又来了!!根据传入的 highLightText 去高亮某些文字,我们分析一下需求
鸡你太美
这个歌名,我们在搜索框输入 鸡你
我们需要把
<span> 鸡你太美 </span>
转化为
<span>
<span class="high-light"> 鸡你 </span>
太美
</span>
我们在 template 里找到音乐标题这行,写下这端代码:
<template v-else-if="column.prop ==='name'">
<span>{{this.genHighlight(scope.row.name)}}</span>
</template>
methods: {genHighlight(text) {return <span>xxx</span>}
}
我发现无从下手了, 因为 jsx 最终编译成的是 return vnode 的方法,genHighlight 执行以后返回的是 vnode,但是你不能直接把 vnode 放到 template 里去。
jsx 终极解决方案
所以我们要统一环境,直接使用 jsx 渲染我们的组件,文档可以参照
babel-plugin-transform-vue-jsx
vuejs/jsx
data() {
const commonHighLightSlotScopes = {
scopedSlots: {default: (scope) => {
return (<span>{this.genHighlight(scope.row[scope.column.property])}</span>
)
}
}
}
return {
columns: [{
prop: 'name',
label: '音乐标题',
...commonHighLightSlotScopes
}, {
prop: 'artistsText',
label: '歌手',
...commonHighLightSlotScopes
}, {
prop: 'albumName',
label: '专辑',
...commonHighLightSlotScopes
}, {
prop: 'durationSecond',
label: '时长',
width: '100',
scopedSlots: {default: (scope) => {
return (<span>{this.$utils.formatTime(scope.row.durationSecond)}</span>
)
}
}
}]
}
},
methods: {genHighlight(title = '') {
... 省去一些细节
const titleSpan = matchIndex > -1 ? (
<span>
{beforeStr}
<span class="high-light-text">{hitStr}</span>
{afterStr}
</span>
) : title;
return titleSpan;
},
},
render() {
const tableAttrs = {
attrs: this.$attrs,
on: {
...this.$listeners,
['row-click']: this.onRowClick
},
props: {['table-cell-class-name']: this.tableCellClassName,
data: this.songs
},
style: {width: '99.9%'}
}
return this.songs.length ? (
<el-table
{...tableAttrs}
>
{this.showColumns.map((column, index) => {const { scopedSlots, ...columnProps} = column
return (<el-table-column key={index} {...{ props: columnProps}} scopedSlots={scopedSlots} >
</el-table-column>
)
})}
</el-table>
) : null
}
注意 $listeners
需要放在 on 中,attrs
的透传也不要忘了,这样我们在外部想使用 el-table 的一些属性和事件才比较方便。
可以看到代码中模板的部分少了很多重复的判断,维护性和扩展性都更强了,jsx 可以说是复杂组件的终极解决方案。