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 {
- [key: string]: 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 这类封装能降低重复代码量,也让逻辑边界更清晰。 |