基于Tsx+Element+Vue3表格组件的二次封装

506 阅读6分钟

一、写在前面

在后台管理系统中,表格可以说是使用率最高的组件。在ElementUI这个框架加持下,table不再像原生那么复杂。可能是我比较崇尚JSON配置,我希望将Element Table中的table-column抽离成JSON模式去渲染,将配置与UI分离,从而二次封装了基于Tsx+Element的Table组件。

之前做后台管理系统的时候有做过一版,后面每次做后台管理系统就会复制一份过去,遇到特殊需求直接修改组件。这里的原因是因为我没有完全集成Element的原生属性,而是平时使用频率较高的属性。

所以这次开发主要解决的就是,复制原生集成问题。

那么复制的解决方法就是上npm,原生集成就用代码解决。但是之前那一版使用的是vue2+js开发的,那么现在升级成vue3+ts开发。还是得讲一下整个心路历程和踩的坑。

本帖只分享自己开发组件时的思考和遇到的问题,涉及如何使用可以看组件的README.md

二、前期构想

这里的TsxElementTable(组件的名称),根据我做过10套左右的后台管理系统看来,需要分为三块。分别是:操作栏、表格、分页68747470733a2f2f7a686f6e677a68616f6c692e6769746875622e696f2f7075626c69634173736574732f66697273742e6a7067.jpeg 这里就直接展示我的成品图。这里的三块内容除了表格,都是可以选择隐藏的。

但是这三块不拆分组件,也就是说你不可以单独引入操作栏组件,也不能单独引入表格组件。这里不是因为技术实现不了,是我对需求上的认知吧,这三块在这个大组件内是存在交互的,尽管在代码上确实可以分离。

三、操作栏实现

操作栏右边三个按钮是固定的,功能分别为:刷新数据、紧凑程度、字段管理

左边按钮是可JSON配置的,需要传入labelkeytype,且按钮样式是固定的。当然也可以自定义内容。可以通过slot插槽放入你想要的东西。

这一块内容用vue模版语法写确实易如反掌,可能是我第一次写Tsx还是踩了很多坑,或者说还不够熟悉,作为记录还是要说一下。

如果使用Tsx,那么就得用vue提供的defineComponent函数生成函数式组件。在setup函数通过解构拿到props,其中的handle - columns数组很好处理,跟模版语法差不多,代码如下:

  {(props.handle?.columns || []).map((item) => {
    return (
      <el-button
        size={handleSize}
        key={item.key}
        type={item.type || ''}
        onClick={item.action ?? (() => handleClick(item.key))}
      >
        {item.label}
      </el-button>
    );
  })}

同时可以在setup拿到slots,这个是这个组件中所有的slots,那么需要专门设置一个key值判别是属于操作栏的插槽。

重点是这里,要以函数式组件的思想去想这个插槽问题。在模版语法中就是嵌套HTML模版,但是在Tsx中插槽内容是一段函数。想要渲染这段插槽,那就需要运行这段函数。(见笑了)

// 逻辑语句内容
function getHandleSlot() {
  return slots[DEFAULT_HANDLE_SLOT_KEY];
}
const slot = getHandleSlot();

// 使用Tsx返回模版,渲染插槽内容
<div className="tetHandleLeftSlotBox">{slot && slot()}</div>

那么插槽的无论任何事件丢给开发者去处理,JSON配置的操作按钮通过统一的handle-click函数返回,会将JSON中配置的key值作为标识返回给回调函数。或者在JSON中可以配置action函数直接点击后直接执行对应代码。

表格刷新功能抛出函数让开发者处理,紧凑程度使用响应式数据直接动态改变Element tablesize属性,字段管理在表格实现会提及。

四、表格实现

表格主要有3点难点,集成原Table的所有配置、table-columns的JSON模式、字段管理。

props可以直接通过解构赋值进行一个传递。

  <el-table
    ...
    {...tableProps}>
  </el-table>

