共计 7719 个字符,预计需要花费 20 分钟才能阅读完成。
在日常开发中,我们肯定不止一次碰到重复的业务代码,明明功能相似,但总没思路去把它封装成组件。关于封装组件,希望这篇文章能带给大家新的思路,去更高效的完成日常开发。(注:例子都是基于 ElementUI, 但思路都是一样的)
示例地址 -> https://www.lyh.red.admin
代码地址
数据驱动
构建页面:设计数据结构(绑定 value,绑定事件,相关属性)-> 生成 dom -> dom 绑定相关
监听事件:操作 UI -> 触发事件 -> 更新数据 -> 更新 UI
数据驱动是基于数据触发的,在编写业务的时候,只需要编写好组件的 dom 结构,之后我们便可以不用再去关心 dom 层,只需要关心数据就 ok。基于这种思路,那留给我们的只有两步,组件设计和数据设计。
先看看效果
搜索栏配置以及生成效果
// 过滤相关配置
filterInfo: {
query: {
create_user: ”,
account: ”,
name: ”
},
list: [
{type: ‘input’, label: ‘ 账户 ’, value: ‘account’},
{type: ‘input’, label: ‘ 用户名 ’, value: ‘name’},
// {type: ‘select’, label: ‘ 创建人 ’, value: ‘create_user’},
// {type: ‘date’, label: ‘ 创建时间 ’, value: ‘create_time’},
{type: ‘button’, label: ‘ 搜索 ’, btType: ‘primary’, icon: ‘el-icon-search’, event: ‘search’, show: true},
{type: ‘button’, label: ‘ 添加 ’, btType: ‘primary’, icon: ‘el-icon-plus’, event: ‘add’, show: true}
]
}
表格配置以及生成效果
// 表格相关
tableInfo: {
refresh: false,
initCurpage: false,
data: [],
fieldList: [
{label: ‘ 账号 ’, value: ‘account’},
{label: ‘ 用户名 ’, value: ‘name’},
{label: ‘ 性别 ’, value: ‘sex’, width: 80, list: ‘sexList’},
{label: ‘ 账号类型 ’, value: ‘type’, width: 100, list: ‘accountTypeList’},
{label: ‘ 状态 ’, value: ‘status’, width: 90, list: ‘statusList’},
{label: ‘ 创建人 ’, value: ‘create_user’},
{label: ‘ 创建时间 ’, value: ‘create_time’, minWidth: 180},
{label: ‘ 更新人 ’, value: ‘update_user’},
{label: ‘ 更新时间 ’, value: ‘update_time’, minWidth: 180}
],
handle: {
fixed: ‘right’,
label: ‘ 操作 ’,
width: ‘180’,
btList: [
{label: ‘ 编辑 ’, type: ”, icon: ‘el-icon-edit’, event: ‘update’, show: true},
{label: ‘ 删除 ’, type: ‘danger’, icon: ‘el-icon-delete’, event: ‘delete’, show: true}
]
}
}
dom 配置和完整页面
<template>
<div class=”app-container”>
<!– 条件栏 –>
<page-filter
:query.sync=”filterInfo.query”
:filterList=”filterInfo.list”
:listTypeInfo=”listTypeInfo”
@handleClickBt=”handleClickBt”
@handleEvent=”handleEvent”>
</page-filter>
<!– 表格 –>
<page-table
:refresh=”tableInfo.refresh”
:initCurpage=”tableInfo.initCurpage”
:data.sync=”tableInfo.data”
:api=”getListApi”
:query=”filterInfo.query”
:fieldList=”tableInfo.fieldList”
:listTypeInfo=”listTypeInfo”
:handle=”tableInfo.handle”
@handleClickBt=”handleClickBt”
@handleEvent=”handleEvent”>
</page-table>
<!– 弹窗 –>
<page-dialog
:title=”dialogInfo.title[dialogInfo.type]”
:visible.sync=”dialogInfo.visible”
:width=”dialogInfo.width”
:btLoading=”dialogInfo.btLoading”
:btList=”dialogInfo.btList”
@handleClickBt=”handleClickBt”
@handleEvent=”handleEvent”>
<!– form –>
<page-form
:refObj.sync=”formInfo.ref”
:data=”formInfo.data”
:fieldList=”formInfo.fieldList”
:rules=”formInfo.rules”
:labelWidth=”formInfo.labelWidth”
:listTypeInfo=”listTypeInfo”>
</page-form>
</page-dialog>
</div>
</template>
封装一个搜索栏(功能栏)组件
根据需求设计数据结构
参数设计
搜索参数 query,比如要查询的参数有账号,名字。
dom 相关属性设计
首先要考虑 dom 的类型,和显示,这是基本的,还有扩展类型,比如事件可以设置 event 属性,是否显示设置 show 属性,这些是比较通用的。而基于不同类型的 dom,如果是 input,select,datetime 类型的 dom,作为一个承载数据的容器,则需要一个 value 属性去和 query 中的属性名对上,除此之外不同类型的 dom 还有不同的特定属性,比如 select 需要提供对应的 list,datetime 需要相关的 pickersOptions 去限制时间范围,如果是按钮,比如 el-button, 则可以设置 icon,按钮相关 type。
最终实现:
filterInfo: {
query: {
create_user: ”,
account: ”,
name: ”
},
list: [
{type: ‘input’, label: ‘ 账户 ’, value: ‘account’},
{type: ‘input’, label: ‘ 用户名 ’, value: ‘name’},
// {type: ‘select’, label: ‘ 创建人 ’, value: ‘create_user’},
// {type: ‘date’, label: ‘ 创建时间 ’, value: ‘create_time’},
{type: ‘button’, label: ‘ 搜索 ’, btType: ‘primary’, icon: ‘el-icon-search’, event: ‘search’, show: true},
{type: ‘button’, label: ‘ 添加 ’, btType: ‘primary’, icon: ‘el-icon-plus’, event: ‘add’, show: true}
]
}
循环的 dom 列表
设计 dom 结构
先考虑设计的这个 dom 需要什么属性
比如 dom 是 el-input,一个输入框,可以设置是否禁止 disabled,可以设置是否可清空 clearable,v-model 要绑定的数据,设置 dom 的 class 名,设置 dom 绑定的事件。比如 dom 是 el-select,除了上面这些属性,我们还需要这个元素可循环的 list
最终 dom 结构为:
<div class=”filter-item” v-for=”(item, index) in getConfigList()” :key=”index”>
<!– <label class=”filter-label” v-if=”item.type !== ‘button'”>{{item.key}}</label> –>
<!– 输入框 –>
<el-input
:class=”`filter-${item.type}`”
v-if=”item.type === ‘input'”
:type=”item.type”
:disabled=”item.disabled”
:clearable=”item.clearable || true”
:placeholder=”getPlaceholder(item)”
@focus=”handleEvent(item.event)”
v-model=”searchQuery[item.value]”>
</el-input>
<!– 选择框 –>
<el-select
:class=”`filter-${item.type}`”
v-if=”item.type === ‘select'”
v-model=”searchQuery[item.value]”
:disabled=”item.disabled”
@change=”handleEvent(item.even)”
:clearable=”item.clearable || true”
:filterable=”item.filterable || true”
:placeholder=”getPlaceholder(item)”>
<el-option v-for=”(item ,index) in listTypeInfo[item.list]” :key=”index” :label=”item.key” :value=”item.value”></el-option>
</el-select>
<!– 时间选择框 –>
<el-time-select
:class=”`filter-${item.type}`”
v-if=”item.type === ‘time'”
v-model=”searchQuery[item.value]”
:picker-options=”item.TimePickerOptions”
:clearable=”item.clearable || true”
:disabled=”item.disabled”
:placeholder=”getPlaceholder(item)”>
</el-time-select>
<!– 日期选择框 –>
<el-date-picker
:class=”`filter-${item.type}`”
v-if=”item.type === ‘date'”
v-model=”searchQuery[item.value]”
:picker-options=”item.datePickerOptions || datePickerOptions”
:type=”item.dateType”
:clearable=”item.clearable || true”
:disabled=”item.disabled”
@focus=”handleEvent(item.event)”
:placeholder=”getPlaceholder(item)”>
</el-date-picker>
<!– 按钮 –>
<el-button
:class=”`filter-${item.type}`”
v-else-if=”item.type === ‘button'”
v-waves
:type=”item.btType”
:icon=”item.icon”
@click=”handleClickBt(item.event)”>{{item.label}}</el-button>
</div>
</div>
事件的处理
事件怎么绑定在 dom 上
绑定事件,可以在数据结构中给 dom 设置一个 event 属性,比如说是查询 search,在 dom 结构中我们可以设计一个中间层函数去处理,比如:
<!– 按钮 –>
<el-button
:class=”`filter-${item.type}`”
v-else-if=”item.type === ‘button'”
v-waves
:type=”item.btType”
:icon=”item.icon”
@click=”handleClickBt(item.event)”>{{item.label}}</el-button>
中间层函数接收事件类型,然后统一处理。
组件中的函数,外部怎么处理
我觉得组件的话,就承载一个去重复的作用,将所以重复的事情去除就可以,像如果是表格,表单,功能栏类似这种可能显示重复但是事件多变性的组件,我们则可以考虑将它们的事件派发到业务相关页面处理,组件保持去除重复的工作,简单干净明了就好了。将事件全部交给父级处理:
// 绑定的相关事件
handleEvent (evnet) {
this.$emit(‘handleEvent’, evnet)
},
// 派发按钮点击事件
handleClickBt (event, data) {
this.$emit(‘handleClickBt’, event, data)
}
封装一个 tree 组件
在后台管理页面树状组件用到次数实在太多了,静态的树数据加载,动态的树数据懒加载,左键点击事件,右键点击事件等等,封装之后,哼哼,谁用谁知道,一个字,爽。
设计属性
其实就是将 elementui 中的大部分用上的 tree 属性加上,然后再设计一部分让组件更加好用的属性,部分举个例子。
属性
类型
描述
lazy
Boolean
是否懒加载
lazyInfo
Array
懒加载相关数据
loadInfo
Object
正常相关数据
rightClick
Boolean
是否需要右键菜单
rightMenuList
Array
右键菜单列表
懒加载数据和正常加载数据结构的详细设计
/**
* 懒加载相关数据
* key -> 唯一标识 label -> 显示 type -> 类型 api -> 接口 params -> 参数 leaf -> 是否叶子节点
*/
lazyInfo: {
type: Array,
default: () => {
return [
{
key: ‘id’,
label: ‘name’,
type: ”,
api: () => {},
params: {key: ”, value: ”, type: ‘url’}, // url/query->{data: [{key: ”, value: ”, default: ”}] type: ‘query’}
leaf: true
}
]
}
},
/**
* 正常加载相关
*/
loadInfo: {
key: ‘id’,
label: ‘name’,
api: () => {},
params: {key: ”, value: ”, type: ‘url’} // url/query->{data: [{key: ”, value: ”, default: ”}] type: ‘query’}
},
事件处理
事件处理同样是需要派发到父级处理,以保证组件的可复用性,通过中间件将树组件的相关事件派发搭到父级。
实现效果
懒加载树组件相关数据配置:
// 树相关信息
treeInfo: {
refresh: false,
refreshLevel: 0,
nodeKey: ‘key’,
lazy: true,
type: 0, // 省市区类型
lazyInfo: [
{
key: ‘id’,
label: ‘name’,
type: 1,
api: getAllApi,
params: {key: ‘pid’, value: 1, type: ‘url’}
},
{
key: ‘id’,
label: ‘name’,
type: 2,
api: getAllApi,
params: {key: ‘pid’, value: ”, type: ‘url’},
leaf: true
}
],
rightMenuList: []
},
懒加载树 dom 结构:
<div class=”page-tree” v-loading=”treeLoading” @contextmenu.prevent=”handleTreeClick”>
<el-tree
class=”tree-component disabled-select”
ref=”TreeComponent”
:show-checkbox=”checkBox”
:node-key=”nodeKey”
:data=”treeData”
:load=”handleLoadNode”
:lazy=”lazy”
:draggable=”draggable”
:allow-drop=”handleDrop”
:expand-on-click-node=”false”
:check-strictly=”checkStrictly”
:filter-node-method=”filterNode”
:default-checked-keys=”defaultChecked”
:default-expanded-keys=”defaultExpanded”
@node-click=”handleClickLeft”
@node-contextmenu=”handleClickRight”
@check=”handleCheck”
@check-change=”handleCheck”
@current-change=”handleCheck”
@node-expand=”handleCheck”
highlight-current
:render-content=”renderContent”
:props=”treeProps”>
</el-tree>
<!– 右键菜单 –>
<ul class=’contextmenu’ v-show=”rightMenu.show” :style=”{left: rightMenu.left +’px’,top: rightMenu.top +’px’}”>
<li v-for=”(item, index) in rightMenu.list” :key=”index” @click=”handleRightEvent(item.type, item.data, item.node, item.vm)”>{{item.name}}</li>
</ul>
</div>
实现效果:
总结
本文以后台管理页面为例,一般后台管理页面常用到的 tree, table, form, dialog, 搜索栏已经全部做成了可复用的组件,只需要配置好相关数据,引入组件即可使用。关于组件的相关逻辑,可能要在文章里面一次性说清楚,还是需要费很大的精力,不过希望数据驱动的思想能够让之前没有体会到这种开发乐趣的小伙伴们有到新的想法。