Unleash 前端数据变更标准化:useAPI Hook 统一数据写入与错误处理实战指南
【免费下载链接】unleashOpen-source feature management platform项目地址: https://gitcode.com/GitHub_Trending/un/unleash
本文基于 Unleash 仓库中的架构决策记录(ADR)preferred-data-mutation-method.md 展开。作为开源核心(open-core)产品,Unleash 需要同时服务开源版与 SaaS 平台,前端数据变更(写入)路径的标准化是降低复杂度、统一错误处理的关键工程决策。读完本文,你将掌握 Unleash 前端"写请求"的标准范式:如何用
useAPI顶层 Hook 格式化请求、自动拼接 basePath、统一错误处理与 loading 状态,并能直接在真实组件中复刻这一套模式。
一、背景:为什么需要标准化的数据变更方法
Unleash 是开源核心(open-core)模式的功能开关(feature flag)管理平台。正如 ADR 中所言,这种商业模式带来了复杂的工程约束:SaaS 平台与开源产品有着不完全兼容的诉求——例如 SaaS 环境需要更严格的权限校验、更精细的错误上报、以及部署在子路径(subpath)下的场景支持。
这些差异如果放任不管,每个开发者写 API 调用时各自为政,就会出现大量重复且不一致的样板代码:
- 有的组件用
fetch直接裸调,缺少统一错误处理; - 有的组件忘记拼接部署子路径(basePath),在子路径部署环境下请求直接 404;
- 错误处理各写一套,错误信息格式五花八门,组件难以统一展示;
- loading 状态的管理方式混乱,难以做全局请求性能观测。
因此,ADR 的决策是:在前端实现一个顶层的useAPIHook,统一承担请求格式化、basePath 拼接、错误包装与一致性返回,从而标准化数据变更(mutation)与错误处理流程。该决策与姊妹篇 ADR preferred-data-fetching-method.md(采用 SWR 处理数据读取)互补,共同构成 Unleash 前端"读用 SWR、写用 useAPI"的数据层规范。
二、决策核心:顶层 useAPI Hook
ADR 明确了useAPI的职责边界:
- 格式化请求:统一组装请求头、凭证与 body;
- 拼接 basePath:当 Unleash 托管在子路径(如
/unleash)下时,自动为 API 路径加上前缀; - 包装错误处理器:按 HTTP 状态码分类处理错误,并可选择向上抛出(propagate);
- 一致性返回:统一返回
errors与loading状态,让调用方接口保持一致。
ADR 给出的规范示例是一个标准的"变更类" API Hook ——useTagTypesApi,完整代码如下(该示例在仓库源码 useTagTypesApi.ts 中有同构实现):
import { ITagPayload } from 'interfaces/tags'; import useAPI from '../useApi/useApi'; export const useTagTypesApi = () => { const { makeRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true, }); const createTag = async (payload: ITagPayload) => { const path = `api/admin/tag-types`; const req = createRequest(path, { method: 'POST', body: JSON.stringify(payload), }); try { const res = await makeRequest(req.caller, req.id); return res; } catch (e) { throw e; } }; const validateTagName = async (name: string) => { const path = `api/admin/tag-types/validate`; const req = createRequest(path, { method: 'POST', body: JSON.stringify({ name }), }); try { const res = await makeRequest(req.caller, req.id); return res; } catch (e) { throw e; } }; const updateTagType = async (tagName: string, payload: ITagPayload) => { const path = `api/admin/tag-types/${tagName}`; const req = createRequest(path, { method: 'PUT', body: JSON.stringify(payload), }); try { const res = await makeRequest(req.caller, req.id); return res; } catch (e) { throw e; } }; const deleteTagType = async (tagName: string) => { const path = `api/admin/tag-types/${tagName}`; const req = createRequest(path, { method: 'DELETE' }); try { const res = await makeRequest(req.caller, req.id); return res; } catch (e) { throw e; } }; return { createTag, validateTagName, updateTagType, deleteTagType, errors, loading, }; };这段代码展示了useAPI的标准用法骨架:
useAPI({ propagateErrors: true }):开启错误向上传播,调用方可以用try/catch感知失败;createRequest(path, options):把相对 API 路径与请求选项(method、body 等)封装为一个含caller与id的请求描述对象;makeRequest(req.caller, req.id):真正发起请求并统一处理响应;- 返回
errors与loading:供组件在表单提交或列表操作时展示错误与加载状态。
三、源码级解析:useAPI 的真实实现
ADR 描述的是设计意图,其落地实现在 frontend/src/hooks/api/actions/useApi/useApi.ts。从源码结构看,useAPI对外暴露五个核心成员:
| 成员 | 类型 | 职责 |
|---|---|---|
createRequest(path, options, requestId?) | 函数 | 拼接默认请求头、组装caller与id的请求描述对象 |
makeRequest(caller, requestId, loadingOn?) | 函数 | 发起请求、管理loading、按状态码分发错误处理 |
makeLightRequest(caller, requestId, loadingOn?) | 函数 | 轻量请求,失败仅抛出通用错误,不处理细分状态码 |
errors | Record<string, string> | 按错误类别累积的错误消息 |
loading | boolean | 当前是否有请求进行中 |
3.1 createRequest:请求的统一入口
const createRequest = useCallback( (path: string, options: any, requestId: string = '') => { const defaultOptions: RequestInit = { headers, credentials: 'include', }; return { caller: () => { return fetch(formatApiPath(path), { ...defaultOptions, ...options, }); }, id: requestId, }; }, [], );关键点有三:
- 统一请求头:
headers来自 frontend/src/utils/apiUtils.ts,固定为Accept: application/json与Content-Type: application/json; - 携带凭证:
credentials: 'include'确保跨域场景下带上 Cookie,配合后端的会话鉴权; - basePath 拼接:
formatApiPath(path)来自 frontend/src/utils/formatPath.ts,它会读取 HTML 中<meta name="baseUriPath">标签内容(开发模式下优先使用 Vite 的BASE_URL),将api/admin/...这类相对路径转换为/<basePath>/api/admin/...。这正是 ADR 强调的"如果 Unleash 托管在子路径下自动加 basePath"的实现细节。
3.2 makeRequest 与错误分发机制
makeRequest负责核心的状态与错误管理:
const makeRequest = useCallback( async (apiCaller, requestId, loadingOn = true): Promise<Response> => { if (loadingOn) { setLoading(true); } try { const res = await apiCaller(); setLoading(false); if (res.status > 299) { await handleResponses(res, requestId); } if (res.status === OK) { setErrors({}); } return res; } catch (e) { setLoading(false); throw e; } }, [handleResponses], );请求成功(200)时清空errors;状态码大于 299 时交给handleResponses按状态码分类处理。结合 frontend/src/constants/statusCodes.ts 中的常量,错误映射关系如下:
| HTTP 状态码 | 常量 | 默认错误文案(未指定自定义 handler 时) | propagateErrors 时抛出的错误类 |
|---|---|---|---|
| 400 | BAD_REQUEST | Bad request format | BadRequestError(apiUtils.ts) |
| 401 | UNAUTHORIZED | 访问被拒绝文案(ACCESS_DENIED_TEXT) | AuthenticationError |
| 403 | FORBIDDEN | This operation is forbidden | ForbiddenError |
| 404 | NOT_FOUND | Could not find the requested resource | NotFoundError |
| 409 | CONFLICT | Conflict | ConflictError |
| 503 | UNAVAILABLE | This operation is unavailable | UnavailableError |
| > 399 | — | 从响应details[0]或response[0]提取消息 | Error(消息取自服务端返回) |
3.3 自定义错误处理器与 propagateErrors
useAPI的入参IUseAPI支持两类配置:
propagateErrors?: boolean(默认false):为true时,遇到错误状态码会抛出对应异常类,让调用方try/catch接管;为false时只把错误消息写入errors状态,由组件自行读取展示;- 五个可选的错误处理器:
handleBadRequest、handleNotFound、handleUnauthorized、handleForbidden、handleUnavailable。传入后,对应状态码会走自定义处理函数(形如(setErrors, res, requestId) => void),否则使用默认文案。
设计上,errors使用对象键值(如badRequest、notFound、conflict、unknown)累积多个错误,组件可以按类别定位具体失败原因。
3.4 开发模式下的请求计时
源码中还实现了一个值得注意的细节:在NODE_ENV === 'development'时,makeRequest会被包装为makeRequestWithTimer(见 useApi.ts 中requestWithTimer),对每个请求记录耗时,超过 500ms 会在控制台输出[DEVELOPMENT LOG]警告,提示可能存在渲染性能问题。这意味着useAPI不仅是请求规范,也内置了请求性能观测能力。
四、实战对照:仓库中真实 Hook 的写法演进
值得对比的是:ADR 示例使用try { ... } catch (e) { throw e; }包裹makeRequest,而当前仓库的 useTagTypesApi.ts 已简化为直接return makeRequest(req.caller, req.id):
const createTag = async (payload: ITagPayload) => { const path = `api/admin/tag-types`; const req = createRequest(path, { method: 'POST', body: JSON.stringify(payload), }); return makeRequest(req.caller, req.id); };从源码结构可以推断,由于开启propagateErrors: true后makeRequest已统一负责错误抛出,外层再包一层无操作try/catch属于冗余代码,仓库实践倾向直接返回 Promise。这体现了 ADR 规范的演进方向:约定不变,实现逐步精简。
4.1 大型案例:useFeatureApi
功能开关(feature toggle)模块的 useFeatureApi.ts 是useAPI大规模使用的范本,覆盖了完整的方法类型:
- 校验类:
validateFeatureToggleName、validateConstraint,POST 到api/admin/features/validate、api/admin/constraints/validate; - 创建/更新类:
createFeatureToggle(POSTapi/admin/projects/${projectId}/features)、patchFeatureToggle(PATCH)、cloneFeatureToggle(POST clone 端点); - 开关切换类:
toggleFeatureEnvironmentOn/toggleFeatureEnvironmentOff,其中开关类操作特意使用makeLightRequest—— 因为开关切换是高频率、轻量级操作,失败只需抛出通用错误,无需细分状态码; - 批量操作类:
bulkToggleFeaturesEnvironmentOn/Off,POST 批量端点; - 删除/归档类:
archiveFeatureToggle(DELETE)。
值得注意的是其中的差异化选型:makeLightRequest用于高频轻量操作,makeRequest用于需要精细错误处理的业务操作。这是useAPI设计中最具实战价值的分层思路,可总结为:
- 需要向用户展示具体错误原因、需要处理冲突/权限等场景 → 用
makeRequest+propagateErrors; - 操作简单、失败统一提示、追求低开销 → 用
makeLightRequest。
此外,useFeatureApi中的 URL 编码细节也值得借鉴:删除 feature 标签时使用encodeURIComponent(type)与encodeURIComponent(value)编码路径参数(见 useFeatureApi.ts),避免特殊字符破坏路径。
五、与数据读取规范的分工:读 SWR,写 useAPI
useAPI只负责**变更(写)**路径。Unleash 前端的数据读取(读)路径由姊妹 ADR preferred-data-fetching-method.md 规范:移除 Redux,改用useSWR(stale-while-revalidate 缓存策略)。
两者分工清晰,构成完整的数据层闭环:
| 维度 | 读取(fetching) | 变更(mutation) |
|---|---|---|
| 规范工具 | useSWR(swr 库) | useAPI顶层 Hook |
| 典型位置 | frontend/src/hooks/api/getters | frontend/src/hooks/api/actions |
| 职责 | 缓存数据、自动重验证、mutate手动刷新 | 格式化请求、错误分类、loading 管理 |
| 错误处理 | 返回error状态供组件渲染 | 按状态码映射错误 + 可选向上抛出 |
实际操作中两者常常配合:先用useAPI完成一次变更(如创建 tag),再调用对应 getter 的refetch(内部mutate)刷新列表,保证界面与后端一致。
六、实践小结:编写"变更类" API Hook 的标准步骤
结合 ADR 规范与仓库源码,在 Unleash 前端新增一个数据变更 Hook 的推荐步骤是:
- 创建文件:在
frontend/src/hooks/api/actions/<Feature>Api/use<Feature>Api.ts下建立 Hook,命名遵循use<Resource>Api; - 初始化 useAPI:
const { makeRequest, makeLightRequest, createRequest, errors, loading } = useAPI({ propagateErrors: true }); - 定义每个操作:先写相对路径
api/admin/...(无需带 basePath,formatApiPath会自动拼接),用createRequest(path, { method, body })组装,再用makeRequest(或高频轻量操作用makeLightRequest)发起; - 返回统一接口:把各操作方法连同
errors、loading一起返回,供组件消费; - 在组件中消费:调用方法时
await即可,错误要么从errors读取展示,要么依赖propagateErrors用try/catch处理。
通过这套约定,Unleash 前端数百个 API 操作(frontend/src/hooks/api/actions 下按资源划分的 Hook 目录)保持了高度一致的调用形态与错误语义,这也是 ADR 作为团队工程契约的实际价值所在。
参考文档与源码索引
- 本 ADR 原文:preferred-data-mutation-method.md
- 姊妹 ADR(读取规范):preferred-data-fetching-method.md
useAPI核心实现:frontend/src/hooks/api/actions/useApi/useApi.ts- basePath 拼接实现:frontend/src/utils/formatPath.ts
- 错误类与默认请求头:frontend/src/utils/apiUtils.ts
- 状态码常量:frontend/src/constants/statusCodes.ts
- 规范示例的真实实现:frontend/src/hooks/api/actions/useTagTypesApi/useTagTypesApi.ts
- 大型实战案例:frontend/src/hooks/api/actions/useFeatureApi/useFeatureApi.ts
【免费下载链接】unleashOpen-source feature management platform项目地址: https://gitcode.com/GitHub_Trending/un/unleash
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考