从一次需求封装,对比函数式组件与模板组件优劣

1,626 阅读6分钟

开发总是领导们的玩具,领导们的需求就是朝令夕改。这不,领导上网随便一搜一个时髦的后台框架,一眼就相中了列展示这个骚功能(注意,下面的实现都是Vue2)。具体如下:

列展示功能展示.gif 由于用的是Element框架,本身就不提供这样的方法,没办法,这只能自己去造了。

实现模板表格封装

要实现这样的功能,基本上是需要多选框表格相互配合好才行,由于用过像Ant Design Vue这样的框架,所以我第一时间就想到的是写对象数组,参考el-table-column的属性,我就可以得出下面的数据接口。

const tableData  = [
    {
      title: '测试',
      unit_name: '这是单位',
      unit_nature_name: '这是单位性质',
      nature_name: '这是职位性质'
    }
]

const tableColumn = [
    {
      prop: 'title',
      label: '职位名称',
    },
    {
      prop: 'unit_name',
      label: '单位名称',
    },
    {
      prop: 'unit_nature_name',
      label: '单位性质',
    },
    {
      prop: 'nature_name',
      label: '职位性质',
    },
]

接着我只需要设计一个组件,如下:

<template>
  <el-table
    v-bind="$attrs"
    :cell-style="{
      height: '76px',
    }"
    :header-cell-style="{
      height: '56px',
      backgroundColor: '#FCF2F1',
      color: '#573E41',
      textAlign: 'center',
    }"
  >
    <template v-for="(column, index) in tableColumn">
      <el-table-column
        :key="index"
        :prop="column.prop"
        :label="column.label"
        :width="column.width"
        align="center"
      >
      </el-table-column>
    </template>
  </el-table>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    tableColumn: {
      type: Array,
      default() {
        return [];
      },
    },
  },
};
</script>


<style scoped>
</style>

那么我就可以实现一个最基础的渲染了表格。如下:

Snipaste_2023-10-27_16-13-08.png 当然,表格的需求往往没这么简单,剩下的需求实现,我就斗胆献丑了,给大家参考一下我的最终代码:

<template>
  <el-table
    ref="caitlynTable"
    v-loading="loading"
    border
    :row-key="rowKey"
    max-height="500px"
    :cell-style="{
      height: '76px',
    }"
    :header-cell-style="{
      height: '56px',
      backgroundColor: '#FCF2F1',
      color: '#573E41',
      textAlign: 'center',
    }"
    v-bind="$attrs"
    @selection-change="selectionChange"
  >
    <el-table-column
      v-if="needIndex"
      :key="caitlynKey"
      type="selection"
      :selectable="selectable"
      width="55"
      align="center"
    ></el-table-column>

    <template v-for="(column, index) in tableColumn">
      <el-table-column
        v-if="!column.show"
        :key="index"
        :prop="column.prop"
        :label="column.label"
        :width="column.width"
        :fixed="column.fixed"
        :align="column.align ? column.align : 'center'"
        :formatter="
          (row, column, cellValue, index) => {
            if (!cellValue && cellValue !== 0) {
              return '--';
            } else {
              return cellValue;
            }
          }
        "
      >
        <template v-if="column.slotHeader" #header="scope">
          <slot :name="column.slotHeader" v-bind="scope"></slot>
        </template>
        <template v-if="column.slotName" #default="scope">
          <slot :name="column.slotName" v-bind="scope"></slot>
        </template>
      </el-table-column>
    </template>
  </el-table>
</template>
<script>
export default {
  props: {
    loading: {
      type: Boolean,
      default: false,
    },
    tableColumn: {
      type: Array,
      default() {
        return [];
      },
    },
    needIndex: {
      type: Boolean,
      default: false,
    },
    rowKey: {
      type: String,
      default: 'id',
    },
    selectionChange: {
      type: Function,
      default() {
        return {};
      },
    },
    selectable: {
      type: Function,
      default() {
        return {};
      },
    },
  },
  data() {
    return {
      caitlynKey: 'caitlynKey',
    };
  },
  watch: {
    tableColumn: {
      handler() {
        this.$refs.caitlynTable.doLayout();
      },
      deep: true,
    },
  },
};
</script>
<style scoped lang="scss"></style>

这样的封装已经能实现我项目的所有需求了。由于我需要自己手动改的表格非常的多,就单单这个修改需求,我就整整做了有一个星期有余,我就开始发现了不少缺点。(列展示组件的如何实现不是本文的重点,所以就不展示相关代码)

1、tableCloumn数据格式属性过多

因为原来的表格都是用Element上面的例子做的,所以就会出现很多el-table-column的情况,由于需要写一个tableColumn的数组,我需要写很多个数据,非常的麻烦,譬如:

