Vue3 组合式 API 的实战技巧 —— 组合式 API 帮我解决了不少 Options API 难以应对的问题,尤其是在代码复用和复杂组件维护上。
一、为什么放弃 Options API?聊聊三年项目里的真实痛点
刚接触 Vue3 时,我曾因 “惯性” 继续用 Options API 写业务,但随着项目复杂度提升,两个痛点越来越明显:
- 代码碎片化:一个数据请求相关的逻辑(加载态、数据处理、错误提示),要分散在data、methods、mounted里,后期维护时需在多个选项间跳转,尤其复杂表单组件,逻辑溯源成本极高;
- 复用性差:比如多个组件都需要 “分页加载列表” 功能,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 个避坑技巧(三年经验总结)
- 避免在 setup 中直接修改 props:Vue3 中 props 仍是单向数据流,若需修改 props,可通过emit通知父组件,或用toRef创建 props 的响应式引用(如const name = toRef(props, 'name'));
- watch 监听对象时要加 deep:若监听的是对象 / 数组,需开启deep: true才能监听到内部属性变化(如watch(user, () => {}, { deep: true })),但尽量避免监听整个对象,可直接监听具体属性(如watch(() => user.name, () => {})),性能更好;
- Hook 命名规范统一:自定义 Hook 建议以use开头(如useRequest、usePagination),方便团队识别和维护,避免出现getPagination、paginationUtil这类不统一的命名。
四、总结与交流
以上就是我基于 Vue3+JS 栈的实战分享 —— 从项目痛点出发,用组合式 API 封装通用 Hook,既能减少重复代码,又能提升项目可维护性。这也是我三年前端开发中,从 “写功能” 到 “写优雅的功能” 的一个重要转变。