查看: 243|回复: 0

Vue3 Hooks封装useTable实现表格分页请求复用

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
Vue3 Composition API 中的 Hooks 是以 use 开头的组合式函数,用于封装可复用的状态逻辑。管理后台中表格的增删查改逻辑经常被复制到不同页面,维护成本较高。原文从 Vue3 官网的鼠标跟踪案例出发,先说明组合式函数如何抽取带响应式状态和生命周期的公共逻辑,再给出 useBorder 和 useTable 两个实战封装。下面按原文事实和代码逻辑重新整理。

一、从官网 useMouse 案例理解组合式函数

先看组件内直接实现的鼠标位置跟踪:
  1. <script setup>
  2. import { ref, onMounted, onUnmounted } from 'vue'
  3. const x = ref(0)
  4. const y = ref(0)
  5. function update(event) {
  6.   x.value = event.pageX
  7.   y.value = event.pageY
  8. }
  9. onMounted(() => window.addEventListener('mousemove', update))
  10. onUnmounted(() => window.removeEventListener('mousemove', update))
  11. </script>
  12. <template>Mouse position is at: {{ x }}, {{ y }}</template>
复制代码

这段代码把 x、y 状态写在组件内部,鼠标移动时更新坐标。与时间格式化这类无状态函数不同,鼠标位置是带响应式状态和副作用的逻辑,因此需要把它抽成组合式函数。原文将其提取到 mouse.js:
  1. // mouse.js
  2. import { ref, onMounted, onUnmounted } from 'vue'
  3. // 按照惯例,组合式函数名以“use”开头
  4. export function useMouse() {
  5.   // 被组合式函数封装和管理的状态
  6.   const x = ref(0)
  7.   const y = ref(0)
  8.   // 组合式函数可以随时更改其状态。
  9.   function update(event) {
  10.     x.value = event.pageX
  11.     y.value = event.pageY
  12.   }
  13.   // 一个组合式函数也可以挂靠在所属组件的生命周期上
  14.   // 来启动和卸载副作用
  15.   onMounted(() => window.addEventListener('mousemove', update))
  16.   onUnmounted(() => window.removeEventListener('mousemove', update))
  17.   // 通过返回值暴露所管理的状态
  18.   return { x, y }
  19. }
复制代码

组件中使用时,只负责引入和解构:
  1. <script setup>
  2. import { useMouse } from './mouse.js'
  3. const { x, y } = useMouse()
  4. </script>
  5. <template>Mouse position is at: {{ x }}, {{ y }}</template>
复制代码

这里的关键点是:useMouse 对外暴露 x 和 y,组件中解构后仍能保持响应式变化。由此可以理解 Hooks 的基本封装方式:管理状态、处理副作用、通过返回值暴露状态或方法。

二、封装 useBorder 处理 dataV 宽高自适应

原文项目使用了 dataV 组件。页面宽高发生变化时,dataV 组件的宽高有时无法自适应。官网给出的思路是在 resize 时修改组件的 key,让组件重新渲染。原始写法如下:
  1. <BorderBox8 class="borderbox8" :key="key"></BorderBox8>
  2. ...
  3. // dataV宽高自适应的key
  4. let key = ref(1);
  5. // 宽高改变框图自适应
  6. function initBorder() {
  7.   window.addEventListener('resize', function () {
  8.     nextTick(() => {
  9.       key.value++;
  10.     });
  11.   });
  12. }
  13. initBorder();
复制代码

由于多个页面都使用 dataV 组件,这段逻辑需要复用。页面 resize 后 key 会变化,符合组合式函数封装条件。原文封装了 useBorder:
  1. /*
  2. * @Description: 公用的Datav初始化逻辑,在使用dataV组件时使用
  3. */
  4. import { ref, onMounted, nextTick } from 'vue';
  5. export function useBorder() {
  6.   const key = ref<number>(1);
  7.   function initBorder() {
  8.     nextTick(() => {
  9.       console.log('resize', key.value);
  10.       key.value++;
  11.     });
  12.   }
  13.   onMounted(() => {
  14.     window.addEventListener('resize', initBorder);
  15.   });
  16.   return { key };
  17. }
复制代码

页面中使用:
  1. import { useBorder } from '@/hooks/useBorder';
  2. const { key } = useBorder();
复制代码

这样 dataV 宽高自适应逻辑就从页面中抽离出来,在需要的页面中通过 useBorder 获取 key。需要注意的是,原文 useBorder 只在 onMounted 中注册 resize 监听,没有展示 onUnmounted 移除监听的处理;在页面频繁切换或组件卸载的场景下,应额外关注事件监听清理。

三、封装 useTable 统一表格请求与分页

原文随后以管理后台表格为例,使用 TypeScript 封装 useTable。目标是提供表格数据请求和分页信息,并把请求后的数据暴露给组件。由于不同页面请求不同接口,因此 useTable 接收 requestApi,由调用方决定具体请求方法。

先看类型定义:
  1. export interface ListResponse<R extends object> {
  2.   code: number;
  3.   data: {
  4.     list: R[];
  5.   };
  6. }
复制代码

