Vue3+JS 组合式 API 实战:从项目痛点到通用 Hook 封装

发布于:2025-09-13 ⋅ 阅读:(21) ⋅ 点赞:(0)

Vue3 组合式 API 的实战技巧 —— 组合式 API 帮我解决了不少 Options API 难以应对的问题,尤其是在代码复用和复杂组件维护上。

一、为什么放弃 Options API?聊聊三年项目里的真实痛点​

刚接触 Vue3 时,我曾因 “惯性” 继续用 Options API 写业务,但随着项目复杂度提升,两个痛点越来越明显:​

  1. 代码碎片化:一个数据请求相关的逻辑(加载态、数据处理、错误提示),要分散在data、methods、mounted里,后期维护时需在多个选项间跳转,尤其复杂表单组件,逻辑溯源成本极高;​
  2. 复用性差:比如多个组件都需要 “分页加载列表” 功能,Options API 只能通过mixins实现,但mixins存在命名冲突、逻辑来源不清晰的问题(比如同事接手项目时,不知道某个变量是来自组件本身还是mixins)。​

直到用组合式 API 重构了第一个列表页,才真正体会到 “逻辑聚合” 的爽 —— 所有和 “列表请求” 相关的代码都放在一起,复用只需复制一个函数,这也是我今天想重点分享的核心:用组合式 API 封装通用 Hook,解决重复造轮子问题。​

二、实战:封装 2 个 Vue3+JS 通用 Hook(附完整代码)​

下面结合我项目中高频使用的场景,分享两个通用 Hook 的封装思路,所有代码均基于 Vue3+JS 编写,可直接复制到项目中使用。​

1. useRequest:统一处理请求状态(加载 / 成功 / 失败)​

几乎所有业务组件都需要请求接口,而 “加载态显示”“错误提示”“请求取消” 是通用需求。之前每个组件都要写loading: false、error: '',重复代码占比 30% 以上,用useRequest可完全统一。​

代码实现:

import { ref, onUnmounted } from 'vue';
import { ElMessage } from 'element-plus'; // 假设项目用Element Plus,可替换为其他UI库

/**
 * 通用请求Hook
 * @param {Function} requestFn - 接口请求函数(需返回Promise)
 * @param {Object} options - 配置项(可选)
 * @param {boolean} options.autoRun - 是否默认执行请求(默认true)
 * @param {Function} options.onSuccess - 请求成功回调
 * @param {Function} options.onError - 请求失败回调
 */
export function useRequest(requestFn, options = {}) {
  const { autoRun = true, onSuccess, onError } = options;
  const loading = ref(false); // 加载态
  const data = ref(null); // 请求结果
  const error = ref(''); // 错误信息
  const controller = new AbortController(); // 用于取消请求

  // 核心请求函数
  const fetchData = async (...args) => {
    loading.value = true;
    error.value = '';
    try {
      // 传递signal,支持取消请求
      const result = await requestFn(...args, { signal: controller.signal });
      data.value = result;
      onSuccess && onSuccess(result); // 自定义成功回调
    } catch (err) {
      // 排除手动取消请求的错误
      if (err.name !== 'AbortError') {
        error.value = err.message || '请求失败,请重试';
        ElMessage.error(error.value); // 全局错误提示
        onError && onError(err); // 自定义失败回调
      }
    } finally {
      loading.value = false;
    }
  };

  // 自动执行请求(默认开启)
  if (autoRun) {
    fetchData();
  }

  // 组件卸载时取消请求,避免内存泄漏
  onUnmounted(() => {
    controller.abort();
  });

  return {
    loading,
    data,
    error,
    fetchData, // 手动触发请求(比如刷新按钮)
    cancelRequest: () => controller.abort() // 手动取消请求
  };
}

使用示例(用户列表组件):

import { useRequest } from '@/hooks/useRequest';
import { getUserList } from '@/api/user'; // 接口函数

export default {
  setup() {
    // 1. 初始化请求Hook,传入接口函数和配置
    const { loading, data: userList, fetchData } = useRequest(getUserList, {
      onSuccess: (res) => {
        console.log('用户列表请求成功', res);
      }
    });

    // 2. 手动刷新(比如搜索按钮点击)
    const handleRefresh = (searchParams) => {
      fetchData(searchParams); // 传递参数给接口函数
    };

    return {
      loading,
      userList,
      handleRefresh
    };
  }
};
2. usePagination:快速实现分页功能​

