ky+TanStackQuery请求管理
ky+TanStackQuery请求管理
选型原因
做了这么多项目后,简单的发送请求对我并不是难事,我想要探索一些架构设计的内容,即做一个可维护、可扩展、工程化的数据请求与管理体系。ky基于fetch,不像axios那样依赖XHR,浏览器兼容性更强,也更轻,并且内建了并发控制,这就使得我不需要再手动维护loading或error的状态,也不用关心请求结束后的事情。TanStack Query数据层会负责管理请求得来的数据,自动去重、并发合并、重试等。
软件工程讲究SRP(单一职责原则),使用ky+TanStackQuery可以把请求本身和服务端数据生命周期管理分离,实现进一步解耦。当然,对于demo项目没太大必要,这次试验也仅作娱乐。
数据流
考虑一个最简单的表单提交业务,按下提交按钮后调用后端接口,把表单数据存进数据库表。传统的黑马写法是(axios):
const handleSubmit = (values) => {
axios.post('/api/menu/add', values).then((data) => {
// 后续处理
});
};这种写法完全点对点,直接请求后端接口,也直接通过then拿到返回值。现在改用ky,并且加入一个数据管理层。首先,根据TS的精神,先给请求表单数据value一个单独的类型(最好不要直接用表单本身的数据类型,那样不便于扩展):
export interface CreateMenuDTO {
menuname: string;
linkUrl: string;
openType: 'New window' | 'Recent window';
icon: string;
adminOnly: boolean;
parentMenu?: string;
}之后,需要定义mutation hook。在TanStack Query中,Query用于获取数据,可缓存也可共享;而mutation用来修改数据,不可缓存。这里既然要加数据,属于会改变服务器状态的操作,用mutation实现。对应增删改,总共需要三个mutation hook,先写出增加的:
export const MENU_QUERY_KEY = 'menus';
export const useCreateMenu = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateMenuDTO) => menuService.createMenu(data),
onSuccess: () => {
// 成功后使菜单列表缓存失效,触发重新获取
queryClient.invalidateQueries({ queryKey: [MENU_QUERY_KEY] });
},
});
};useMutation是 TanStack Query 提供的 Hook,用于执行异步副作用操作,自动管理异步状态,并返回一个 可触发的操作对象。queryClient是全局 Query 缓存的控制器,用于在数据被修改后,通知相关 Query:你们的数据已经过期了。MENU_QUERY_KEY这是菜单列表 Query 的统一标识,保证 Query / Mutation 使用同一套 key,Query Key 是 TanStack Query 中“缓存定位”的唯一依据。
返回的mutationFn是这个hook的主要功能,传入表单数据,返回一个Promise。这里通过 menuService 将 HTTP 细节(ky / fetch / axios)完全隔离,使其只关心业务行为,而不关心请求具体是怎么发的,在这一层看来,“调用 createMenu,就能创建一个菜单”。
当创建成功后,会执行onSuccess,将 menus 这个 Query 标记为 “已过期”,所有使用该 Query 的组件会自动重新获取数据,这样就保证了数据的一致性。
接下来就是menuService.createMenu(service层)的实现:
export const menuService = {
createMenu: async (data: CreateMenuDTO): Promise<Menu> => {
const response = await request.post('menus', { json: data }).json<{ menu: Menu }>();
return response.menu;
},
// ...
};request.post('menus', { json: data })会使用ky请求实例发送json格式的请求,.json<{ menu: Menu }>()处理的是响应数据,把它解析为json,放进menu字段并指定返回类型(为后续代码提供类型推导依据)。最后返回刚才拿到的menu字段,即响应数据。可以看出它也没有实现具体的请求发送,而是调用了一个ky实例request。最后一步就是实现这个单一实例,后续任何发送请求的场景都只需使用这个request:
export const request = ky.create({
prefixUrl: 'https://backstage-management.fuufhjn.link/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
hooks: {
beforeRequest: [
(request) => {
// 可以在这里添加认证 token 等
console.log('Request:', request.url);
},
],
afterResponse: [
async (request, options, response) => {
// 可以在这里统一处理响应
if (!response.ok) {
console.error('Response error:', response.status);
}
return response;
},
],
},
});这是文档中建议的写法,hooks中的东西类似axios的拦截器,区别是他不依赖XHR,更轻,但是功能上不如axios,不能中断请求改写数据(劫持),必须显式处理网络行为。
现在必要的工具已经全部集齐,新版的提交按钮回调是:
const createMenuMutation = useCreateMenu();
const handleSubmit = async (values: CreateMenuDTO) => {
try {
await createMenuMutation.mutateAsync(values);
// 成功后关闭模态框并重置表单
close();
form.reset();
} catch (error) {
console.error('Failed to create menu:', error);
// 可以在这里添加错误提示
}
};它其实只调用了刚才的mutation hook,即发出了一个“我要添加数据”的指令,其他的啥也不用管,后期扩展时会比黑马版本要舒服很多(未必)。
