• 作者:老汪软件技巧
  • 发表时间:2024-10-09 07:00
  • 浏览量:

引言

相信大家只要是用vue写管理端的,大多都是用element plus这个组件库,在管理端的开发中,表格的需求尤其常见,在不同的需求下,大家都有各种的二次封装方法,下面我继续以小明的身份和大家分享一下,希望大家有更好的idea能在下面给我些建议,一起交流学习

封装前的代码

刚刚入职的小明,收到了一个管理端的数据展示需求,按照国际惯例,机智的小明先去看看有没有前辈的代码可以学习参考,下面就是小明找到的参考代码,一个很标准的表格


<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>

看到这段代码,小明很纳闷,因为在小明的印象中,表格展示需求一般就是分为两步

但是上述的代码,不止是这两个步骤,还有一个表格的定位问题。小明眉头一皱,感觉这业务代码就应该是写业务相关的,这种定位问题的,是每个表格都需要的,应该被封装起来。还有那个分页器,每个表格下面都有显示,也应该被封装进去,于是小明开工了

应该封装成什么样子

对于表格这个表格应该封装哪些东西,封装成什么样子,小明觉得有下面几点要求

下面是封装后的代码


<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的类型定义

export type IRequestParam = {
  params: { page_size: number; page_num: number };
  sort: { [key: string]: 'descending' | 'aescending' };
  filter: { [key: string]: Array<string | number | boolean> };
};
export type IRequestRspany> = { data: T[]; total: number };
export type IMyTableFetchFuncany> = (options: IRequestParam) => Promise<IRequestRsp>;

从上述代码可以看出,使用了新封装的表格,业务层的职责如下

而表格里面需要做的事情如下

里面最重要的是维护表格数据,不仅要在页面渲染时候请求,而且无论是外面的搜索条件状态变化还是组件里面自身的分页状态变化,都要触发重新请求去刷新数据

如何实现

这里分成三点,第一点是这个表格的核心实现,其它是比较繁琐的点,遇到的困难

表格数据维护

不多时,小明便用了五十行代码实现了雏形

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,这个函数在很前面,是长这样的


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,这时候还原一下位置就好

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(); // 回退时候顺便刷新一下
});

区分内外部状态变化

就在小明得意洋洋的时候,测试同学的建议就来了,说当切换到表格第二页时候进行搜索,必须把表格的页数切回第一页,不然就会搜不到东西。上述的代码只是当依赖变化时候就更新表格数据,所以需要监听搜索条件变化时候,把内部的页数重置一下。

这时候问题就来了,这个表格是一个通用的组件,如何监听是外部条件变化?

经过一番思考,小明想出了两个主意:

内部状态外部状态

pageNum、pageSize等

通过fetchData订阅到的业务层的搜索条件等状态

小明想着第一个方法还需要多传一个函数,用起来贼麻烦,多个香炉多个鬼,之后就选择了第二个方法,直接watch里面去调用fetchData,这样就能监听到是外部依赖的变化

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参数获取排序状态和过滤状态,像下面一样


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时候传入

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、分页、排序、过滤等状态。多个香炉多个鬼,多个状态多个不确定性,通过这个表格,小明更好地将一些非业务逻辑内聚于组件之中,从此小明能够一心投入业务,能够更好地解决表格需求了。由于这种表格需求还是比较业务化的,还是需要根据自己业务的具体情况再去改造的,希望小明写代码的经历对大家有帮助。