27
在日常开发中,我们肯定不止一次碰到重复的业务代码,明明功能相似,但总没思路去把它封装成组件。关于封装组件,希望这篇文章能带给大家新的思路,去更高效的完成日常开发。(注:例子都是基于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}
        ]
      }

clipboard.png

表格配置以及生成效果
      // 表格相关
      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}
          ]
        }
      }

clipboard.png

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>

clipboard.png

封装一个搜索栏(功能栏)组件

根据需求设计数据结构

参数设计

搜索参数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>

实现效果:

clipboard.png

总结

本文以后台管理页面为例,一般后台管理页面常用到的tree, table, form, dialog, 搜索栏已经全部做成了可复用的组件,只需要配置好相关数据,引入组件即可使用。
关于组件的相关逻辑,可能要在文章里面一次性说清楚,还是需要费很大的精力,不过希望数据驱动的思想能够让之前没有体会到这种开发乐趣的小伙伴们有到新的想法。

我在长安长安
1.2k 声望42 粉丝

一个很懒的假前端