event需要注意驼峰命名的问题,event的获取方式是通过const instance = getCurrentInstance();获取的所以都是小写,需要做一定的转换。 因为在组件内获取到开发者传递的on-xxxx事件,所以对应的我们要生成emit抛出事件。

  const instance = getCurrentInstance();
  const events: any = {};
  const eventKeys = Object.keys(instance?.vnode?.props ?? []).filter(
    (v) => v.startsWith('on') && !v.startsWith('onUpdate')
  );
  eventKeys.forEach((key) => {
    const newKey = key.replace('on', '');
    const emitKey = newKey
      .replace(/([a-z])([A-Z])/g, '$1-$2')
      .toLowerCase();
    events[key] = (...args: any[]) => {
      emit(emitKey, ...args);
    };
  });
  return (
    <div className="tetTableBox">
      <el-table
        ...
        {...events}
      >
      ...
      </el-table>
    </div>
  );

第二个table-column的JSON配置,我们还是延用Element-TableColumn-Prop的属性,让开发者能够在JSON里编写对应Element的Props属性。首先就是TS的加持。

type ElTableColumnProps = InstanceType<typeof ElTableColumn>['$props'];

这就是Element给TableColumn的TS支持,我们直接延用生成一个JSON。这里的TableColumnProps继承了上面的ElTableColumnProps,原因是我添加了一些自己的东西,便于开发字段管理。

export const tableColumns: TableColumnProps[] = [
  {
    label: '用户名称',
    align: 'center',
    prop: 'user_name'
  },
  {
    label: '操作名称',
    align: 'center',
    prop: 'operation'
  },
  {
    label: '操作时间',
    align: 'center',
    prop: 'create_time'
  },
  {
    label: '操作状态',
    align: 'center',
    prop: 'response_code'
  }
];

然后就是简单的循环JSON,渲染出来即可。

function renderTableColumn(column: HandleDisplayProps){
    ...
    return (
      column.show && (
        <el-table-column {...column} key={column.prop}>
          {columnSlots}
        </el-table-column>
      )
    );
}

{unref(columns).map((column: HandleDisplayProps) => {
  return renderTableColumn(column);
})}

最后就是字段管理,这里就只说逻辑吧,懒得复制代码了...

首先在TableColumnProps这个Interface加上一个show属性,来控制表格是否展示此字段,对应的就是是否在循环中渲染el-table-column这个组件。

做一个弹窗,将columncheckbox组件的UI形式循环渲染在弹窗中,让用户勾选是否展示,当一个column的show字段被改变,那么会重新运行renderTable函数(响应式系统),重新渲染表格列。

这里其实还有个做法可以优化,用空间换时间的方式。将开发者传过来的源数据加上show字段后复制一份出来专门做checkbox的实时修改。table展示的还是源数据。在弹窗添加一个确认按钮,点击确认后再将最新数据赋值给源数据。这样做会好很多。

三、分页栏实现

分页栏没什么额外的东西,就是做一个数据传递问题。通过语法糖实现组件之间的双向绑定即可。

  const currentPage = props?.currentPage || DEFAULT_PAGE;
  const pageSize = props?.pageSize || DEFAULT_PAGE_SIZE;
  const total = props?.pagination?.total || DEFAULT_PAGE_TOTAL;
  const _paginationShow = props.pagination?.show ?? DEFAULT_PAGE_SHOW;
  const paginationShow =
    _paginationShow === 'auto' ? total > pageSize : _paginationShow;
  if (!paginationShow) return;
  const onPageChange = (pageNum: number) => {
    emit('update:currentPage', pageNum);
    emit('page-change', { currentPage: pageNum, pageSize });
  };
  const onSizeChange = (pageSize: number) => {
    emit('update:pageSize', pageSize);
    emit('page-change', { currentPage, pageSize });
  };

三、最后

这个组件第一版结束之后,直接运用在公司项目上。没有满足所有需求,但也在一点一点更新。因为是用Ts写的所以每次上包之前都得build一次,挺容易忘记的,所以可能一次就会上两个包。后面需要继续尝试将代码拆分成不同的组件,开发者可以分三块引入。这点其实不难,但是之前尝试过一下,好像总出错。以后再研究吧~

以下是源码和npm的安装地址:如果喜欢可以点个star,谢谢!

Github:github.com/zhongzhaoli…

Npm:www.npmjs.com/package/tsx…