在现代前端开发中,通过封装 API 请求和利用自定义 Hook,可以有效提高代码的复用性和可维护性。本文将以实际业务代码为例,总结如何在 Vue 3 项目中编写接口请求逻辑以及使用 Hook 获取和处理数据。
一、封装业务接口
封装接口的目的是使代码模块化、清晰化,并便于复用。在示例中,使用了 @vueuse/integrations/useAxios
结合 Axios 实现接口请求封装。
核心代码
import { useAxios } from '@vueuse/integrations/useAxios';
import { RequestParams } from '@/store/dashboard-store-creator/typing';
import { ResponseType } from '@/types/http';
import ApiUrl from '@/utils/apiUrl';
import http from '@/utils/http';
export function fetchChartData<RS = any, RP = any>() {
const request = useAxios<ResponseType<RS>, RequestParams<RP>>(
'',
{
method: 'POST',
},
http,
{ immediate: false },
);
return {
...request,
execute(params: RequestParams<RP>) {
return request.execute(`${ApiUrl.Dashboard.getChartData}/${params.chartId}`, { data: params });
},
};
}
实现步骤
-
依赖引入:
- 使用
@vueuse/integrations/useAxios
提供的 Hook,结合 Axios 实现请求封装。 - 将请求参数和返回值类型化,便于后续使用时获得更好的类型提示。
- 使用
-
动态接口调用:
- 将接口地址与动态参数绑定,支持灵活传递
chartId
等动态路径。
- 将接口地址与动态参数绑定,支持灵活传递
-
优化请求配置:
immediate: false
确保接口不会在 Hook 初始化时自动触发请求,便于在需要时调用。
-
统一封装响应格式:
- 使用
ResponseType
类型统一接口响应的数据结构。
- 使用
二、编写 Hook 实现业务逻辑
通过自定义 Hook 封装数据获取逻辑,将请求接口与视图层解耦,提升组件复用性。
核心代码
import { getFilterApiParams } from '@/components/BinSelectFilter/utils';
import { fetchChartData } from '@/service/dashboard-chart';
import { useOverallYieldStore } from '../store';
const store = useOverallYieldStore();
export const useBinTableData = () => {
const { isLoading, execute, data } = fetchChartData();
watchDebounced(
[
() => store.binTab.binSelect.binDefinition,
() => store.binTab.binSelect.binFilter,
() => store.binTab.binSelect.binType,
store.binTab.binSelect.appliedBins,
() => store.binTab.binSelect.appliedBins,
store.filters,
],
() => {
const { binFilter, binDefinition, binType } = store.binTab.binSelect;
const binFilterParams = getFilterApiParams(binFilter);
const dataSource = 'TEST_RAW_DATA';
execute({
chartId: 'overallBinPareto',
fields: [],
metrics: [],
filters: store.globalFilters,
dataSource,
setting: {
dataSource,
binType,
binDefinition,
needFillNoDataFieldData: true,
selectedBins: store.binTab.binSelect.appliedBins ?? [],
...binFilterParams,
binSortType: 'BIN_RATE',
binSort: 'DESC',
},
});
},
{
debounce: 100,
immediate: true,
},
);
// 同步数据到 store
watch(data, (res) => {
store.binTab.binTable.data = res?.data ?? [];
});
return {
isLoading,
};
};
实现步骤
-
初始化接口请求:
- 调用
fetchChartData
接口 Hook,获取数据加载状态、执行方法execute
和响应数据data
。
- 调用
-
监听数据变化:
- 使用
watchDebounced
监听多个 store 中的数据源(如binFilter
和binType
),在数据变化时触发execute
请求。
- 使用
-
请求参数动态化:
- 动态生成接口所需的复杂参数(如
binFilterParams
和selectedBins
),确保接口的灵活性。
- 动态生成接口所需的复杂参数(如
-
防抖和立即执行:
- 在监听逻辑中引入
debounce
参数以减少频繁的接口调用,同时设置immediate: true
确保初次加载时立即执行。
- 在监听逻辑中引入
-
结果处理:
- 使用
watch
监听接口返回数据,并将结果同步到 Store 中供组件使用。
- 使用
三、最佳实践总结
1. 接口封装
- 抽象公共逻辑:对 Axios 请求的初始化和响应格式进行统一封装。
- 类型安全:在参数和返回值中使用 TypeScript,增强代码可靠性。
- 灵活性:支持动态路径和参数组合。
2. 使用 Hook
- 解耦逻辑与视图:将数据获取和逻辑处理封装到自定义 Hook 中,保持组件轻量化。
- 动态监听:结合
watch
或watchDebounced
动态监听状态变化,并根据变化触发数据请求。 - 状态同步:及时将请求结果同步到全局状态管理(如 Pinia)中。
3. 性能优化
- 防抖优化:通过
debounce
减少频繁请求。 - 懒加载接口:设置
immediate: false
避免 Hook 初始化时多余的接口调用。
四、完整使用场景
通过上述封装,业务组件中可以直接调用 useBinTableData
Hook 获取所需状态:
const { isLoading } = useBinTableData();
在模板中:
<template>
<LoadingSpinner v-if="isLoading" />
<BinTable v-else :data="store.binTab.binTable.data" />
</template>
通过这种方式,开发者可以专注于视图层逻辑,数据获取与处理完全由 Hook 和封装接口负责,代码更加清晰简洁。
PS: 看情况和需要,也可以不使用store
,而是把data
return出去在业务组件里使用。
这套接口与 Hook 的实践既适合中小型项目的快速开发,也能满足大型项目的复杂需求,值得在 Vue 3 项目中推广。
五、接口竞态覆盖问题解决方案
思路大概是,在接口的某个合适的上下文中维护一个abortControllers
Map,每次发起新的请求时检查一下这个 Map 中有没有对应的旧请求,如果有则调用abort
方法取消旧的请求(需要将AbortController
的信号传递给Axios
的请求配置中才能取消Axios
的请求),每次请求成功响应后在 Map 中清空对应的请求记录。
对「一、」中代码改造举例:
import { useAxios } from '@vueuse/integrations/useAxios';
import { RequestParams } from '@/store/dashboard-store-creator/typing';
import { ResponseType } from '@/types/http';
import ApiUrl from '@/utils/apiUrl';
import http from '@/utils/http';
export function fetchChartData<RS = any, RP = any>(abortPrevious: boolean = true) {
// * [src/utils/dashboard/dashboard-context/provider.ts] 为每个有独立 meta 的图表创建了新的 Chart 上下文
// * 只有同一个 meta 的图表共用一个 request, 会触发旧调用废弃逻辑, 所以默认开启 abort 逻辑没有问题
const abortControllers = new Map<string, AbortController>(); // 存储每个 chartId 对应的 AbortController
const request = useAxios<ResponseType<RS>, RequestParams<RP>>(
'',
{
method: 'POST',
},
http,
{ immediate: false },
);
return {
...request,
execute(params: RequestParams<RP>) {
const { chartId } = params;
// 如果开启 abortPrevious 且 chartId 相同,取消之前的请求
if (abortPrevious && abortControllers.has(chartId)) {
const previousController = abortControllers.get(chartId);
if (previousController) {
previousController.abort(); // 取消旧请求
}
}
// 创建新的 AbortController
const controller = new AbortController();
abortControllers.set(chartId, controller); // 存储新的 AbortController
// 发起请求
return request
.execute(`${ApiUrl.Dashboard.getChartData}/${chartId}`, {
data: params,
signal: controller.signal, // 将 AbortController 的信号传递给请求
})
.then(() => {
// 请求完成后,清理 AbortController
if (abortControllers.get(chartId) === controller) {
abortControllers.delete(chartId);
}
}).catch(() => {
// 请求完成后,清理 AbortController
if (abortControllers.get(chartId) === controller) {
abortControllers.delete(chartId);
}
});
},
};
}
Comments NOTHING