Vue优雅设计一个组件
本组件已开源,已npm publish,可自行npm install km-grid,应小伙伴要求,已上传github。
注:虽然现在已经出现大量优秀的UI框架,如elementUI,iviewUI,单从业务、功能及性能仍不能满足项目所有需求,这时,就要考虑如何设计适合自己的组件。
组件设计其实就是模块的设计。我把它分为4个部分:UI设计,基本功能,业务拓展需求,性能。以下我已我自己设计的高性能table为例,剖析我是如何优雅的设计一个组件。
- UI设计交互就好比模块的import、exports,要保持统一的出口。即统一风格配色,统一交互。
- 基本功能就好比模块的基本作用。必须保证基本功能的同时,兼容其他。
- 业务拓展就好比模块间的集成,这就要求组件可依耐性低、可拔插集成、高聚低耦合。
- 性能要考虑到大量数据或大量计算与GUI渲染线程互斥导致的性能问题。
注:一个组件的设计先从基本功能开始。
先看table的基本功能包括:头部、序号列、多选列、左右固定列、总计行;
业务拓展功能:列搜索。
考虑到左右固定列需要单独的头部、搜索行、内容、总计行。可将km-grid设计为3个单独的table,每个单独的table都是一个独立的km-grid-item。
<div :class="cls" :style="tableStyles"> <km-grid-item v-if="fixedLeftCol&&fixedLeftCol.length" fixed="left" v-on="$listeners" :columns="fixedLeftCol" :header-styles="leftFixedHeaderStyles" :body-styles="leftFixedBodyStyles"></km-grid-item> <km-grid-item v-on="$listeners" :columns="centerCol" :expandColumn="expandCol" :header-styles="headerStyles" :body-styles="bodyStyles"></km-grid-item> <km-grid-item v-if="fixedRightCol&&fixedRightCol.length" fixed="right" v-on="$listeners" :columns="fixedRightCol" :header-styles="rightFixedHeaderStyles" :body-styles="rightFixedBodyStyles"></km-grid-item> </div> 复制代码
这样,整个table的设计便会轻便且简洁。
单个table的设计又怎么办呢?
那我是如何在单个组件驱动其他组件的呢?
mounted () { this.dispatch("KmGrid", "on-km-grid-item-add", this); } 复制代码
在km-grid接收:
created () { this.$on('on-km-grid-item-add', item => { this.itemVms.push(item); }) } 复制代码
接着在事件中触发同步驱动整个table:
HandleBodyScroll (event) { this.$refs.header.scrollLeft = event.target.scrollLeft if (this.$parent.itemVms && this.$parent.itemVms.length > 0) { this.$parent.itemVms.forEach(v => { let scrollDom = v.$el.querySelectorAll('.km-grid-noscroll')[0] if (scrollDom) { scrollDom.scrollTop = event.target.scrollTop } }) } } 复制代码
业务拓展需求有:根据行数据显示不同按钮,不同行为,数据格式化,行下拉动态渲染。
把需求转化为数据操作就是先获取行数据,根据行数据展示不同的按钮,然后自定义行为在业务中编写:
先定义column及操作:
{ title: '操作', type: 'handle', handle: [ { type: 'edit' }, { type: 'warehouse', icon: 'km-stock', click: row => { this.$refs.listpage.showList = false this.$refs.WarehouseInfo.loadEntity(row.id) } } ], width: 90, align: 'left', fixed: 'right' } 复制代码
根据行数据渲染:
showCustomOperate: { warehouse: row => { switch (row.type) { case '4': return true default: return false } } } RebuildTableDataByColOperate (columns, data) { data.forEach(v => { handles.forEach(handle => { v['__' + handle.type] = this.showCustomOperate[handle.type](v) || false }) }) } HandleIconClick (item, row) { return item.click(row); } 复制代码
通过这种集成方式组件不必关心业务需求,自动化配置自定义按钮及操作行为,可拔插的集成,避免影响平台组件维护。
首先在columns里配置:
{ type: 'expand', width: 50, render: (h, params) => { return h(inventoryControl, { props: { row: params.row } }) } } components: { inventoryControl: () => import('@/view/main-components/inventoryControl.vue') } 复制代码
然后在km-grid-item的tr里面根据行数据动态render:
<div v-if="!fixed&&expandColumn.render" :class="cls+'-tr-expand'"> <div style="width:100%" v-if="row._clicked"> <td-render :row="row" :render="expandColumn.render"></td-render> </div> </div> 复制代码
通过这种集成方式组件不必关心业务需求,返回行数据让业务自己交互,不会污染当前组件DOM及多余的代码逻辑。
数据格式化是一个老生常谈的问题,例如下面我们只用通过这个方法即可格式化系统数据:
GetTdData (row, col) { if (col.enumData) { let desc = (col.desc && row[col.desc(row)]) ? '(' + row[col.desc(row)] + ')' : '' return col.desc ? col.enumData[row[col.key]] + desc : col.enumData[row[col.key]] } else if (col.valueKey) { return row[col.key] ? `[${row[col.key]}]${row[col.valueKey]}` : '' } else if (['date', 'datetime', 'time'].includes(col.type)) { const dateEnum = { date: 'yyyy-MM-dd', datetime: 'yyyy-MM-dd HH:mm:ss', time: 'HH:mm:ss' } if (!row[col.key]) { return '' } else { return formatDate(row[col.key], col.format || dateEnum[col.type]) } } else if (col.bizType && ['price', 'amt', 'qty', 'rate'].includes(col.bizType)) { const formatEnum = { price: 4, amt: 2, qty: 3, rate: 4 } if (typeOf(row[col.key]) === 'null') { return '-' } else { return parseFloat(row[col.key]).toFixed(col.format || formatEnum[col.bizType]) } } return row[col.key] }, GetTotalData (col) { if (!col.total) { return '' } else if (col.totalValue) { return col.totalValue } else if (col.bizType && ['price', 'qty', 'amt'].includes(col.bizType)) { const formatEnum = { price: 4, qty: 3, amt: 4 } return this.$parent.data.map(row => Number(row[col.key])).reduce((a, b) => a + b, 0).toFixed(col.format || formatEnum[col.bizType]) } return '' } 复制代码
这种默认配置是前端与后端约定好,前端在columns规定好,即可有序展开。
总结一下就是,拓展需求就是要可尽量少的配置、自动化、可拔插、不污染组件、减少增加组件逻辑、无代码侵入的支持业务功能。
经测试,km-grid拥有高效的性能并兼容主流浏览器(ie11以上,包含ie11)。对比了同样的iview-table,element-table,在500条级数据时,km-grid表现更佳,大大超越iview-table。各位同学可直接在npm install。
属性名 | 属性类型 | 描述 |
---|---|---|
data | Array | 元数据 |
headerRowHeight | Number | 头部高度 |
rowHeight | Number | 行高度 |
totalRowHeight | Number | 总计高度 |
height | Number | 指定高度 |
width | Number | 指定宽度 |
border | Boolean | 显示分割线 |
pageSize | Number | 分页数据 |
current | Number | 分页数据 |
draggable | Boolean | 是否可拖拽列宽 |
clickAsync | Number | 点击事件是否异步加载 |
事件名 | 描述 |
---|---|
on-selection-change | 全选,单选时触发 |
on-row-click | 点击 |
on-row-dblclick | 双击 |
on-sort-change | 排序 |
方法名 | 描述 |
---|---|
GetSelectRows | 获取勾选选行 |
ClearSelectRows | 清除沟选行 |
GetCurrentRow | 获取选中行 |
km-grid在设计上精简,使用v-on="$listeners"向上抛出事件,所以写km-grid-item的emit事件不用重复的再往上面抛,而对于需要对外开放的API,km-grid则故意将需要开放的API写在km-grid里面。
// public API so i Expose in km-grid GetSelectRows () { return this.itemVms[0].GetSelectRows() }, // public API so i Expose in km-grid ClearSelectRows () { this.data.forEach(row => { this.$set(row, '_checked', false) }) }, // public API so i Expose in km-grid GetCurrentRow () { return this.data.find(row => row._clicked) } 复制代码
现在的前端越来越后后端化,从npm仓库到模块化,自动化,前端早已不是几年前的那个画页面前端。更多的考虑是模块化组件的同时,处理好组件与平台,平台与业务这三方面的“交互”。如果你能很好的理解VUE的VueComponent响应式,将在VUE的海洋如鱼得水,同时也对面向对象编程有自己独到的见解。
下一期我将展示如何将km-grid即成为高性能的可编辑双向绑定table,同时也将开源。