# 统一请求库封装计划(src/utils/request.ts 版) ## Summary 目标:把统一请求库集中封装在 `src/utils/request.ts`,类型集中在 `src/shared/`,实现: - baseURL/timeout/取消请求(Axios Web) - 默认 header 注入:Authorization(Bearer)、clientId、X-Latitude、X-Longitude、language - 业务错误/网络错误/HTTP 错误的统一归一化(只抛错,不做 Toast/跳转) - 输出说明文档到 `docs/request.md` ## Current State Analysis(以实际仓库为准) 已确认的事实: - 依赖中已包含 `axios`,但 `src/` 内暂无 axios 的实际封装与使用。 - `src/utils/request.ts` 存在但为空文件,且命名疑似历史遗留。 - 代码中存在大量“缺失模块引用”(详见文末“历史遗留问题清单”),其中 `@/request/api/*` 属于最集中、最影响后续迁移的一类。 结论: - 本次统一请求库规划要做到“新增即可用、可渐进替换”,不依赖现有(缺失的)API 模块结构。 ## Assumptions & Decisions(你已确认) - 底层实现:Axios(Web) - baseURL:从 `import.meta.env` 的环境变量读取(本计划固定变量名为 `VITE_API_BASE_URL`) - header 值来源:全局上下文注入(内存态),由登录/定位/语言切换时写入 - Authorization:`Bearer `(token 为空则不带 Authorization) - 错误处理:请求库只做错误标准化并抛错,不做 Toast/跳转 - header 字段:`Authorization`、`clientId`、`X-Latitude`、`X-Longitude`、`language` ## 为什么不拆得更细(回答你的问题) 可以都放在 `src/utils/request.ts` 里完成,且更适合你现在“先落地一个可用的统一入口”的诉求: - 单文件落地成本低:少目录、少入口、少导出点,替换/回滚都更直接 - 不牺牲可维护性:在单文件里用“内部分区函数 + 类型从 shared 引入”的方式,同样能保持边界清晰 - 何时再拆分:当出现“单文件 > 300~500 行、多人同时改动频繁、需要独立单测/Mock”的情况,再把 errors/context/http 拆出去更合理 因此本计划采用:**实现集中在 request.ts,类型抽到 shared** 的折中方案。 ## Proposed Changes(全新规划) ### 1) 新增类型目录:`src/shared/` 新增目录:`src/shared/` 新增文件(建议): - `src/shared/request-types.ts` 包含的类型定义(稳定、可复用): - `export interface ApiResponse { code: number; message?: string; data: T }` - `export interface RequestOptions { signal?: AbortSignal; headers?: Record; skipAuth?: boolean }` - `export type RequestContext = { token: string | null; clientId: string | null; latitude: number | null; longitude: number | null; language: string | null }` - `export type NormalizedErrorKind = "business" | "http" | "network" | "unknown"` - `export interface NormalizedError extends Error { kind: NormalizedErrorKind; code?: number; httpStatus?: number; response?: unknown }` ### 2) 统一请求实现:`src/utils/request.ts` 新增文件:`src/utils/request.ts` 职责(全部在一个文件内完成): #### 2.1 上下文容器(内存态) 提供以下导出函数(由业务在合适时机调用): - `setAuthToken(token: string | null): void` - `setClientId(clientId: string | null): void` - `setLocation(latitude: number | null, longitude: number | null): void` - `setLanguage(language: string | null): void` - `getRequestContext(): RequestContext` 默认值策略: - `language`:如果未显式设置,尝试读取 `src/i18n/index.ts` 的 `getCurrentLocale()`(读取失败则不注入 language header) #### 2.2 Axios 实例创建与默认配置 - `baseURL`:`import.meta.env.VITE_API_BASE_URL` - 若缺失:直接抛出清晰错误(提示需要配置 baseURL) - `timeout`:可选 `import.meta.env.VITE_API_TIMEOUT_MS`,缺失则默认 15000 #### 2.3 请求拦截器(header 注入) 组装 headers 规则: - 合并顺序:`默认headers < 上下文headers < 调用方options.headers` - `Authorization`:仅当 token 存在时注入 `Bearer `;当 `options.skipAuth === true` 时跳过 - `clientId`:存在则注入 - `X-Latitude` / `X-Longitude`:存在则注入(数值转字符串) - `language`:存在则注入 #### 2.4 响应处理(业务码 + 错误归一化) 1. 正常响应: - 若响应体符合 `ApiResponse` 且 `code === 0`:返回整个 `ApiResponse` - 若响应体符合 `ApiResponse` 且 `code !== 0`:抛出 `NormalizedError(kind="business", code, message, response)` 2. 异常响应(axios error): - 有 `response.status`:抛 `NormalizedError(kind="http", httpStatus, response)` - 无 `response`:抛 `NormalizedError(kind="network")` 对外导出(建议最小集合): - `export async function request(config, options?: RequestOptions): Promise>` - 可选:`export async function requestData(...): Promise`(内部调用 request 并返回 `data`,用于未来更干净的调用风格) ### 3) 文档输出到 `docs/` 新增目录:`docs/` 新增文档:`docs/request.md`(必须包含): - 环境变量约定:`VITE_API_BASE_URL`、`VITE_API_TIMEOUT_MS`(可选) - 上下文写入时机: - 登录成功:`setAuthToken(token)` - 应用初始化:`setClientId(clientId)` - 定位成功:`setLocation(lat, lng)` - 语言切换:`setLanguage(locale)`(或依赖默认读取 i18n) - 使用方式: - 旧代码风格兼容:`const res = await request(); res.data...` - 新代码推荐:`const data = await requestData()` - 错误处理示例:区分 business/http/network/unknown 并给出上层建议策略 ## 历史遗留问题清单(独立梳理,方便逐个替换) 说明:以下均为“当前仓库扫描可证实”的问题点,按模块引用位置列出,便于你后续按优先级逐个替换/补齐。 ### A. `@/request/api/*` 悬空引用(13 个文件) - `src/pages/quick/index.vue` - `src/components/Feedback/index.vue` - `src/components/CreateServiceOrder/index.vue` - `src/pages/service/order/components/OrderCard/index.vue` - `src/pages/booking/components/FooterSection/index.vue` - `src/pages/goods/index.vue` - `src/pages/order/order/components/FooterSection/index.vue` - `src/pages/quick/components/Tabs/index.vue` - `src/pages/order/order/list.vue` - `src/pages/service/order/index.vue` - `src/pages/booking/index.vue` - `src/pages/login/index.vue` - `src/pages/order/order/detail.vue` 建议后续替换策略: 1. 先确定“这些 API 模块的真实来源”(是否在其它分支/其它仓库/尚未迁入)。 2. 如果确认不会迁回,则逐文件把 `@/request/api/*` 替换为基于 `src/utils/request.ts` 的真实调用(需要你提供具体 URL/参数约定)。 ### B. `@/utils` 目录级导入缺失(6 个文件) 当前仓库没有 `src/utils/index.*`,但以下文件在导入 `@/utils`: - `src/pages/quick/index.vue` - `src/pages/booking/components/FooterSection/index.vue` - `src/pages/goods/index.vue` - `src/pages/order/order/components/FooterSection/index.vue` - `src/pages/booking/index.vue` - `src/pages/home/index.vue` ### C. `@/store` 与实际目录 `src/stores/` 命名不一致(6 个文件) - `src/components/ImageSwiper/index.vue` - `src/pages/quick/components/Card/index.vue` - `src/pages/goods/album/index.vue` - `src/pages/goods/index.vue` - `src/pages/booking/index.vue` - `src/pages/home/index.vue` ### D. `@/hooks/*` 缺失(3 个文件) - `src/components/SwipeCards/index.vue` - `src/pages/login/index.vue` - `src/pages/home/index.vue` ### E. `@/constant/*` 缺失(6 个文件) - `src/components/ModuleTitle/index.vue` - `src/components/Feedback/index.vue` - `src/components/SurveyQuestionnaire/index.vue` - `src/components/CreateServiceOrder/index.vue` - `src/pages/booking/index.vue` - `src/pages/login/index.vue` ### F. `src/utils/request.ts`(空文件 + 疑似拼写问题) - 该文件当前为空,且命名疑似应为 `requests` 或 `request` - 建议后续统一策略:保留/替换/删除前先全仓搜索确认是否在其它分支被引用 ## Verification(执行阶段你手动确认运行) 你在实现完成后手动执行: - `yarn typecheck` - `yarn build` - 如有用例:`yarn test` 验收标准: - `src/utils/request.ts` + `src/shared/request-types.ts` 编译通过 - 发起请求时能注入 headers(Authorization/clientId/X-Latitude/X-Longitude/language) - `code !== 0` 时抛出稳定结构的错误对象(上层可根据 kind 做统一处理)