仿el-table实现表格控件(内含作用域插槽访问数据, JSX, render)

        前阵子看了饿了么的el-table源码,自己动手封装了一个具有下拉加载功能的表格控件,其中涉及到的知识有JSX,render渲染函数,组件间的状态管理等等。该组件实现了下拉加载,作用域插槽访问数据,加载动画,和宽度自适应这些主要效果,以及一些el-table常用的属性。下面是实现的效果动态图,点击大图查看效果更好:

 

 

仿el-table实现表格控件(内含作用域插槽访问数据, JSX, render)_第1张图片

 由于gif动态图不太清晰,下面放送两张清晰的组件效果图:

仿el-table实现表格控件(内含作用域插槽访问数据, JSX, render)_第2张图片

 

仿el-table实现表格控件(内含作用域插槽访问数据, JSX, render)_第3张图片

 该组件名为hb-table,样式和结构上均参照el-table,由如下文件构成

仿el-table实现表格控件(内含作用域插槽访问数据, JSX, render)_第4张图片

         在el-table的源码中饿了么的团队自定义了一个store来管理所属el-table的组件之间的状态,hb-table通过一个简单的HbTableStore对象来管理hb-table-body, hb-table-column等之间的状态。下面是主要文件的介绍:

  • hb-table.vue 组合表格各部分
  • hb-table-head.js 渲染表格列标题
  • hb-table-body.js  渲染表格列内容
  • hb-table-column.js 收集表格列相关属性
  • hb-table-store.js 用作状态管理
  • utils.js 存放公共方法

  hb-table.vue代码解读:




        下面讲解几个比较重要的部分:

的作用是我们需要通过这种包含的形式让hb-table-column可以正常初始化但不希望最终渲染出来,我们真正需要的只是hb-table-column上的相关属性来进一步配置我们的渲染函数。这里需要在hb-table-column初始化的时候于created生命周期函数中往我们的hb-table-store中加入列的相关配置。具体的配置内容可以在本文的hb-table-column.js代码解读中看到。
  • let store = new HbTableStore(this.tableId) 让我们开始实例化一个HbTableStore对象来管理公共的状态,同时通过store属性往子组件传递。
  • self.doLayout()用于重新计算表格的宽度,其中的计算函数calcColumnWidth来着utils.js
  • fixedHeight: 经过测试,当内容宽度超过表格宽度出现水平滚动条,竖直滚动条滑到底部的时候仍有零点几的距离误差,fixedHeight 用于修正这点误差距离。
  • handleScroll函数用于处理表格的下拉加载事件,this.tHead.scrollLeft = this.tBody.scrollLeft是来同步表格头部和内容的水平滚动距离。当垂直方向的滚动条滑到底部并且当前数据的页码不是最后一页的时候,就触发加载动画,同时向外抛出loadMore事件,触发父组件的加载数据函数。
  • data监听: 数据到位,关闭加载动画。同时将数据添加到tableData中。当前页码为第一页的时候会去初始化相关数据。这么做的原因是当前表格数据的查询条件有变化的时候,让组件内部自动清理相关数据。
  •      hb-table-column.js 代码解读

     

    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)
        }
    }
    
    • created 函数把列渲染所需的配置都放在了column对象中,最后通过store的insertColumn把配置对象插入待渲染的列。这里的store对象就是hb-table.vue中HbTableStore实例化的对象
    • renderCell 渲染函数: 当type为index时,渲染行号。当prop存在时,直接渲染数据对应属性内容。剩下的情况就是需要渲染外部插槽的内容,同时配置$scopeSlots,在作用域插槽中绑定当前行的数据以及行号。不熟悉$scopedSlots的朋友可以看一下Vue官网有关插槽的部分。
    • originWidth 用于存放传入的自定义宽度数值,width是真正渲染时所用到的宽度属性,这里先初始化为0.

        hb-table-store.js代码解读 

     

    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
        }
    }
    
    • columnLabelMap 用于记录当前列的标题
    • realTableWidth 用于记录经过计算后的表格宽度数据

        hb-table-head.js代码解读 

    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标签,开始组装表格列标题内容。

        hb-table-body.js代码解读

    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渲染内容的简洁性。

         utils.js代码解读

    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里面就一个计算表格宽度的方法。 

    • let bodyWidth = table.$el.clientWidth - 24  这里减去了垂直滚动条的宽度,这部分宽度不参与后面的列宽计算。
    • if (typeof column.originWidth !== 'string') return column 这里把没有定义宽度的列返回,用于参与富余宽度的分配。
    • column.width = column.originWidth || 80;tableWidth += parseInt(column.width) 将未定义宽度的列设置最小宽度80,同时开始计算表格所占的总宽度。
    • flexColumns[0].width += flexWidth - flexSpaceWidth * (flexColumns.length - 1) 第一个可参与富余宽度分配的列宽取富余总宽度和其他剩余列的宽度和之差。因为富余总宽度存在除不尽的情况,这么做是为了保证富余列宽完全被分配。后面就是记录表格实际的占有宽度了。

              hb-table的介绍就到此为止了,如有任何疑问可于评论下方留言。业界知名框架的源码里面有很多值得学习的东西,最主要的就是一个设计的思想和实现的思路,合理的设计加上合理的代码实现,就是一个合理的组件了。

    你可能感兴趣的:(自定义组件系列)