# react-ts-axios-request **Repository Path**: nsdd/react-ts-axios-request ## Basic Information - **Project Name**: react-ts-axios-request - **Description**: react-ts-axios-request - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-04-25 - **Last Updated**: 2025-04-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 更新功能 1. requestKey 生成优化 - 添加了 sortObject 函数,递归对 params 和 data 对象进行排序,确保属性顺序一致。 - 在 request 中更新 requestKey 生成逻辑,使用 sortObject 处理 config.params 和 config.data。 2. 缓存清理性能优化 - 引入 lru-cache 替换原有的 Map,设置 max: 100(可根据需求调整)和 ttl: 5 分钟。 - 移除 setInterval 清理逻辑,lru-cache 自动管理过期和上限。 - 保持 generateCacheKey 和 isCached 的接口不变,兼容现有调用。 3. Token 刷新并发控制优化 - 使用 Promise.race 结合 setTimeout 添加刷新超时(5 秒),如果超时则拒绝 Promise。 - 在 finally 中清空 failedQueue,避免内存泄漏。 - 添加 console.error 日志,便于调试刷新失败的原因。 4. 错误提示优化 - 引入 messageQueue 数组,存储待显示的提示(内容、类型、时间戳)。 - 实现 enqueueMessage 函数,支持去重(5 秒内相同内容只显示一次)和队列管理(最多缓存 6 条,显示 3 条)。 - showNextMessage 按顺序显示提示,回调中移除已显示的条目。 - 更新 showErrorMessage 和 showSuccessMessage,使用 enqueueMessage 替代直接调用 antdMessage。 ## axios 请求功能实现 `request.ts` 是一个封装了 HTTP 请求的核心模块,基于 `axios` 构建,集成了多种功能以提升开发效率和用户体验。以下是对其功能的详细总结: --- ### 1. **基础 HTTP 请求** - **功能**: - 提供了 `get`、`post`、`put`、`delete` 四种常用 HTTP 方法,用于发送请求。 - 基于 `axios` 构建,支持异步请求,返回 `Promise`。 - 配置了默认 `baseURL`(`https://jsonplaceholder.typicode.com`)和超时时间(10 秒)。 - **实现**: - 使用 `axios.create` 创建实例,支持全局配置(如 `baseURL` 和 `timeout`)。 - 提供了 `request` 方法作为底层通用请求函数,支持自定义配置(如 `url`、`method`、`params`、`data` 等)。 --- ### 2. **请求拦截与响应处理** - **功能**: - **请求拦截**:在请求发送前添加元数据(如 `startTime` 和 `requestId`)和 token(从 `localStorage` 获取)。 - **响应拦截**:处理响应状态,记录请求耗时,统一处理错误(如 401、403、404 等)。 - **错误处理**:根据 HTTP 状态码生成错误消息(如“请求地址出错”、“未授权”等),并通过 `antdMessage` 提示用户。 - **实现**: - 使用 `axios.interceptors.request` 和 `axios.interceptors.response` 添加拦截器。 - 记录请求耗时(`startTime` 到响应时间的差值),打印日志(如 `Request ...` 和 `Response ...`)。 - 错误处理通过 `getErrorMessage` 映射状态码到提示信息,调用 `showErrorMessage` 显示。 --- ### 3. **请求去重** - **功能**: - 防止重复请求,同一请求(`url`、`method`、`params`、`data` 相同)只发送一次。 - 返回相同的 `Promise`,避免重复网络请求。 - **实现**: - 使用 `pendingRequests`(`Map`)存储正在进行的请求,`requestKey` 由 `method`、`url`、`params` 和 `data` 生成。 - 在 `request` 方法中检查 `requestKey`,如果存在则直接返回已有 `Promise`。 - 添加日志(`Request deduplicated: ...`)验证去重效果。 - 请求完成或失败后通过 `finally` 清理 `pendingRequests`。 --- ### 4. **请求队列** - **功能**: - 限制并发请求数量,防止过多请求同时发送,减轻服务器压力。 - 当前并发限制为 3(`concurrency: 3`)。 - **实现**: - 使用 `p-queue` 库创建 `queue`,设置 `concurrency` 为 3。 - 在 `request` 方法中通过 `queue.add` 调度请求,确保并发控制。 --- ### 5. **请求取消** - **功能**: - 支持单个请求取消和全局取消。 - 每个请求返回一个带有 `cancel` 方法的 `CancellablePromise`,允许手动取消。 - 提供 `cancelAllRequests` 方法取消所有正在进行的请求。 - **实现**: - 使用 `AbortController` 为每个请求生成 `signal`,通过 `controller.abort()` 取消请求。 - 在 `request` 方法中为 `Promise` 添加 `cancel` 方法,调用 `controller.abort()`。 - 使用 `controllers`(`Map`)存储每个请求的 `AbortController`,通过 `cancelAllRequests` 遍历取消。 --- ### 6. **请求缓存** - **功能**: - 支持 `GET` 请求缓存,减少重复网络请求。 - 缓存有效期可配置(默认 5 分钟,`DEFAULT_CACHE_DURATION`)。 - 自动清理过期缓存。 - **实现**: - 使用 `cache`(`Map`)存储缓存数据,`cacheKey` 由 `method`、`url` 和 `params` 生成。 - 在 `get` 方法中检查缓存,若命中则直接返回(`Cache hit for key: ...`)。 - 缓存存储时记录时间戳和有效期(`timestamp` 和 `duration`),通过 `setInterval` 每分钟清理过期缓存。 - 添加日志(`Cache set for key: ...` 和 `Cache cleared for key: ...`)验证缓存行为。 --- ### 7. **Token 刷新** - **功能**: - 自动处理 401 错误(未授权),刷新 token 并重试请求。 - 支持并发请求排队,等待 token 刷新完成。 - 刷新失败时清空 `localStorage` 并跳转到登录页。 - **实现**: - 在响应拦截器中捕获 401 错误,调用 `refreshToken` 方法模拟刷新。 - 使用 `isRefreshing` 标志和 `failedQueue` 管理并发请求,等待刷新完成后再重试。 - 刷新成功后更新 `localStorage` 和 `axios` 默认头部的 `Authorization`。 --- ### 8. **错误提示优化** - **功能**: - 确保同一时间只显示一个错误提示,避免堆叠。 - 合并相似错误,减少重复提示。 - **实现**: - 使用 `antdMessage.config({ maxCount: 1 })` 全局限制提示数量为 1。 - 在 `showErrorMessage` 中销毁当前提示(`antdMessage.destroy()`)并显示最新消息。 - 修改 `getErrorMessage`,移除动态 URL 信息(如 404 错误中的 `url`),确保相似错误一致(如“请求地址出错”)。 --- ### 9. **自动重试** - **功能**: - 对特定错误(如超时或 500+ 状态码)自动重试,最多 3 次。 - 每次重试间隔递增(1s、2s、3s)。 - **实现**: - 使用 `axios-retry` 插件,配置 `retries: 3` 和 `retryDelay`。 - 设置 `retryCondition`,仅对 `ECONNABORTED` 或 500+ 状态码重试。 --- ### 10. **全局 Loading 状态** - **功能**: - 跟踪全局请求状态,记录是否有正在进行的请求。 - **实现**: - 使用 `loadingCount` 计数器,在请求开始时加 1,结束时减 1。 - 通过 `setLoading` 方法更新状态,打印日志(`Global Loading: ...`)。 --- ### 11. **日志记录** - **功能**: - 记录请求和响应的详细信息,包括耗时、状态码等。 - 记录缓存命中、去重、取消等操作。 - **实现**: - 在拦截器中打印请求和响应日志(如 `Request ...` 和 `Response ...`)。 - 缓存操作打印 `Cache hit/set/cleared for key: ...`。 - 去重操作打印 `Request deduplicated: ...`。 --- ### 总结 `request.ts` 提供了以下核心功能: - **基础请求**:支持 `GET`、`POST`、`PUT`、`DELETE` 方法,基于 `axios` 实现。 - **请求优化**: - **去重**:防止重复请求,节省资源。 - **队列**:限制并发,提升性能。 - **取消**:支持单个和全局请求取消。 - **缓存**:支持 `GET` 请求缓存,减少重复请求。 - **错误处理**: - 统一处理 HTTP 错误,生成用户友好的提示。 - 限制提示数量(`maxCount: 1`),合并相似错误。 - **Token 管理**:自动刷新 token,处理 401 错误。 - **自动重试**:对特定错误自动重试,提升成功率。 - **日志**:详细记录请求、响应、缓存等操作,便于调试。 --- ### 使用场景 - **前端开发**:适合需要频繁发送 HTTP 请求的场景,如列表加载、表单提交等。 - **高并发环境**:通过队列和去重机制,适用于高并发请求场景。 - **用户体验优化**:通过错误提示优化和缓存,提升用户体验。 在 `request.ts` 中,TypeScript 被广泛应用以提升代码的可维护性、类型安全性和开发效率。以下是对 TypeScript 在该文件中的应用知识方法的详细总结,涵盖其核心用法和实践经验: --- ## TypeScript 应用知识方法总结 ### 1. **接口(Interface)与类型别名(Type Alias)** - **应用**: - 定义了 `CustomAxiosRequestConfig` 接口,扩展了 `axios` 的 `InternalAxiosRequestConfig`,添加了自定义属性(如 `metadata` 和 `_retry`),增强了类型检查。 - 定义了 `RequestConfig` 接口,指定了请求配置的结构(如 `url`、`method`、`params` 等),确保参数类型一致。 - 使用了 `CancellablePromise` 类型别名,扩展了 `Promise`,添加了 `cancel` 方法,提供了灵活的类型定义。 - **方法**: - 通过接口扩展现有类型(`extends`),实现类型复用。 - 使用泛型 `` 使返回值类型动态,适用于不同数据类型。 - 结合 `Omit` 工具类型,排除特定属性(如 `url`、`method`),创建子类型。 - **优势**: - 提高了代码的可读性和可预测性,避免了运行时类型错误。 - 支持 IDE 的智能提示,方便开发。 --- ### 2. **泛型(Generics)** - **应用**: - 在 `request`、`get`、`post` 等方法中使用了泛型 ``,表示返回数据的类型,允许调用者指定具体类型(如 `Promise`)。 - 例如,`get(url: string, params?: Record, config: Omit = {}): Promise`。 - **方法**: - 通过泛型参数 `` 约束返回值类型,确保类型一致性。 - 结合接口(如 `AxiosResponse`)使用,传递类型信息。 - **优势**: - 提高了代码的复用性,适配不同 API 返回类型。 - 避免了类型断言(如 `as any`),增强了类型安全性。 --- ### 3. **类型断言与类型守卫** - **应用**: - 在响应拦截器中,`response.config as CustomAxiosRequestConfig` 使用类型断言,将 `config` 转换为自定义类型。 - 在 `get` 方法中,`cached.data as T` 使用类型断言,将缓存数据转换为泛型类型。 - 使用 `axios.isAxiosError(error)` 作为类型守卫,判断错误是否为 `AxiosError`,以进行特定处理。 - **方法**: - 使用 `as` 关键字进行类型断言,仅在类型明确时使用。 - 通过 `if (axios.isAxiosError(error))` 进行窄化,启用错误对象的特定属性(如 `response?.status`)。 - **优势**: - 提高了类型精确性,避免了不必要的类型检查。 - 减少了运行时错误的可能性。 --- ### 4. **工具类型(Utility Types)** - **应用**: - 使用 `Omit` 排除 `RequestConfig` 中的特定属性,生成默认配置类型。 - **方法**: - 利用 TypeScript 内置工具类型 `Omit`,从现有接口中移除字段。 - **优势**: - 简化了类型定义,减少重复代码。 - 增强了配置的灵活性,允许部分属性可选。 --- ### 5. **联合类型与可选属性** - **应用**: - 在 `RequestConfig` 中,`method?: "get" | "post" | "put" | "delete"` 使用联合类型定义可选的 HTTP 方法。 - `params?: Record` 和 `data?: Record` 使用可选属性(`?`)表示可省略。 - `cache?: boolean | { duration: number }` 支持联合类型,允许布尔值或对象配置。 - **方法**: - 使用 `|` 分隔联合类型,定义可能的值范围。 - 使用 `?` 标记可选属性,增强接口的灵活性。 - **优势**: - 提供了丰富的配置选项,适应不同场景。 - 减少了类型强制性,提升了 API 使用便利性。 --- ### 6. **类型推断** - **应用**: - 在 `request` 方法中,`Promise.all(promises)` 自动推断返回类型为 `Promise`,无需显式指定。 - 在 `get` 方法中,`cached.data as T` 依赖上下文推断 `T` 的类型。 - **方法**: - 依靠 TypeScript 的类型推断机制,减少手动类型注解。 - 结合泛型和接口,允许编译器推导复杂类型。 - **优势**: - 减少了冗余代码,提高了开发效率。 - 确保了类型的一致性。 --- ### 7. **模块化与导出** - **应用**: - 导出 `cancelAllRequests`、`request`、`get`、`post`、`put` 和 `del` 函数,供外部使用。 - 使用 `export const` 声明常量(如 `DEFAULT_CACHE_DURATION`),便于全局配置。 - **方法**: - 使用 `export` 关键字暴露公共 API,隐藏私有实现(如 `cache` 和 `controllers`)。 - 通过模块化组织代码,遵循 TypeScript 的模块系统。 - **优势**: - 提高了代码的封装性,防止外部修改内部状态。 - 便于测试和维护。 --- ### 8. **类型安全与错误处理** - **应用**: - 在 `refreshToken` 和 `processQueue` 中,明确定义返回类型(如 `Promise`)和参数类型。 - 在 `showErrorMessage` 中,`content: string` 和 `isCritical: boolean = false` 提供类型约束。 - **方法**: - 使用函数签名明确输入输出类型。 - 结合类型守卫和断言处理动态类型(如 `error`)。 - **优势**: - 避免了运行时类型错误,确保函数行为可预测。 - 增强了代码的可测试性。 --- ### 9. **与第三方库集成** - **应用**: - 与 `axios`、`p-queue` 和 `antdMessage` 集成,定义了兼容的类型(如 `AxiosResponse` 和 `CancellablePromise`)。 - **方法**: - 使用 TypeScript 的声明文件(`@types/axios` 等)支持第三方库。 - 扩展现有类型(如 `CustomAxiosRequestConfig`)以满足需求。 - **优势**: - 确保了类型一致性,减少了与第三方库的兼容性问题。 - 提供了完整的类型提示,提高了开发体验。 --- ### 10. **调试与日志** - **应用**: - 在日志中使用了类型安全的字符串拼接(如 `Request ${config.metadata.requestId}: ...`)。 - **方法**: - 利用 TypeScript 的字符串模板字面量,结合类型检查。 - **优势**: - 确保日志输出格式正确,减少调试时的类型错误。 --- ### 总结与最佳实践 1. **类型定义**: - 使用接口和类型别名清晰定义数据结构,结合泛型提升复用性。 - 利用工具类型(如 `Omit`)简化类型操作。 2. **类型安全**: - 通过类型守卫和断言处理动态类型,确保运行时安全性。 - 避免过度使用 `any`,优先使用具体类型或联合类型。 3. **灵活性与扩展性**: - 结合可选属性和联合类型,提供灵活的配置选项。 - 使用泛型支持不同场景,增强代码适应性。 4. **模块化与封装**: - 通过导出明确公共 API,隐藏内部实现。 - 与第三方库集成时扩展类型,确保兼容性。 5. **调试支持**: - 利用类型系统增强日志和错误处理的可靠性。 TypeScript 在 `request.ts` 中的应用体现了其在大型项目中的价值,通过类型检查和智能提示显著降低了错误率,同时保持了代码的可维护性和可扩展性。如果需要进一步优化(如添加更复杂的类型约束),可以根据具体需求扩展! ## 可能涉及的面试问题 ### 一、请求封装功能相关问题 `request.ts` 文件是一个基于 `axios` 的 HTTP 请求封装模块,集成了请求去重、队列、缓存、Token 刷新、错误处理等功能。以下问题聚焦于功能实现、设计思路和优化方向。 1. **总体设计**: - 为什么选择 `axios` 作为底层请求库?如果需要替换为 `fetch`,需要做哪些调整? - `request.ts` 的主要功能有哪些?这些功能是如何满足项目需求的? - 为什么需要对请求进行封装,而不是直接使用 `axios`? 2. **请求去重**: - `pendingRequests` 是如何实现请求去重的?`requestKey` 的生成逻辑有什么潜在问题? - 如果两个请求的 `params` 顺序不同但内容相同,会导致去重失败吗?如何优化? - 如何处理动态参数(如时间戳)导致的去重失效? 3. **请求队列**: - 为什么使用 `p-queue` 实现请求队列?`concurrency: 3` 的设置依据是什么? - 如果需要动态调整并发数(例如根据网络状态),如何实现? - 队列机制如何影响请求性能?在哪些场景下可能成为瓶颈? 4. **请求取消**: - `AbortController` 是如何实现请求取消的?有哪些局限性? - `cancelAllRequests` 会取消所有请求,如何实现更细粒度的取消(例如只取消特定类型的请求)? - 如果一个请求已经被取消,如何确保后续逻辑不会继续执行? 5. **请求缓存**: - `cache` 机制是如何实现的?如何确保缓存不过期? - 为什么只对 `GET` 请求启用缓存?`POST` 请求是否可以缓存? - 如果缓存数据过大,如何优化内存使用?(例如设置缓存上限) - `setInterval` 清理缓存的实现有什么潜在问题?如何改进? 6. **Token 刷新**: - `refreshToken` 的实现中,如何确保并发请求不会重复触发刷新? - 如果 `refreshToken` 请求失败,如何优雅地处理用户体验(例如避免重复跳转到登录页)? - `failedQueue` 的作用是什么?如何避免内存泄漏? 7. **错误处理**: - `getErrorMessage` 是如何映射状态码到错误消息的?这种方式的局限性是什么? - 为什么对 401 错误进行特殊处理?如何扩展以支持其他状态码(如 429 限流)? - `showErrorMessage` 中 `maxCount: 1` 的设置可能会丢失提示,如何改进? 8. **自动重试**: - `axios-retry` 的重试机制是如何工作的?`retryCondition` 的条件是否合理? - 如何根据网络状态动态调整重试次数或间隔? - 重试机制可能导致哪些问题(例如重复提交)?如何避免? 9. **全局 Loading 状态**: - `loadingCount` 是如何管理全局 Loading 状态的?有哪些潜在的竞争条件? - 如果多个组件都需要显示 Loading 状态,如何将 `loadingCount` 集成到状态管理(如 Redux)? - 如何避免 `loadingCount` 变成负数? 10. **日志记录**: - 日志记录(如 `Request ...` 和 `Response ...`)的作用是什么?如何优化日志输出(例如按环境过滤)? - 如果日志量过大,如何避免性能问题? 11. **性能优化**: - `request.ts` 中有哪些性能瓶颈?如何优化? - `JSON.stringify` 在生成 `requestKey` 和 `cacheKey` 时可能影响性能,如何改进? - 如何减少不必要的请求(例如预加载或批量请求)? 12. **扩展性**: - 如果需要支持文件上传(如 `multipart/form-data`),需要对 `request.ts` 做哪些调整? - 如何扩展 `request.ts` 以支持 GraphQL 请求? - 如何添加请求超时提示(例如超过 5 秒未响应时显示提示)? 13. **错误场景**: - 如果服务器返回非标准的 HTTP 状态码(如 499),`request.ts` 如何处理? - 如何处理网络中断(例如离线状态)时的请求? - 如果 `baseURL` 发生变化(例如切换环境),如何动态调整? 14. **安全性**: - `request.ts` 中如何防止 XSS 或 CSRF 攻击? - `localStorage` 存储 token 是否安全?如何改进? - 如何确保请求头不泄露敏感信息? 15. **测试性**: - 如何对 `request.ts` 进行单元测试?需要 mock 哪些部分? - 如何模拟 Token 刷新失败的场景进行测试? - 如何测试请求去重和缓存功能的正确性? 16. **用户体验**: - 如何在请求失败时提供更友好的用户反馈(例如重试按钮)? - 如果网络请求耗时过长,如何提前通知用户? - 如何处理重复请求导致的用户操作混乱? 17. **其他**: - 为什么使用 `queueMicrotask` 清理 `pendingRequests`?与 `setTimeout` 有何不同? - `axios.create` 的优势是什么?为什么不直接使用 `axios` 全局实例? - 如果需要支持多实例(例如不同的 `baseURL`),如何改造 `request.ts`? --- ### 二、TypeScript 使用相关问题 `request.ts` 中大量使用了 TypeScript 来确保类型安全和代码可维护性。以下问题聚焦于 TypeScript 的应用、类型设计和最佳实践。 1. **类型定义**: - `CustomAxiosRequestConfig` 是如何扩展 `InternalAxiosRequestConfig` 的?为什么要这样做? - `RequestConfig` 中哪些属性是可选的?为什么要设计为可选? - `CancellablePromise` 的作用是什么?为什么需要扩展 `Promise`? 2. **泛型**: - `request` 方法中泛型 `` 的作用是什么?如何确保类型安全? - 如果调用 `get` 时不指定类型(如 `get("/users")`),会发生什么?如何改进? - 泛型在 `cache` 的定义中如何使用?(`Map`) 3. **接口与类型别名**: - 为什么使用 `interface` 定义 `RequestConfig`,而不是 `type`? - `failedQueue` 的类型定义中,为什么使用 `Array` 而不是 `[]`? - 如何在 `RequestConfig` 中添加一个新属性,同时保持向后兼容? 4. **工具类型**: - `Omit` 的作用是什么?为什么需要它? - 如果需要从 `RequestConfig` 中提取某些字段(如 `url` 和 `method`),可以使用哪些工具类型? - 如何使用 `Partial` 使 `RequestConfig` 的所有字段都变成可选? 5. **类型断言**: - `response.config as CustomAxiosRequestConfig` 的类型断言可能带来哪些风险? - 在 `cached.data as T` 中,为什么需要类型断言?如何避免? - 如何在编译时避免不必要的类型断言? 6. **类型守卫**: - `axios.isAxiosError(error)` 的作用是什么?如何扩展以支持自定义错误类型? - 如果 `error` 是一个未知类型,如何安全地访问其属性? - 如何使用 `in` 操作符进行类型窄化? 7. **联合类型与可选属性**: - `method?: "get" | "post" | "put" | "delete"` 的设计有什么优势? - `cache?: boolean | { duration: number }` 的联合类型如何影响调用者的使用? - 如何处理联合类型导致的复杂逻辑(例如 `cache` 的值可能是布尔或对象)? 8. **类型推断**: - 在 `Promise.all(promises)` 中,TypeScript 如何推断返回类型? - `cache.get(cacheKey)` 的返回值类型是什么?如何改进推断? - 如何确保 `JSON.stringify(params || {})` 的返回值类型正确? 9. **非空断言**: - `prom.resolve(token!)` 中的 `!` 有什么风险?如何避免? - 如何在编译时确保 `token` 非空,而不是依赖 `!`? - 非空断言在哪些场景下是安全的? 10. **可选链**: - `metadata?.startTime ?? 0` 的作用是什么?为什么需要 `??`? - 如果 `error.response` 可能为 `null`,如何安全地访问嵌套属性? - 可选链在性能上有什么影响? 11. **函数类型**: - `onError?: (error: any) => void` 的定义有什么问题?如何改进? - `refreshToken` 的返回类型 `Promise` 如何确保实现正确? - 如何为 `showErrorMessage` 添加更精确的类型(例如区分错误类型)? 12. **Record 类型**: - `Record` 在 `params` 中的作用是什么?为什么不使用具体类型? - `headers?: Record` 为什么允许多种值类型? - 如何限制 `Record` 的键名范围(例如只允许特定字符串)? 13. **类型注解**: - `loadingCount: number` 的类型注解是否有必要?为什么? - `setLoading` 的参数和返回值类型如何影响代码可读性? - 如何为复杂对象(例如 `cache`)添加更详细的类型注解? 14. **模块化与导出**: - 为什么 `cache` 和 `pendingRequests` 不导出?如何设计更安全的导出? - `export { showErrorMessage, showSuccessMessage }` 的设计有什么优点? - 如果需要将 `request.ts` 拆分为多个模块,如何调整类型定义? 15. **类型安全**: - `error: any` 的使用有什么风险?如何改进? - 如何确保 `requestKey` 的生成逻辑类型安全? - 如何避免 `as` 导致的类型不匹配问题? 16. **高级类型特性**: - 如何使用条件类型(Conditional Types)优化 `CancellablePromise`? - 如何使用映射类型(Mapped Types)重构 `RequestConfig`? - 如何使用 `infer` 关键字推断 `AxiosResponse` 的泛型类型? 17. **性能与类型**: - 复杂的类型定义(如嵌套泛型)对编译性能有什么影响? - `JSON.stringify` 的返回值类型如何影响类型检查? - 如何优化大型项目的类型定义以提高编译速度? 18. **错误处理**: - 如何为 `error` 定义一个更精确的类型(例如 `AxiosError`)? - 如果 `error` 的类型未知,如何安全地处理? - 如何使用 `never` 类型优化错误分支? 19. **与第三方库的类型集成**: - `axios` 的类型定义是如何集成到 `request.ts` 中的? - 如果 `p-queue` 的类型定义不完整,如何手动补充? - 如何处理 `antd` 的 `message` 类型以支持自定义配置? 20. **最佳实践**: - `request.ts` 中有哪些 TypeScript 最佳实践?有哪些可以改进的地方? - 如何避免过度使用 `any` 类型? - 如何在团队中推广 TypeScript 的正确使用? 21. **调试与类型**: - 如果类型定义错误,如何快速定位问题? - 如何使用 `tsconfig.json` 优化类型检查(例如启用 `strict` 模式)? - 如何调试复杂的泛型推断问题? ## 模块介绍逐字稿 ### 面试逐字稿:`request.ts` 请求二次封装介绍 #### 开场:为什么选择 `request.ts` 进行介绍 首先,我想说一下为什么我会选择 `request.ts` 这个模块来介绍。可能在项目中,很多人会更关注一些复杂的业务逻辑,比如复杂的组件设计、状态管理,或者一些炫酷的 UI 效果。但我认为,`request.ts` 虽然看起来不起眼,但它却是整个项目的基础和核心。 为什么这么说呢?因为在任何一个前端项目中,网络请求都是必不可少的,而请求的封装质量直接决定了项目的整体品质。无论是数据的加载速度、用户体验,还是代码的可维护性,都和请求封装息息相关。如果请求模块设计得不好,比如没有去重、没有错误处理,项目很容易出现重复请求、接口报错用户无感知等问题。所以,我觉得这个模块虽然基础,但非常重要,值得拿出来和大家分享。 接下来,我会从两个方面来介绍 `request.ts`:**功能部分** 和 **TypeScript 应用部分**,同时我会重点突出一些难点和解决方案。 --- #### 第一部分:功能部分 首先,我们来看 `request.ts` 的功能部分。这个模块是基于 `axios` 进行二次封装的,主要解决了我们在项目中常见的网络请求问题。 总结一下功能部分,`request.ts` 通过去重、队列、取消、缓存、Token 刷新等功能,解决了重复请求、性能瓶颈、用户体验等问题。重点难点在于去重的 `requestKey` 生成、缓存清理的内存管理,以及 Token 刷新的并发处理。 它的核心功能包括以下几个方面: 1. **基础请求方法** 我们提供了 `get`、`post`、`put` 和 `delete` 四种常用方法,基于 `axios.create` 创建了一个实例,设置了统一的 `baseURL` 和超时时间。这样可以让所有请求都有一个统一的入口,方便管理和维护。 2. **请求去重** 一个重点功能是请求去重。我们通过 `pendingRequests` 这个 `Map` 来存储正在进行的请求,用 `requestKey`(由 `method`、`url`、`params` 和 `data` 生成)来标识每个请求。如果同一个请求已经存在,我们直接返回已有的 `Promise`,避免重复发送。 **难点**:这里的一个难点是 `requestKey` 的生成。如果 `params` 或者 `data` 的顺序不同,可能会导致去重失败。我们通过 `JSON.stringify` 来序列化参数,但这可能会带来性能问题,尤其是在参数很大的情况下。优化方向是可以通过深度排序参数,或者只序列化关键字段来提升性能。 3. **请求队列** 我们使用了 `p-queue` 库,设置了并发限制为 3,防止一次性发送过多请求压垮服务器。 **难点**:并发数的设置是个难点。如果设置得太小,请求可能排队过长,影响用户体验;如果设置得太大,服务器可能扛不住压力。我们目前是静态设置的,未来可以根据网络状态动态调整并发数。 4. **请求取消** 我们通过 `AbortController` 实现了请求取消,支持单个请求的取消和全局取消(`cancelAllRequests`)。每个请求返回的 `Promise` 都带有一个 `cancel` 方法,方便调用者手动取消。 **难点**:一个难点是取消后的状态管理。如果请求被取消,后续的 `finally` 逻辑可能还会执行,我们通过 `queueMicrotask` 清理 `pendingRequests`,但仍需注意避免竞争条件。 5. **请求缓存** 我们对 `GET` 请求实现了缓存,通过 `cache` 这个 `Map` 存储数据,默认缓存 5 分钟,并通过 `setInterval` 定时清理过期缓存。 **难点**:缓存清理是个难点。`setInterval` 可能会导致内存泄漏,尤其是在模块卸载时没有清除定时器。我们可以通过监听页面卸载事件来清理,或者改用更现代的 `requestIdleCallback`。 6. **Token 刷新** 对于 401 错误,我们实现了自动 Token 刷新。通过 `isRefreshing` 标志和 `failedQueue` 队列,确保并发请求不会重复触发刷新。刷新成功后,更新 `localStorage` 和请求头,然后重试失败的请求。 **难点**:并发请求的排队是个难点。如果刷新失败,`failedQueue` 可能会导致内存泄漏。我们通过 `finally` 清空队列来解决,但仍需考虑极端场景,比如用户频繁刷新页面。 7. **错误处理和提示** 我们通过拦截器统一处理错误,根据状态码生成用户友好的提示,比如 404 显示“请求地址出错”。我们还通过 `antdMessage` 限制了提示数量(`maxCount: 1`),避免堆叠。 **难点**:错误提示的合并是个难点。如果短时间内有多个错误,可能会丢失提示信息。我们通过 `destroy` 当前提示来解决,但未来可以考虑使用队列机制显示所有错误。 8. **自动重试和全局 Loading** 我们通过 `axios-retry` 实现了自动重试,对超时或 500+ 状态码重试 3 次。同时,通过 `loadingCount` 管理全局 Loading 状态,方便 UI 显示加载中效果。 --- #### 第二部分:TypeScript 应用部分 接下来,我介绍一下 `request.ts` 中 TypeScript 的应用。 我们通过接口、泛型、工具类型和类型守卫,确保了代码的类型安全,同时提升了开发体验。重点难点在于泛型推断的准确性、第三方库类型兼容,以及非空断言的风险控制。 这个模块大量使用了 TypeScript 来确保类型安全和代码可维护性,我会重点讲几个关键点。 1. **接口和泛型** 我们定义了 `RequestConfig` 接口来描述请求配置,包含 `url`、`method`、`params` 等字段,其中 `method` 是联合类型(`"get" | "post" | "put" | "delete"`),`params` 和 `data` 是 `Record`,支持灵活的参数结构。 在 `request` 方法中,我们使用了泛型 ``,让调用者可以指定返回类型,比如 `get` 就能明确返回的是用户数组类型。 **难点**:泛型的使用是个难点。如果调用者不指定类型,TypeScript 会推断为 `unknown`,可能导致运行时错误。我们可以通过更好的文档或者默认类型来优化。 2. **类型扩展** 我们通过 `CustomAxiosRequestConfig` 扩展了 `axios` 的 `InternalAxiosRequestConfig`,添加了 `metadata` 和 `_retry` 字段,用于记录请求元数据和重试状态。 另外,我们定义了 `CancellablePromise`,扩展了 `Promise`,添加了 `cancel` 方法,确保返回的 `Promise` 类型安全。 **难点**:类型扩展的难点在于与第三方库的类型兼容。如果 `axios` 的类型定义更新,可能会导致不兼容。我们可以通过定期更新依赖,或者手动补充类型定义来解决。 3. **工具类型和类型守卫** 我们大量使用了工具类型,比如 `Omit`,用来生成 `get` 方法的配置类型,排除不需要的字段。 在错误处理中,我们用了 `axios.isAxiosError(error)` 作为类型守卫,确保安全访问 `error.response?.status`。 **难点**:类型守卫的难点在于动态类型的处理。如果 `error` 的类型未知,可能会导致运行时错误。我们可以通过更精确的类型定义(比如 `AxiosError`)来改进。 4. **类型安全和最佳实践** 我们尽量避免使用 `any`,比如 `error: any` 改成了更精确的类型定义。同时,我们使用了可选链(`metadata?.startTime ?? 0`)和非空断言(`token!`),确保代码安全。 **难点**:非空断言的难点在于潜在的风险。如果 `token` 真的为 `undefined`,会导致运行时错误。我们可以通过更好的逻辑校验,或者使用条件类型来优化。 --- #### 总结与展望 总的来说,`request.ts` 是一个非常核心的模块,它通过去重、队列、缓存、Token 刷新等功能,解决了网络请求中的常见问题,同时通过 TypeScript 确保了代码的类型安全和可维护性。 在开发过程中,我重点解决了去重逻辑、缓存清理、Token 并发等难点,同时在 TypeScript 方面,优化了泛型推断和类型兼容问题。