useTable 的完整实现如下:
  1. /*
  2. * @Description: 公用的table逻辑,在使用表格时使用
  3. */
  4. import { HttpResponse } from '@/utils/request';
  5. import { Paging, ListResponse } from '@/api/types';
  6. import { computed, reactive, toRefs } from 'vue';
  7. interface TableState<R extends object> {
  8.   tableData: R[];
  9.   loading: boolean;
  10. }
  11. interface OtherSearchParams {
  12.   [key: string]: unknown;
  13. }
  14. interface TableBaseMethodsType<R extends object> {
  15.   requestApi: (
  16.     page: number,
  17.     keyword?: string
  18.   ) => Promise<HttpResponse<ListResponse<R>>>;
  19.   otherSearchParams?: OtherSearchParams;
  20. }
  21. // table的常见数据请求和分页交互
  22. // 接收:表格数据的范型T, 请求方法以及参数
  23. // 抛出以下:
  24. export default function useTable<R extends object>(
  25.   config: TableBaseMethodsType<R>
  26. ) {
  27.   const { otherSearchParams = {}, requestApi } = config;
  28.   const tableState = reactive<TableState<R>>({
  29.     loading: false,
  30.     tableData: []
  31.   });
  32.   // 分页参数
  33.   const pageParams = reactive<Paging>({
  34.     page: 1
  35.   });
  36.   // 分页器配置项
  37.   const pagination = computed(() => {
  38.     return {
  39.       total: tableState.tableData.length,
  40.       curruntPage: pageParams.page
  41.     };
  42.   });
  43.   /**
  44.   * @description: 请求加载数据
  45.   * @return {*}
  46.   */
  47.   const loadList = async () => {
  48.     const page = pageParams.page;
  49.     const params = { ...otherSearchParams };
  50.     // console.log(params, 'params');
  51.     tableState.loading = true;
  52.     try {
  53.       const res = await requestApi(page, params.keyword);
  54.       console.log(res);
  55.       if (res.status === 200) {
  56.         console.log(res.data.data.list, 'data');
  57.         tableState.tableData = res.data.data.list;
  58.         console.log(tableState.tableData, '111');
  59.       }
  60.     } catch (error) {
  61.       console.log(error);
  62.       tableState.tableData = [];
  63.     } finally {
  64.       tableState.loading = false;
  65.     }
  66.   };
  67.   return {
  68.     ...toRefs(tableState),
  69.     ...toRefs(pageParams),
  70.     pagination,
  71.     loadList
  72.   };
  73. }
复制代码

从实现看,useTable 的职责比较清晰:

- 接收 requestApi,要求签名支持 page 和可选 keyword,返回 Promise<HttpResponse<ListResponse<R>>>。
- 接收 otherSearchParams 作为额外搜索参数,默认空对象,并在 loadList 中通过 { ...otherSearchParams } 合并。
- 内部维护 tableState,包括 tableData 和 loading。
- pageParams 维护分页参数,page 初始值为 1。
- pagination 是计算属性,total 取 tableState.tableData.length,curruntPage 取 pageParams.page。原文中拼写为 curruntPage。
- loadList 负责发起请求:先取当前 page,合并搜索参数,设置 loading 为 true;请求成功后判断 res.status === 200,再把 res.data.data.list 赋给 tableState.tableData;异常时清空 tableData;finally 中关闭 loading。
- 最后通过 toRefs 展开 tableState 和 pageParams,并返回 pagination 与 loadList。

组件侧拿到这些返回后,就可以使用 tableData、loading、page、pagination 和 loadList 完成表格数据加载与分页交互。原文没有继续展开组件模板代码,但 useTable 已经把请求、状态、分页配置和加载方法集中在同一个组合式函数中。

四、封装思路与注意点

结合 useMouse、useBorder、useTable 三个例子,可以归纳出原文的 Hooks 封装路径:

1. 组合式函数名以 use 开头,例如 useMouse、useBorder、useTable。
2. 函数内部可以创建 ref、reactive、computed,并随时更改这些状态。
3. 组合式函数可以挂靠组件生命周期,例如 onMounted、onUnmounted,用于注册或卸载副作用。
4. 通过返回值暴露组件需要的状态和方法,组件侧通过解构使用。
5. 对于表格这类场景,把请求 API、分页参数、加载状态、列表数据和加载方法统一封装,减少页面间的复制粘贴。

兼容性和实现细节上,原文代码也留下了一些值得注意的点:useBorder 示例只展示了 onMounted 注册 resize,没有在 onUnmounted 中移除监听;useTable 的 pagination.total 直接取 tableData.length,如果后端分页返回总条数,需要结合实际接口另行处理;useTable 中 requestApi 的 keyword 来自 otherSearchParams 合并后的 params.keyword,调用方应保证搜索参数结构符合预期。原文还同时出现了 ListResponse 的本地定义与从 @/api/types 导入的写法,实际项目中应统一类型来源,避免重复定义。

总体来看,Vue3 Composition API 的 Hooks 并不是单纯的函数抽取,而是把响应式状态、生命周期副作用和异步请求组织成可复用的组合式函数。对于管理后台表格、dataV 自适应等重复逻辑,useTable 和 useBorder 这类封装能降低重复代码量,也让逻辑边界更清晰。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-10 17:33 , Processed in 0.027638 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部