简介

最近做的一个CMS后台系统项目频繁遇到表单和表格,该项目用到element-ui,参考前人对element-ui的封装,总结下form表单和table组件的封装方法,加深对组件化开发的理解

前述

  1. 在此项目之前,我理解的封装一直都是一个组件的封装,单一功能的封装。因为封装好的组件通常是UI组件,上面顶多有几个属性和事件。直到遇到大量的form表单和table表格,才需要在一个组件中封装更多的功能,且需要在父组件取到子组件的数据。
  2. element-ui的二次封装我之前没有思考过。由于element-ui的组件本身已经足够方便,但诸如表单dom的配置仍然是写在template中,这造成复用时仍然在写大量重复代码。
  3. 从使用的角度看,element-ui仍然需要去写dom(即模板)。为了实现更好的配置化即在vue的data中通过数据驱动视图,将几乎所有常用的form/table再封装成为自然的选择。
  4. 这样做的缺点是丧失一定的灵活性,但同一项目中的二次封装能够提高form/table组件的开发效率,如果有新的需求,再去扩展和丰富原组件的封装
  5. 前人的封装方法是建立Vue2.0或者较早的版本上,因此大量使用了Vue.component(‘my-form’,{})和Vue.extend以及模板的ES5字符串拼接,现在已经过时,当前更适合用单文件组件去做封装的子组件。所用的slot-scope来取到子组件内部数据流的Api方法也已经废弃,需要了解最新的v-slot的API
  6. 本文主要是为了梳理封装方法,更多理解组件封装。

form封装实例

效果

form.png

代码解析

封装后模板

<my-form :form="queryForm"/>

js数据

