引言
相信大家只要是用vue写管理端的,大多都是用element plus这个组件库,在管理端的开发中,表格的需求尤其常见,在不同的需求下,大家都有各种的二次封装方法,下面我继续以小明的身份和大家分享一下,希望大家有更好的idea能在下面给我些建议,一起交流学习
封装前的代码
刚刚入职的小明,收到了一个管理端的数据展示需求,按照国际惯例,机智的小明先去看看有没有前辈的代码可以学习参考,下面就是小明找到的参考代码,一个很标准的表格
ts
<template>
<el-form>
<el-form-item label="主题搜索">
<el-input v-model="search.name" placeholder="输入搜索主题" />
</el-form-item>
</el-form>
<el-table class="my-table-class" v-loading="loading" :data="tableDataAll">
<el-table-column prop="id" fixed="left" label="数据ID" min-width="80" column-key="id" />
<!-- 此处省略几十行的表格数据 -->
</el-table>
<div class="table-footer">
<div class="total-num">共 {{ total }} 条数据</div>
<el-pagination
:total="total"
v-model:page-size="search.page_size"
v-model:current-page="search.page_num"
></el-pagination>
</div>
</template>
<script lang="ts" setup>
import * as API from '@web/server/api';
import { onMounted, reactive, ref, nextTick, onActivated } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
// 查询参数
const search = reactive({
page_num: 1,
page_size: 50,
name: '',
});
/* 总数 */
const total = ref(0);
/* 表格数据 */
const tableDataAll = ref([]);
const loading = ref(true);
const tableRef = ref(null);
const fetchTableData = async () => {
const params = { ...search };
loading.value = true;
const res = await API.queryDataList(params);
loading.value = false;
if (!res) return;
tableDataAll.value = res.tableDataAll;
total.value = res.total;
};
// 表格跳转时候记录当前的定位
let scrollPosition = 0;
onBeforeRouteLeave(() => {
const tableDom = window.document.querySelector(`.my-table-class .el-scrollbar__wrap`);
scrollPosition = tableDom?.scrollTop || 0;
});
const scrollToTarget = () => {
tableRef.value?.scrollTo({ top: scrollPosition });
};
onActivated(() => {
scrollToTarget();
fetchTableData();
});
</script>
看到这段代码,小明很纳闷,因为在小明的印象中,表格展示需求一般就是分为两步
- 获取数据
- 将数据传给el-table组件
但是上述的代码,不止是这两个步骤,还有一个表格的定位问题。小明眉头一皱,感觉这业务代码就应该是写业务相关的,这种定位问题的,是每个表格都需要的,应该被封装起来。还有那个分页器,每个表格下面都有显示,也应该被封装进去,于是小明开工了
应该封装成什么样子
对于表格这个表格应该封装哪些东西,封装成什么样子,小明觉得有下面几点要求
- 首先是要支持el-table所有的属性,就是主体对象依然是el-table,它的属性都要支持,都要透传
- 其次是对于获取数据的形式,小明想着,授人予鱼不如授人予渔,想把获取数据的行为和状态内聚到这个组件里面
- 最后是ts,对表格进行封装,不能说丢了ts类型
下面是封装后的代码
ts
<template>
<MyTable ref="tableRef" :fetch-data="fetchData">
<template #search>
<div class="multiple-search-container">
<el-form>
<el-form-item label="主题搜索">
<el-input v-model="search.name" placeholder="输入搜索主题" />
</el-form-item>
</el-form>
</div>
</template>
<template #default>
<el-table-column prop="id" fixed="left" label="数据ID" min-width="80" column-key="id" />
<!-- 此处省略几十行的表格数据 -->
</template>
</MyTable>
</template>
<script lang="ts" setup>
import MyTable, { IMyTableFetchFunc } from '@web/components/MyTable/index.tsx';
import * as API from '@web/server/api';
import { reactive } from 'vue';
// 查询参数
const search = reactive({
name: '',
});
const fetchData: IMyTableFetchFunc = async options => {
const params = {
...options.params,
...search,
};
const res = await API.queryDataList(params);
const data = res.data;
return {
data: data,
total: res.total,
};
};
</script>
下面是IMyTableFetchFunc的类型定义
ts
export type IRequestParam = {
params: { page_size: number; page_num: number };
sort: { [key: string]: 'descending' | 'aescending' };
filter: { [key: string]: Array<string | number | boolean> };
};
export type IRequestRsp<T = any> = { data: T[]; total: number };
export type IMyTableFetchFunc<T = any> = (options: IRequestParam) => Promise<IRequestRsp<T>>;
从上述代码可以看出,使用了新封装的表格,业务层的职责如下
- 需按照将IMyTableFetchFunc给表格组件提供一个如何获取数据的异步函数
- 需要对表格所需要展示列的信息进行定义,就是写el-table-column
- 业务层需要维护一些搜索查询的条件
而表格里面需要做的事情如下
- 根据业务层提供的fetchData去获取和维护表格数据
- 维护分页状态,每一页的大小,第几页等
- 表格的滑动位置信息,当页面会退时候进行还原
里面最重要的是维护表格数据,不仅要在页面渲染时候请求,而且无论是外面的搜索条件状态变化还是组件里面自身的分页状态变化,都要触发重新请求去刷新数据
如何实现
这里分成三点,第一点是这个表格的核心实现,其它是比较繁琐的点,遇到的困难
- 表格数据维护 - 如何通过外层传入的fetchData和里面的分页状态去更新展示数据
- 区分内外部状态
表格数据维护
不多时,小明便用了五十行代码实现了雏形
tsx
defineComponent({
props: {
// 透传ElTable的属性
...ElTable.props,
// 加了一下自定义的属性
...mktTableProps,
},
setup(props, { slots, emit, expose }) {
const { fetchData } = props;
// 分页信息的控制
const pageSize = ref(props.pageSize);
const total = ref(0);
const pageNum = ref(1);
const {
data: serverData,
isLoading,
} = useAsyncData(async () => {
const res = await fetchData?.({
params: { page_num: pageNum.value, page_size: pageSize.value },
});
if (!res) return [];
total.value = res.total;
return res.data;
});
return () => {
return (
<div class="mkt-table">
{withDirectives(
<ElTable
{...props}
data={serverData.value}
>
{{ ...slots }}
</ElTable>,
[[vLoading, isLoading.value]]
)}
<div class="table-footer">
<div class="total-num">共 {total.value} 条数据</div>
<ElPagination
total={total.value}
pageSize={pageSize.value}
onUpdate:page-size={val => (pageSize.value = val)}
pageSizes={props.pageSizes}
currentPage={pageNum.value}
onUpdate:current-page={val => (pageNum.value = val)}
></ElPagination>
</div>
</div>
);
};
},
});
上述代码实现了根据业务层提供的fetchData去获取和维护表格数据,里面使用了useAsyncData,这个是上一期文章介绍的一个工具,可以理解为就是一个支持异步的computed函数,里面是一个异步函数,当里面的依赖变化时候,就会重新计算更新响应式的data和isLoading。
从上述代码中可以看出,useAsyncData里面的函数,订阅了pageNum和pageSize两个响应式变量,以及业务层传进来的fetchData,这个函数在很前面,是长这样的
ts
const fetchData: IMyTableFetchFunc = async options => {
const params = {
...options.params,
...search,
};
const res = await API.queryDataList(params);
const data = res.data;
return {
data: data,
total: res.total,
};
};
可以看到这个fetchData里面订阅search这个reactive的响应式变量,所以无论是外部的搜索条件改变了,还是里面的分页条件改变了,都能触发这次effect,就都能刷新我们的数据内容了
到此,小明已经把授人以鱼 转变成了授人以渔,成功地通过业务层传入的获取数据函数,配合自己维护去分页状态,在合适的时机去请求和更新表格的数据了
滑动位置的还原
主要就是在RouteLeave这个时机记录一下位置,在回退时候就会触发onActivated,这时候还原一下位置就好
ts
let scrollPosition = 0;
onBeforeRouteLeave(() => {
const tableDom = window.document.querySelector(`.${curInstantClass} .el-scrollbar__wrap`);
scrollPosition = tableDom?.scrollTop || 0;
});
const scrollToTarget = () => {
tableRef.value?.scrollTo({ top: scrollPosition });
};
onActivated(() => {
scrollToTarget();
refresh(); // 回退时候顺便刷新一下
});
区分内外部状态变化
就在小明得意洋洋的时候,测试同学的建议就来了,说当切换到表格第二页时候进行搜索,必须把表格的页数切回第一页,不然就会搜不到东西。上述的代码只是当依赖变化时候就更新表格数据,所以需要监听搜索条件变化时候,把内部的页数重置一下。
这时候问题就来了,这个表格是一个通用的组件,如何监听是外部条件变化?
经过一番思考,小明想出了两个主意:
- 第一个是需要业务层多传一个方法,之后组件里面去调用这个方法,订阅这个方法的依赖变化再去重置表格的页数。
- 第二个就是在原来的fetchData这个方法做文章,认为这个方法订阅的状态就是外部状态的变化
内部状态 | 外部状态 |
---|---|
pageNum、pageSize等 | 通过fetchData订阅到的业务层的搜索条件等状态 |
小明想着第一个方法还需要多传一个函数,用起来贼麻烦,多个香炉多个鬼,之后就选择了第二个方法,直接watch里面去调用fetchData,这样就能监听到是外部依赖的变化
ts
watch(
async () => {
try {
// 这里只是为了要订阅外部状态的变化
await fetchData?.({
params: { page_num: -999999, page_size: 999999 }
});
} catch (e) {}
},
() => {
// 监听到外部依赖变动,页面改成1,回滚到最上方
pageNum.value = 1;
scrollPosition = 0;
scrollToTarget();
}
);
但这个方法会导致多请求一次,我这里就在统一封装的请求方法那里拦截page_num为-999999就不发起请求
其它
还有些拦截sortChange和filterChange去记录参数,以减少业务层无谓的请求状态,业务层直接在option参数获取排序状态和过滤状态,像下面一样
ts
const fetchData: IMyTableFetchFunc = async options => {
const { params, sort, filter } = options;
const sortParams = Object.entries(sort).reduce(
(pv, [key, val]) => Object.assign(pv, { [`${key}_sort`]: val === 'descending' ? 2 : 1 }),
{} as any
);
const _params = {
...params,
...searchParams,
...sortParams,
tag: filter['tag']?.join?.(','),
};
const res = await API.fetchData(_params);
return {
data: res.list_data,
total: res.total,
};
};
table里面的实现如下拦截filterChange和sortChange去记录状态,并在调用fetchData时候传入
ts
const sortParams = ref<any>({});
const filterParams = ref<any>({});
const {
data: serverData,
isLoading,
} = useAsyncData(async () => {
const res = await fetchData?.({
params: { page_num: pageNum.value, page_size: pageSize.value },
// 将排序和过滤的参数回传
sort: sortParams.value,
filter: filterParams.value,
});
if (!res) return [];
total.value = res.total;
return res.data;
});
const filterChange = val => {
Object.assign(filterParams.value, { ...val });
emit('filterChange', val) // 把事件传回业务
};
const sortChange = val => {
const sortKey = val.column?.columnKey || val.prop;
sortParams.value = sortKey ? { [sortKey]: val.order } : {};
emit('sortChange', val) // 把事件传回业务层
};
总结
到了这里,小明就得到一个很棒的表格组件,业务层的状态少了很多,不用再去维护那些loading、分页、排序、过滤等状态。多个香炉多个鬼,多个状态多个不确定性,通过这个表格,小明更好地将一些非业务逻辑内聚于组件之中,从此小明能够一心投入业务,能够更好地解决表格需求了。由于这种表格需求还是比较业务化的,还是需要根据自己业务的具体情况再去改造的,希望小明写代码的经历对大家有帮助。