前阵子看了饿了么的el-table源码,自己动手封装了一个具有下拉加载功能的表格控件,其中涉及到的知识有JSX,render渲染函数,组件间的状态管理等等。该组件实现了下拉加载,作用域插槽访问数据,加载动画,和宽度自适应这些主要效果,以及一些el-table常用的属性。下面是实现的效果动态图,点击大图查看效果更好:
由于gif动态图不太清晰,下面放送两张清晰的组件效果图:
该组件名为hb-table,样式和结构上均参照el-table,由如下文件构成
在el-table的源码中饿了么的团队自定义了一个store来管理所属el-table的组件之间的状态,hb-table通过一个简单的HbTableStore对象来管理hb-table-body, hb-table-column等之间的状态。下面是主要文件的介绍:
暂无数据
下面讲解几个比较重要的部分:
let columnIdSeed = 0
export default {
name: 'hb-table-column',
computed: {
tableColumnId () {
return 'columnId-' + (columnIdSeed++)
},
table () {
return this.$parent
}
},
props: {
label: String,
prop: String,
width: String,
align: String,
type: String
},
methods: {
renderCell (data, index) {
let curIndex = index + 1
if (this.type === 'index') {
return curIndex
}
if (this.prop) return data[this.prop]
return this.$scopedSlots.default({
$index: curIndex,
row: data
})
}
},
created () {
let column = {
tableId: this.table.tableId,
columnId: this.tableColumnId,
label: this.label,
prop: this.prop,
originWidth: this.width,
width: 0,
slots: this.$slots.default,
align: this.align,
renderCell: this.renderCell,
type: this.type
}
this.table.store.insertColumn(column)
},
render (h) {
return h('div', this.$slots.default)
}
}
export default class HbTableStore {
constructor (tableId) {
this.storeId = 'store-' + tableId
this.columns = []
this.columnLabelMap = {}
this.realTableWidth = 0
}
insertColumn (column) {
this.columns.push(column)
this.columnLabelMap[column.columnId] = column.label
}
getColumns () {
return this.columns
}
updateTableWidth (width) {
this.realTableWidth = width
}
getTableWidth () {
return this.realTableWidth
}
}
export default {
name: 'hb-table-head',
data () {
return {
columns: []
}
},
computed: {
table () {
return this.$parent
}
},
props: {
store: {
require: true
}
},
methods: {
getHeaderCellStyle (column) {
return {
width: column.width + 'px'
}
}
},
created () {
this.columns = this.store.getColumns()
},
render (h) {
let columns = this.store.getColumns()
let tableWidth = this.store.getTableWidth()
return (
{columns.map(column => {column.label} )}
)
}
}
从render渲染函数开始就用到了JSX ,这里还可用vue自己的render配置写法,用JSX的好处就是更直观,可以一种html 布局的方法描述我们的渲染内容。这里建议使用JSX,不熟悉的朋友可以先找相关文档了解一下。columns开始map遍历store里面存储的列配置,同时返回td标签,开始组装表格列标题内容。
export default {
name: 'hb-table-body',
computed: {
table () {
return this.$parent
}
},
props: {
store: {
require: true
}
},
methods: {
getBodyCellStyle (column) {
return {
width: column.width + 'px',
'text-align': column.align
}
}
},
render (h) {
let tableData = this.table.tableData
let columns = this.store.getColumns()
let tableWidth = this.store.getTableWidth()
return (
{tableData.map((row, index) =>
{columns.map(column =>
{column.renderCell(row, index)}
)}
)
}
)
}
}
到了render渲染函数的这一步,最主要的就是通过JSX把需要渲染的内容组合出来 。我们看到这里出现了renderCell,这就是我们在hb-table-column.js中定义的函数,这样封装的好处就是可以把多种不同的渲染情况交给专业的函数进行处理,而不是在map遍历的时候通过各种条件判断去返回渲染内容,这样会让代码显得很臃肿,而且不利于阅读,同时破坏了render渲染内容的简洁性。
function calcColumnWidth (columns, table) {
let bodyWidth = table.$el.clientWidth - 24
let tableWidth = 0
let flexColumns = []
flexColumns = columns.filter(column => {
if (typeof column.originWidth !== 'string') return column
})
for (let column of columns) {
column.width = column.originWidth || 80
tableWidth += parseInt(column.width)
}
// 宽度有富余
if (tableWidth < bodyWidth) {
// 富余宽度
let flexWidth = bodyWidth - tableWidth
let flexSpaceWidth = parseInt(flexWidth / flexColumns.length)
flexColumns[0].width += flexWidth - flexSpaceWidth * (flexColumns.length - 1)
for (let i = 1; i < flexColumns.length; i++) {
flexColumns[i].width += flexSpaceWidth
}
table.store.updateTableWidth(bodyWidth)
} else {
table.store.updateTableWidth(tableWidth)
}
return columns
}
export {calcColumnWidth}
util.js里面就一个计算表格宽度的方法。
hb-table的介绍就到此为止了,如有任何疑问可于评论下方留言。业界知名框架的源码里面有很多值得学习的东西,最主要的就是一个设计的思想和实现的思路,合理的设计加上合理的代码实现,就是一个合理的组件了。