摘要:前端请求库的演进正从 Promise 范式走向策略范式。本文以 alova 为例,通过分页列表、表单提交、轮询三个典型场景的 Before/After 代码对比,分析策略化设计如何将"过程式编程"转变为"意图式编程",并讨论其适用边界与局限性。
引言
前端请求库的演进,本质上是对"数据获取"这一高频场景的不断抽象。
从 XMLHttpRequest 的回调时代,到 fetch / axios 的 Promise 范式,再到 React Query、SWR 的声明式 Hooks,每一次进化都在降低样板代码的复杂度。
目前有一个值得关注的方向:将请求模式进一步抽象为"策略"------不再让开发者手动管理 loading、error、分页状态、轮询定时器等样板逻辑,而是通过语义化的 Hook 直接声明意图。
本文以 alova 为例,分析从 Promise 范式到策略范式这一进化路径的设计思路和应用边界。
Promise 时代:一个场景看样板代码
以分页列表为例------这是前端最常见的场景之一。传统 Promise 方案的代码结构大致如下:
jsx
// 传统 Promise(axios)方案
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
function TodoList() {
const [list, setList] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
const pageSize = 10;
const [total, setTotal] = useState(0);
const fetchList = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await axios.get('/api/todos', {
params: { page, pageSize }
});
setList(res.data.list);
setTotal(res.data.total);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
}, [page]);
useEffect(() => { fetchList(); }, [fetchList]);
return (
<div>
{loading && <Spinner />}
{error && <ErrorMsg message={error} />}
{list.map(item => <Item key={item.id} {...item} />)}
<Pagination
current={page}
total={total}
pageSize={pageSize}
onChange={p => setPage(p)}
/>
</div>
);
}
这段代码本身不复杂,但有几个共性问题:
- 状态管理重复:loading、error、data 几乎是每个请求都要声明一遍的"三件套"
- 请求依赖声明不够直观:useCallback 依赖数组 + useEffect 的搭配,本意是"page 变化时重新请求",但实现上需要三层嵌套
- 刷新触发分散:当需要外部触发刷新时(比如新增数据后),需要额外通过状态提升或 ref 来实现
- 翻页状态外露:page、pageSize、total 每个都需要手动维护
策略化思路:从"怎么做"到"要什么"
Promise 方案的本质是过程式编程:你需要告诉程序"怎么做"------什么时候发请求、怎么管理加载状态、怎么处理错误、怎么刷新。
策略化的思路反过来:你只需声明"我要什么"------我需要一个分页列表、我需要一个表单提交、我需要一个轮询------库负责执行策略的具体实现。
alova 为这一思路提供了一套面向常见场景的 Hook:
| 策略 Hook | 对应场景 |
|---|---|
usePagination |
分页列表 / 无限滚动 |
useForm |
表单提交(支持草稿、多步表单) |
useAutoRequest |
轮询 / 焦点刷新 / 重连刷新 |
useCaptcha |
验证码发送 + 倒计时 |
useUploader |
文件上传(支持进度、并发控制) |
useRetriableRequest |
指数退避重试 |
三个场景的 Before / After
场景一:分页列表
jsx
// alova usePagination
import { usePagination } from 'alova/client';
function TodoList() {
const {
data,
loading,
error,
page,
pageSize,
total,
send
} = usePagination(
(page, pageSize) => alovaInstance.Get('/api/todos', {
params: { page, pageSize }
})
);
// loading / error / data / page / pageSize / total 由 Hook 统一管理
return (
<div>
{loading && <Spinner />}
{error && <ErrorMsg message={error.message} />}
{data.map(item => <Item key={item.id} {...item} />)}
<Pagination
current={page}
total={total}
pageSize={pageSize}
onChange={p => send(p)}
/>
</div>
);
}
相比 Promise 版本:
- 不再需要 useState 管理 5 个状态(loading / error / data / page / total)
- 不再需要 useCallback + useEffect 声明请求依赖
send(p)替代setPage(p),语义更明确- 内置了翻页状态、预加载和缓存失效逻辑
场景二:表单提交
jsx
// 传统 Promise 方式
function LoginForm() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
try {
await axios.post('/api/login', formData);
router.push('/dashboard');
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{error && <Alert type="error">{error}</Alert>}
<button disabled={loading}>登录</button>
</form>
);
}
jsx
// alova useForm
function LoginForm() {
const { loading, error, send } = useForm(
(formData) => alovaInstance.Post('/api/login', formData),
{ resetAfterSubmitting: true }
);
return (
<form onSubmit={e => send(e)}>
{error && <Alert type="error">{error.message}</Alert>}
<button disabled={loading}>登录</button>
</form>
);
}
useForm 将 loading / error 管理和 try-catch-final 流程内聚到 Hook 内部。额外的能力如草稿持久化、多步表单状态共享也可以通过配置开启,无需额外代码。
场景三:轮询
jsx
// 传统方式:useEffect + setInterval
useEffect(() => {
const fetchStatus = async () => {
const { data } = await axios.get('/api/status');
setStatus(data);
};
fetchStatus();
const timer = setInterval(fetchStatus, 3000);
return () => clearInterval(timer);
}, []);
// alova useAutoRequest
useAutoRequest(
() => alovaInstance.Get('/api/status'),
{
pollingInterval: 3000,
enablePolling: true
}
);
useAutoRequest 除了轮询,还内置了页面焦点变化自动刷新、重连后刷新等功能,这些在 Promise 方案下需要额外的 visibilitychange 和网络事件监听来实现。
策略化的三个技术特征
1. 关注点分离
策略 Hook 将"请求的执行逻辑"与"业务组件"解耦。usePagination 内部处理翻页状态、数据聚合、缓存策略,组件只关心数据和渲染。这种分层让组件的单测和逻辑的单测可以独立进行。
2. 框架无关
alova 通过 StatesHook 适配层支持 React、Vue、Svelte、uniapp 等框架。同一套 usePagination / useForm API 在不同框架中接口一致,降低跨技术栈的认知成本。
3. 可组合性
策略之间可以组合使用。例如,usePagination 的数据可以通过 actionDelegationMiddleware 在其他组件中触发刷新,useForm 提交成功后可以调用 useFetcher 预加载下一页数据。
Trade-off 分析
策略化不是银弹。任何抽象都是一把双刃剑,带来的便利性背后,也有一些需要权衡的方面。
适用场景
- 标准 CRUD 页面:列表、表单、详情等模式固定的场景,策略 Hook 可以直接覆盖大部分数据获取需求
- 中大型项目:页面多、请求模式复杂、团队协作频繁的项目,统一的策略 API 有助于保持代码一致性,降低 review 成本
- 多平台项目:需要同时支持 Web、小程序、移动端的项目,框架无关的策略层可以减少适配工作量
- 需要缓存策略的场景:涉及 L1(内存)+ L2(持久化)缓存、自动失效、请求去重等复杂需求的场景,策略层内置的缓存管理可以减少手动实现的错误概率
局限 / 不适用场景
- 极简单的页面:只有一个 GET 请求的独立页面,引入策略层的抽象成本可能高于收益;直接使用 fetch 或 axios 更轻量
- 高度定制化的请求流程:如果业务的请求模式与内置策略差异很大,自定义实现可能更灵活(alova 支持自定义策略 Hook,但需要额外开发)
- 已有成熟方案的项目:如果项目已基于 React Query 或 TanStack Query 深度集成且运行稳定,整体迁移的 ROI 不高;渐进式引入新模块时尝试策略 Hook 是更务实的做法
- 需要直接操作底层 HTTP 的场景:如 WebSocket 连接管理、流式上传、Server-Sent Events 等场景,可能需要混合使用底层 API 和策略 Hook
- 团队不熟悉策略化模式:策略化的概念需要团队理解和接受,对于习惯传统 Promise 写法的团队有一定的学习曲线
总结
从 Promise 到策略化,本质上是前端数据获取从"过程式编程"到"意图式编程"的一次范式转移。它不替代 Promise,而是在 Promise 之上提供了面向常见场景的语义化抽象。
这种进化路径与状态管理领域中 Redux → Recoil / Zustand 的演进有相似之处------不是"哪一种更好",而是"在不同场景下哪种抽象层级更合适"。
对于大多数业务开发场景来说,用几行代码声明意图,替代几十行样板代码来实现它,这种抽象是有实际价值的。但在简单场景下,过度抽象同样会带来不必要的复杂度。选择何种方案,取决于项目的规模、团队的熟悉程度和具体的业务需求。
标签:alova、前端请求库、请求策略、Promise、数据获取
作者简介:前端工程师,专注于前端工程化和数据层架构设计,关注请求库、状态管理和跨平台开发方向。