export default { data () { return { queryForm: { // my-form组件根属性 labelwidth: '200px', inline: false, labelposition: 'left', // my-form组件的props,存放所有表单值 props: { product_type: 'inline', product_name: '', product_des: 'Chinese', product_des_options: '', product_img: '' }, // my-form组件的dom结构属性 attributes: [ { prop: 'product_type', type: 'require_radio', style: 'width: 60%; margin-left:-200px;', options: [ { label: '选项一', value: 'inline' }, { label: '选项二', value: 'light' } ], // 一些操作表单的事件,changeEvent写在当前vue文件中 onChange: this.changeEvent }, { prop: 'product_name', label: '产品名:', style: 'width: 30%;', isHide: false }, { prop: 'product_des', label: '产品描述', style: 'width: 20%', type: 'select', options: [ { label: '简体中文', value: 'Chinese' }, { label: '英文', value: 'English' }, { label: '法文', value: 'Franch' }, { label: '日文', value: 'Japanese' } ] }, { prop: 'product_des_options', style: 'width: 30%' }, { prop: 'product_img', label: '产品图片', size: 'small', type: 'uploadImg' } ] } }; } }

子组件模板

<el-form ref="myform" :model="form.props" :inline="form.inline" :label-position="form.labelposition" :label-width="form.labelwidth" :action="form.action" :method="form.method" :enctype="form.enctype" :accept-charset="form.acceptCharset" style> <template v-for="item in form.attributes" v-if="!item.isHide"> /* ---------------------基本的表单形式-------------------*/ // 展示文本 <template v-if="item.type===\'plantext\'"> <el-form-item :item="item" :label="item.label" :prop="item.prop" :style="item.style" :label-width="item.labelwidth"> {{form.props[item.prop]}} </el-form-item> </template> <template v-else-if="item.type===\'select\'"> <myselect :form="form" :item="item" :filterable="item.filterable" /> </template> <template v-else> <myinput :form="form" :item="item" :rules="leInputRules"/> </template> </template>

可以看到:

  • queryform的一级属性用来设置最外层el-form组件的dom属性
  • attributes中的每一项是每个自定义组件结构需要的属性

以myselect组件为例:

<el-form-item :label="item.label" :prop="item.prop" :rules="rules" :label-width="item.labelwidth" :style="item.itemStyle"> <el-select :style="item.style || inputStyle" v-model="form.props[item.prop]" @change="onSelectChange" :multiple="multiple" :placeholder="item.placeholder" :disabled="item.disabled || disabled"> <el-option v-for="opt in item.options" :disabled="opt.disabled" :label="opt.label" :value="opt.value" :key="opt.value"></el-option> </el-select> </el-form-item>

这里的关键是子组件的属性怎么能反映到最终组件所在的配置化数据中:

methods: { onSelectChange: function (val) { this.item.onChange && this.item.onChange.call(null, val); } }

通过在子组件中执行item中的方法即可,这意味着配置数据attributes项中能直接写change的监听方法(queryform中例子)

onChange: this.triggerChange

triggerChange(flag)的flag可以直接获得当前选中的值(这依赖于element-ui本身)

table封装实例

效果

table.vue.png

代码解析

封装后模板

<my-table :table="table"></le-table>

js数据

table: { loading: false, attributes: [ { prop: 'action', label: '操作', type: 'ifelse', width: '180', align: 'center', isHide: true, options: [ { title: '上移', icon: 'el-icon-arrow-up', // disabled: 'disabled', onClick: this.moveTo.bind(this, 'up') }, { title: '下移', icon: 'el-icon-arrow-down', onClick: this.moveTo.bind(this, 'down') } ] }, { prop: 'order', label: '桌面顺序', align: 'center' // type: 'index' // width: 60 }, { prop: 'name', label: '包/cms id', align: 'center' }, { prop: 'deskName', label: '桌面名称', align: 'center' // width: 80 }, { prop: 'type', label: '类型', align: 'center' // width: 90 }, { prop: 'isOutput', label: '是否强制外置', align: 'center', type: 'ifelseSelect', // checkvalue: 'no', disabled: true, options: [ { label: '是', value: 'yes' }, { label: '否', value: 'no' } ] }, { prop: 'isLock', label: '是否锁定', align: 'center', type: 'ifelseSelect', // checkvalue: 'yes', disabled: true, options: [ { label: '是', value: 'yes' }, { label: '否', value: 'no' } ] }, { prop: 'status', label: '上线状态', align: 'center' } ], data: [ { order: 1, name: '哈喽', deskName: '123', type: '毛毛虫', isOutput: 'no', isLock: 'no', status: '已上线' }, { order: 2, name: '哈12', deskName: '2323', type: '毛线', isOutput: 'no', isLock: 'yes', status: '已下线' }, { order: 3, name: '哈123', deskName: '2323-3', type: '毛线-3', isOutput: 'no', isLock: 'yes', status: '已上线' } ] }

注意点:

  1. 最终的列表交互后数据存放在table的data数组中,而通过改变data数据,视图也会变好
  2. table比form封装值得注意的是子组件到父组件数据的流动,也即slot-scope的使用问题

子组件模板

<el-table v-loading="table.loading" element-loading-text="加载中.." ref="mytable" :data="table.data" :show-header="table.showHeader" :height = "table.height"> // 每一行 <el-table-column v-for="item in table.attributes" :key="item.prop" v-if="!item.isHide" :width="item.width" :type="item.type" :fixed="item.fixed" :prop="item.prop" :label="item.label" :sortable="item.sortable" :sort-method="item.sortMethod" :formatter="item.formatter" :align="table.align || item.align" > // 具体内容 <template slot-scope="scope"> // select类型表格 <template v-else-if="item.type==='select'"> <el-select :disabled="item.disabled" v-model="scope.row[item.prop]" @change="item.onChange(scope.$index, scope.row, item.prop, scope.row[item.prop])" placeholder="请选择"> <el-option v-for="opt in item.options" :key="opt.value" :label="opt.label" :value="opt.value"></el-option> </el-select> </template> // ifelseselect类型表格 <template v-if="item.type==='ifelseSelect'"> <myselect :scope=scope :item=item></myselect> </template> // 默认类型表格 <template v-else :style="item.style"> {{getCurrentProp(scope.row, item.prop, item)}}'+ </template> </template> </el-table-column> </el-table>

以上:

  1. scope可以理解为数据域:scope.row是行数据,即data中对这样渲染结果生效的数据对象,scope.$index是data的索引值
  2. slot-scope(作用域插槽)是数据子向父传递的神器,他能够获得驱动视图的数据,而使用$emit等事件没有执行时机,无法将数据传回父组件
  3. slot-scope已经废弃,替代使用的是v-slot,那v-slot如何解决slot和slot-scope的问题?

父组件中通过v-slot指令写插槽,插槽名为header,数据域为slotProps(随意命名)

```html
<template v-slot:header="slotProps">
    <h1>{{slotProps.header + ' ' + msg}}</h1>
</template>
```

子组件的header可以在父组件的slotProps对象中取到

<header> <slot name="header" :header="header"></slot> </header>
export default { data() { return { header: '来自子组件的头部消息' } } }

应该说v-slot更直观,与vue其他指令更一致

总结

  • 封装要向着易用的方向进行(将数据驱动视图进行到底)
  • 思维不要局限在形式上(有了封装可以再封装)
  • 封装的思想最重要,而不是技巧和API