管理后台中 “分页列表” 是高频场景,页码切换、每页条数变更、总数计算这些逻辑完全可以复用。usePagination可结合上面的useRequest,快速搭建分页功能。​

代码实现:

import { ref, computed } from 'vue';

/**
 * 通用分页Hook
 * @param {Function} fetchFn - 列表请求函数(需接收page、pageSize参数)
 * @param {Object} defaultParams - 默认分页参数(可选)
 */
export function usePagination(fetchFn, defaultParams = {}) {
  // 分页基础参数
  const page = ref(defaultParams.page || 1); // 当前页码
  const pageSize = ref(defaultParams.pageSize || 10); // 每页条数
  const total = ref(0); // 总条数

  // 计算总页数
  const totalPage = computed(() => Math.ceil(total.value / pageSize.value) || 1);

  // 页码切换事件
  const handlePageChange = (newPage) => {
    page.value = newPage;
    fetchFn({ page: newPage, pageSize: pageSize.value }); // 触发请求
  };

  // 每页条数变更事件
  const handlePageSizeChange = (newSize) => {
    pageSize.value = newSize;
    page.value = 1; // 重置为第一页
    fetchFn({ page: 1, pageSize: newSize }); // 触发请求
  };

  // 重置分页参数(比如搜索时)
  const resetPagination = () => {
    page.value = 1;
    pageSize.value = defaultParams.pageSize || 10;
  };

  return {
    page,
    pageSize,
    total,
    totalPage,
    handlePageChange,
    handlePageSizeChange,
    resetPagination,
    // 分页参数对象(方便传递给请求函数)
    paginationParams: computed(() => ({
      page: page.value,
      pageSize: pageSize.value
    }))
  };
}

结合 useRequest 使用示例:

import { useRequest } from '@/hooks/useRequest';
import { usePagination } from '@/hooks/usePagination';
import { getUserList } from '@/api/user';

export default {
  setup() {
    // 1. 初始化分页Hook,定义请求函数(接收分页参数)
    const { paginationParams, total, handlePageChange, handlePageSizeChange } = usePagination(
      async (params) => {
        // 调用useRequest的fetchData,传递分页参数
        await fetchData({ ...params, ...searchParams.value });
      }
    );

    // 2. 初始化请求Hook,请求时带上分页参数
    const searchParams = ref({}); // 搜索参数(可选)
    const { loading, data: userList, fetchData } = useRequest(
      async (params = {}) => {
        const res = await getUserList({ ...paginationParams.value, ...params });
        total.value = res.total; // 更新总条数
        return res.list;
      }
    );

    // 3. 搜索功能(重置分页)
    const handleSearch = (params) => {
      searchParams.value = params;
      handlePageChange(1); // 搜索时跳转到第一页
    };

    return {
      loading,
      userList,
      page: paginationParams.page,
      pageSize: paginationParams.pageSize,
      total,
      handlePageChange,
      handlePageSizeChange,
      handleSearch
    };
  }
};

三、Vue3+JS 开发的 3 个避坑技巧(三年经验总结)​

  1. 避免在 setup 中直接修改 props:Vue3 中 props 仍是单向数据流,若需修改 props,可通过emit通知父组件,或用toRef创建 props 的响应式引用(如const name = toRef(props, 'name'));​
  2. watch 监听对象时要加 deep:若监听的是对象 / 数组,需开启deep: true才能监听到内部属性变化(如watch(user, () => {}, { deep: true })),但尽量避免监听整个对象,可直接监听具体属性(如watch(() => user.name, () => {})),性能更好;​
  3. Hook 命名规范统一:自定义 Hook 建议以use开头(如useRequest、usePagination),方便团队识别和维护,避免出现getPagination、paginationUtil这类不统一的命名。​

四、总结与交流​

以上就是我基于 Vue3+JS 栈的实战分享 —— 从项目痛点出发,用组合式 API 封装通用 Hook,既能减少重复代码,又能提升项目可维护性。这也是我三年前端开发中,从 “写功能” 到 “写优雅的功能” 的一个重要转变。​


网站公告

今日签到

点亮在社区的每一天
去签到