一、写在前面
在后台管理系统中,表格可以说是使用率最高的组件。在ElementUI这个框架加持下,table不再像原生那么复杂。可能是我比较崇尚JSON配置,我希望将Element Table中的table-column抽离成JSON模式去渲染,将配置与UI分离,从而二次封装了基于Tsx+Element的Table组件。
之前做后台管理系统的时候有做过一版,后面每次做后台管理系统就会复制一份过去,遇到特殊需求直接修改组件。这里的原因是因为我没有完全集成Element的原生属性,而是平时使用频率较高的属性。
所以这次开发主要解决的就是,复制和原生集成问题。
那么复制的解决方法就是上npm,原生集成就用代码解决。但是之前那一版使用的是vue2+js开发的,那么现在升级成vue3+ts开发。还是得讲一下整个心路历程和踩的坑。
本帖只分享自己开发组件时的思考和遇到的问题,涉及如何使用可以看组件的README.md。
二、前期构想
这里的TsxElementTable(组件的名称),根据我做过10套左右的后台管理系统看来,需要分为三块。分别是:操作栏、表格、分页。
这里就直接展示我的成品图。这里的三块内容除了表格,都是可以选择隐藏的。
但是这三块不拆分组件,也就是说你不可以单独引入操作栏组件,也不能单独引入表格组件。这里不是因为技术实现不了,是我对需求上的认知吧,这三块在这个大组件内是存在交互的,尽管在代码上确实可以分离。
三、操作栏实现
操作栏右边三个按钮是固定的,功能分别为:刷新数据、紧凑程度、字段管理。
左边按钮是可JSON配置的,需要传入label、key、type,且按钮样式是固定的。当然也可以自定义内容。可以通过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 table的size属性,字段管理在表格实现会提及。
四、表格实现
表格主要有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这个组件。
做一个弹窗,将column以checkbox组件的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…