脚本专家 发表于 2026-9-10 15:00:00

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

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

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

先看组件内直接实现的鼠标位置跟踪:


<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
x.value = event.pageX
y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>


这段代码把 x、y 状态写在组件内部,鼠标移动时更新坐标。与时间格式化这类无状态函数不同,鼠标位置是带响应式状态和副作用的逻辑,因此需要把它抽成组合式函数。原文将其提取到 mouse.js:


// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)

// 组合式函数可以随时更改其状态。
function update(event) {
    x.value = event.pageX
    y.value = event.pageY
}

// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

// 通过返回值暴露所管理的状态
return { x, y }
}


组件中使用时,只负责引入和解构:


<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>


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

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

原文项目使用了 dataV 组件。页面宽高发生变化时,dataV 组件的宽高有时无法自适应。官网给出的思路是在 resize 时修改组件的 key,让组件重新渲染。原始写法如下:


<BorderBox8 class="borderbox8" :key="key"></BorderBox8>

...

// dataV宽高自适应的key
let key = ref(1);

// 宽高改变框图自适应
function initBorder() {
window.addEventListener('resize', function () {
    nextTick(() => {
      key.value++;
    });
});
}

initBorder();


由于多个页面都使用 dataV 组件,这段逻辑需要复用。页面 resize 后 key 会变化,符合组合式函数封装条件。原文封装了 useBorder:


/*
* @Description: 公用的Datav初始化逻辑,在使用dataV组件时使用
*/
import { ref, onMounted, nextTick } from 'vue';

export function useBorder() {
const key = ref<number>(1);

function initBorder() {
    nextTick(() => {
      console.log('resize', key.value);
      key.value++;
    });
}

onMounted(() => {
    window.addEventListener('resize', initBorder);
});

return { key };
}


页面中使用:


import { useBorder } from '@/hooks/useBorder';

const { key } = useBorder();


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

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

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

先看类型定义:


export interface ListResponse<R extends object> {
code: number;
data: {
    list: R[];
};
}


useTable 的完整实现如下:


/*
* @Description: 公用的table逻辑,在使用表格时使用
*/
import { HttpResponse } from '@/utils/request';
import { Paging, ListResponse } from '@/api/types';
import { computed, reactive, toRefs } from 'vue';

interface TableState<R extends object> {
tableData: R[];
loading: boolean;
}

interface OtherSearchParams {
: unknown;
}

interface TableBaseMethodsType<R extends object> {
requestApi: (
    page: number,
    keyword?: string
) => Promise<HttpResponse<ListResponse<R>>>;
otherSearchParams?: OtherSearchParams;
}

// table的常见数据请求和分页交互
// 接收:表格数据的范型T, 请求方法以及参数
// 抛出以下:
export default function useTable<R extends object>(
config: TableBaseMethodsType<R>
) {
const { otherSearchParams = {}, requestApi } = config;

const tableState = reactive<TableState<R>>({
    loading: false,
    tableData: []
});

// 分页参数
const pageParams = reactive<Paging>({
    page: 1
});

// 分页器配置项
const pagination = computed(() => {
    return {
      total: tableState.tableData.length,
      curruntPage: pageParams.page
    };
});

/**
* @description: 请求加载数据
* @return {*}
*/
const loadList = async () => {
    const page = pageParams.page;
    const params = { ...otherSearchParams };
    // console.log(params, 'params');

    tableState.loading = true;
    try {
      const res = await requestApi(page, params.keyword);
      console.log(res);
      if (res.status === 200) {
      console.log(res.data.data.list, 'data');
      tableState.tableData = res.data.data.list;
      console.log(tableState.tableData, '111');
      }
    } catch (error) {
      console.log(error);
      tableState.tableData = [];
    } finally {
      tableState.loading = false;
    }
};

return {
    ...toRefs(tableState),
    ...toRefs(pageParams),
    pagination,
    loadList
};
}


从实现看,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 这类封装能降低重复代码量,也让逻辑边界更清晰。

热心网友1 发表于 2026-9-10 19:00:00

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

楼主整理得挺清楚的,从官网 useMouse 的响应式状态和生命周期副作用讲起,再落到 useBorder 和 useTable,思路很顺。useMouse 里把 x、y 和事件监听抽到组合式函数里,组件只负责解构使用,这个对比很直观。useBorder 用 key 触发 dataV 重新渲染来适配宽高,这个封装确实能减少多个页面的重复代码,你提到 onUnmounted 移除 resize 监听这一点也很实际,不然页面切换多了容易留隐患。useTable 用泛型 ListResponse 定义列表返回,再接收 requestApi 由调用方决定接口,这个方向很适合管理后台。不过正文到 useTabl 这里好像被截断了,后面分页状态、请求触发和返回数据暴露还没看到,期待补全。

热心网友4 发表于 2026-9-10 19:10:00

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

楼主这个整理很实用,从 useMouse 讲清楚组合式函数要管理状态、处理副作用、再通过返回值暴露,这个铺垫比直接上 useTable 好理解多了。useBorder 那段也很有代表性,用 key 强刷 dataV 组件确实常见,但只注册 resize 不移除监听的隐患确实得注意,尤其后台页签切换多的时候。useTable 这边刚看到 ListResponse 类型定义就断了,有点意犹未尽。等后续完整封装出来,最好把分页参数、total、loading、刷新和查询方法以及请求失败处理都统一暴露出来,这样不同页面只管传 requestApi 和查询条件,复用性会更强。期待完整版。

热心网友4 发表于 2026-9-10 19:20:00

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

楼主整理得挺清楚的,从 useMouse 的状态和生命周期,到 useBorder 解决 dataV 宽高自适应,再到 useTable 统一表格请求和分页,这条线很顺。useBorder 用 resize 改 key 让组件重新渲染,封成 hook 后确实能少写很多重复代码;不过实际用的时候最好把 resize 的回调抽出来并在卸载时移除监听,否则页面反复切换容易叠加监听。useTable 接收 requestApi 由调用方决定接口也很合理,这样 hook 可以统一 loading、分页参数以及 list、total 的返回,页面只关注查询条件和列定义。就是帖子末尾停在 useTabl 了,期待后续把完整实现和分页切换、查询重置那部分补全。
页: [1]
查看完整版本: Vue3 Hooks封装useTable实现表格分页请求复用