{
  prop: '',
  label: '操作',
  width: '150',
  fixed: 'right',
  slotName: 'operation',
  slotHeader: 'operateHeader'
}

记的属性过多,插槽名虽然是自定义的,但是如果这个表格非常多的插槽的时候,会不会变成插槽灾难区呢?

Snipaste_2023-10-27_16-31-21.png 像上面这种没注释的,你根本不知道那个插槽是哪个,还不如el-table-column那种形式查看的实在。

2、需要兼容多个el-tabel-column的属性

我们都知道,封装兼容东西的大部分做法是,直接将他所有的功能都做了,你需要什么就自己配什么。比如我在封装这个组件的时候,基本是遇到一个问题,发现组件没有兼容,然后就需要在里面加,遇到一个加一个。其实这个里面就有心智成本,说到底这样的通用组件,避免不了有些功能没兼容的情况,你自己封装的人会很清楚,哪个属性干什么,但是对用第一次使用的人来说,他根本不知道你这个组件的功能实现需要哪个属性!然后就是去看一遍你的代码,更有甚至缺失的功能要他自己手动添加!

函数式组件的使用

基于上面的问题,我就在想,能不能不修改原来那么多el-table-column,直接就渲染他们出来,我只需要知道,哪个需要渲染而哪个不需要渲染就行了,基于这个原因,我想到了Vue提供的渲染函数:函数式组件。 可以参考这两个地址:Vue2函数式组件 函数式组件语法。那么下面展示代码:

const Table = {
  functional: true,
  name: 'wRenderTable',
  props: {
    selectionChange: {
      type: Function,
      default() {
        return {};
      },
    },
    tableColumn: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  render(h, context) {
    const {
      props,
      data: { attrs },
    } = context;
    const cellStyle = { height: '76px' };
    const headerCellStyle = {
      height: '56px',
      backgroundColor: '#FCF2F1',
      color: '#573E41',
      textAlign: 'center',
    };

    const column = props.tableColumn.map(item => item.label);
    return (
      <el-table
        vLoading={attrs.loading}
        {...{ attrs }}
        cell-style={cellStyle}
        header-cell-style={headerCellStyle}
        border
        row-key="id"
        vOn:selection-change={props.selectionChange}
      >
        {context.children.map(child => {
          const { propsData } = child.componentOptions;
          if (!propsData.type) {
            // 无数据格式化
            propsData.formatter = (row, column, cellValue) => {
              if (!cellValue && cellValue !== 0) {
                return '--';
              } else {
                return cellValue;
              }
            };
            // 默认居中
            !propsData.align && (propsData.align = 'center');
          }
          // propsData.type是选择框时才有。
          if (column.includes(propsData.label) || propsData.type) {
            return child;
          } else {
            return null;
          }
        })}
      </el-table>
    );
  },
};

export default Table;

context.children能获取到函数式组件下面所有的node节点,如下图:

Snipaste_2023-10-27_16-57-10.png 那么我如何控制,到底那个列不需要渲染呢。没错,我只需要通过遍历时,从传进来的tableColumn中判断子节点是否存在,来判断是否渲染,不渲染的时候时候,返回null即可。

优点

  1. 最大的优点就是,我不需要删除原来一大堆的el-table-column,又去写一个繁杂麻烦的对象数组。
  2. 使用者没有多少心智负担,比如列展示的组件会处理最终展示的列,只需要将这个列数组传给表格组件,通过判断即可处理渲染不渲染的逻辑。(为什么需要判断呢,因为获取的节点是全部的,只能内部判断那个节点需要渲染,可以看上面的判断逻辑,很简单)
  3. 最重要的是,由于可以使用原来el-talbel-column,就不需要配置在组件内部配置一大堆属性,而且插槽也不需要自己去定义名字,也不会出现一大堆插槽,不知道谁是谁的局面。
  4. 还有一个必须需要注意的点就是,函数式组件是无状态的,也没有实例的(this),这个意味着什么,我一开始也不知道,直到我碰到那个问题——我怎么知道进来tableColumn是不是最新的。我找了很久都不知道这个怎么去解决,函数式组件也没有watch方法。无状态,所以非常依赖外部状态,当props里面的属性,更新的时候,这个函数式组件也会重新渲染,那么不就意味着,我根本不需要管tableColumn的值,因为它自动会重新熏染。

最后的吹牛

感觉组件封装就是这样的,先是找到一个可以实现功能的方案,然后就是实现功能的过程中,发现这个方案非常的麻烦,然后就开始思考能不能最小化干扰原来的东西,也能修改。这大概就是我最终选用函数式组件的原因。觉得有用的兄弟够我点个赞或者是收藏,呆在3级好久了,谢谢兄弟们了(砰砰砰)