1. HTTP请求的本质与前端开发的关系
作为前端开发者,我们每天都在和HTTP请求打交道。但你是否真正理解一个HTTP请求从浏览器发出到服务器响应的完整生命周期?这就像寄快递——你填写好寄件信息(请求头),打包好物品(请求体),选择快递公司(传输协议),然后等待收件人签收(服务器响应)。只不过在互联网世界里,这个过程发生在毫秒之间。
现代前端框架虽然帮我们封装了大部分HTTP交互细节,但理解底层原理能让你:
- 精准定位接口调试中的各种"玄学"问题
- 针对不同场景选择最合适的请求方式
- 设计出性能更优的API调用策略
- 处理各种边界case时更加得心应手
2. HTTP请求的核心组件拆解
2.1 请求行:请求的"身份证"
每个HTTP请求的第一行都包含三个关键信息:
code复制GET /api/users?id=123 HTTP/1.1
- 方法(Method):定义操作类型
GET:获取资源(查)POST:创建资源(增)PUT/PATCH:更新资源(改)DELETE:删除资源(删)HEAD:只获取响应头OPTIONS:预检请求(CORS用)
实际开发中我曾遇到一个坑:用GET请求带body(虽然规范允许但某些服务器会忽略)。正确做法是查询参数放URL,复杂数据用POST。
2.2 请求头:元信息"说明书"
就像快递面单上的备注信息,请求头告诉服务器如何处理这个请求。常见重要头信息:
| 头字段 | 作用 | 示例 |
|---|---|---|
Content-Type |
请求体类型 | application/json |
Authorization |
认证凭证 | Bearer xxxx |
Accept |
期望响应类型 | application/json |
User-Agent |
客户端标识 | Chrome/120.0 |
Cookie |
会话信息 | sessionId=abc123 |
调试时经常需要关注这些头信息。比如跨域问题往往是因为漏了Origin头,或者服务器没有正确返回Access-Control-Allow-Origin。
2.3 请求体:真正的"货物"
只有POST/PUT/PATCH等方法可以携带请求体,常见格式:
javascript复制// JSON格式(现代API主流)
{
"username": "john",
"password": "123456"
}
// FormData格式(文件上传常用)
const form = new FormData()
form.append('file', fileObj)
// URL编码格式(传统表单提交)
username=john&password=123456
我曾遇到一个文件上传的坑:当同时传文件和JSON数据时,需要把JSON字符串化后作为FormData的一个字段,而不是直接混用格式。
3. 前端发起HTTP请求的几种方式
3.1 原生XMLHttpRequest
虽然现在很少直接使用,但理解它有助于掌握更高级的封装:
javascript复制const xhr = new XMLHttpRequest()
xhr.open('GET', '/api/data')
xhr.onload = () => {
if (xhr.status === 200) {
console.log(xhr.response)
}
}
xhr.send()
特点:
- 事件回调机制
- 需要手动处理readyState
- 兼容性好但API繁琐
3.2 Fetch API:现代浏览器的选择
javascript复制fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({key: 'value'})
})
.then(response => {
if (!response.ok) throw new Error('Network error')
return response.json()
})
.then(data => console.log(data))
.catch(error => console.error(error))
注意事项:
- 默认不会携带cookie,需要设置
credentials: 'include' - 错误处理要同时检查
response.ok和catch网络错误 - 404/500等状态码不会触发catch,需要额外判断
3.3 Axios:功能最全面的方案
javascript复制axios.get('/api/data', {
params: { id: 123 },
timeout: 5000
})
.then(response => {
console.log(response.data)
})
.catch(error => {
if (error.response) {
// 服务器响应错误(4xx/5xx)
console.log(error.response.status)
} else if (error.request) {
// 请求已发出但无响应
console.log('No response received')
} else {
// 其他错误
console.log('Error', error.message)
}
})
优势对比:
- 自动转换JSON数据
- 更完善的错误处理
- 请求/响应拦截器
- 取消请求能力
- 客户端XSRF防护
4. 高阶实战技巧与性能优化
4.1 请求取消:避免内存泄漏
javascript复制// Axios取消令牌
const source = axios.CancelToken.source()
axios.get('/api/data', {
cancelToken: source.token
})
// 组件卸载时取消请求
source.cancel('Operation canceled by user')
// Fetch API使用AbortController
const controller = new AbortController()
fetch('/api/data', {
signal: controller.signal
})
controller.abort()
在React/Vue等框架中,这个技巧对防止组件卸载后setState报错至关重要。
4.2 并发控制与请求节流
当需要同时发送多个请求时:
javascript复制// 使用Promise.all
const [userData, productData] = await Promise.all([
fetch('/api/user'),
fetch('/api/products')
])
// 限制并发数(如只允许同时3个请求)
const limitedRequests = _.chunk(requests, 3).reduce(
async (prev, chunk) => {
await prev
return Promise.all(chunk.map(fn => fn()))
},
Promise.resolve()
)
对于搜索框等高频触发场景,应该添加防抖:
javascript复制function debounceFetch(query) {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
fetch(`/api/search?q=${query}`)
}, 300)
}
4.3 缓存策略实战
浏览器缓存:
- 设置
Cache-Control头 - 对静态资源使用hash文件名
应用层缓存:
javascript复制const cache = new Map()
async function getData(id) {
if (cache.has(id)) {
return cache.get(id)
}
const data = await fetch(`/api/data/${id}`)
cache.set(id, data)
return data
}
SWR/React Query等库实现了更完善的缓存策略,包括:
- 过期重新验证
- 后台更新
- 乐观更新
5. 安全防护与异常处理
5.1 防御性编程实践
javascript复制async function safeFetch(url, options) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(5000)
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const contentType = response.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
throw new TypeError("Oops, we haven't got JSON!")
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request timed out')
} else {
console.error('Fetch error:', error)
}
// 返回兜底数据或抛出统一错误
return { fallback: true }
}
}
5.2 安全防护措施
-
CSRF防护:
- 确保cookie设置SameSite属性
- 使用axios等库自动处理XSRF-TOKEN
-
XSS防护:
- 永远不要直接将API响应插入innerHTML
- 对用户输入进行转义
-
敏感信息:
- 不要在URL中传递敏感参数(会被记录在日志)
- 认证信息使用HttpOnly cookie
5.3 监控与日志
javascript复制// 请求拦截器
axios.interceptors.request.use(config => {
console.log(`[Request] ${config.method} ${config.url}`)
return config
})
// 响应拦截器
axios.interceptors.response.use(response => {
console.log(`[Response] ${response.status} ${response.config.url}`)
return response
}, error => {
if (error.response) {
logErrorToService(error.response)
}
return Promise.reject(error)
})
推荐使用Sentry等工具实现:
- 错误收集
- 性能监控
- 用户行为追踪
6. 现代前端请求架构设计
6.1 API客户端封装模式
typescript复制// apiClient.ts
class ApiClient {
private baseURL: string
constructor(baseURL: string) {
this.baseURL = baseURL
}
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const url = new URL(endpoint, this.baseURL)
url.search = new URLSearchParams(params).toString()
const response = await fetch(url.toString(), {
headers: this.getHeaders()
})
return this.handleResponse(response)
}
private getHeaders(): HeadersInit {
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getAuthToken()}`
}
}
private async handleResponse(response: Response) {
if (!response.ok) {
throw new ApiError(response.status, await response.text())
}
return response.json()
}
}
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message)
}
}
6.2 结合状态管理的请求方案
以Redux为例的中间件方案:
javascript复制// apiMiddleware.js
const apiMiddleware = store => next => action => {
if (action.type === 'API_REQUEST') {
const { url, method, data, onSuccess, onError } = action.payload
fetch(url, {
method,
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
store.dispatch({
type: onSuccess,
payload: data
})
})
.catch(error => {
store.dispatch({
type: onError,
payload: error.message
})
})
}
return next(action)
}
6.3 GraphQL与REST的混合架构
当部分场景需要更灵活的数据查询时:
javascript复制// 使用Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client'
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache()
})
client.query({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
name
email
posts {
title
}
}
}
`,
variables: { id: '123' }
})
架构建议:
- 主体业务用REST保持简单
- 复杂关联查询用GraphQL
- 使用同一个API网关统一入口
7. 调试工具与性能分析
7.1 Chrome开发者工具实战
Network面板高级技巧:
- 使用
Filter框快速定位请求(如method:POST) - 右键点击请求→
Copy as cURL可复现问题 - 勾选
Preserve log保留页面跳转前的请求 - 使用
Throttling模拟慢速网络
调试技巧:
- 在
Initiator列查看调用栈 - 使用
Block request URL功能测试降级方案 - 右键→
Replay XHR快速重放请求
7.2 性能优化指标
关键监控点:
- TTFB(Time To First Byte):服务器响应时间
- 传输时间:受内容大小和网络影响
- Waterfall分析:查看请求依赖关系
优化方案:
- 启用HTTP/2多路复用
- 使用CDN分发静态资源
- 对API响应启用Gzip压缩
- 减少重定向链
7.3 移动端真机调试
Android:
bash复制# 端口转发
adb reverse tcp:8080 tcp:8080
iOS:
- 使用Safari开发者模式
- 安装iOS调试代理工具
通用方案:
- 使用Charles/Fiddler抓包
- 配置代理查看HTTPS流量
- 使用
ngrok暴露本地服务
8. 未来趋势与新技术演进
8.1 WebTransport协议
下一代传输协议特点:
- 基于UDP的低延迟传输
- 多路复用无头阻塞
- 内置加密和拥塞控制
javascript复制const transport = new WebTransport('https://example.com:4999/')
await transport.ready
const stream = await transport.createBidirectionalStream()
const writer = stream.writable.getWriter()
await writer.write(new Uint8Array([1, 2, 3]))
8.2 Service Worker与离线缓存
实现离线优先策略:
javascript复制// sw.js
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})
8.3 WebSocket实时通信
javascript复制const socket = new WebSocket('wss://example.com')
socket.onopen = () => {
socket.send(JSON.stringify({ type: 'ping' }))
}
socket.onmessage = event => {
console.log('Message:', JSON.parse(event.data))
}
优化技巧:
- 添加心跳机制检测连接
- 使用JSON Web Token鉴权
- 实现断线自动重连
- 考虑使用Socket.IO等封装库
9. 项目实战:封装企业级请求库
9.1 基础架构设计
typescript复制interface RequestConfig {
baseURL?: string
timeout?: number
interceptors?: {
request?: Array<(config: RequestConfig) => RequestConfig>
response?: Array<(response: Response) => any>
}
}
class HttpClient {
private config: RequestConfig
constructor(config: RequestConfig) {
this.config = {
timeout: 10000,
...config
}
}
async request<T>(method: string, url: string, data?: any): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout)
try {
const fullUrl = url.startsWith('http') ? url : `${this.config.baseURL}${url}`
// 执行请求拦截器
let config = { method, url: fullUrl, body: data }
this.config.interceptors?.request?.forEach(interceptor => {
config = interceptor(config)
})
const response = await fetch(fullUrl, {
...config,
signal: controller.signal
})
clearTimeout(timeoutId)
// 执行响应拦截器
let processedResponse = response
this.config.interceptors?.response?.forEach(interceptor => {
processedResponse = interceptor(processedResponse)
})
return processedResponse.json()
} catch (error) {
clearTimeout(timeoutId)
throw error
}
}
}
9.2 高级功能实现
重试机制:
typescript复制async requestWithRetry<T>(
method: string,
url: string,
options?: {
retries?: number
retryDelay?: number
}
): Promise<T> {
const { retries = 3, retryDelay = 1000 } = options || {}
for (let i = 0; i < retries; i++) {
try {
return await this.request<T>(method, url)
} catch (error) {
if (i === retries - 1) throw error
await new Promise(resolve => setTimeout(resolve, retryDelay))
}
}
throw new Error('Unreachable')
}
请求队列:
typescript复制class RequestQueue {
private queue: Array<() => Promise<any>> = []
private concurrent: number = 0
constructor(private maxConcurrent: number = 5) {}
async add<T>(task: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
const wrappedTask = async () => {
try {
this.concurrent++
const result = await task()
resolve(result)
} catch (error) {
reject(error)
} finally {
this.concurrent--
this.next()
}
}
this.queue.push(wrappedTask)
this.next()
})
}
private next() {
while (this.queue.length > 0 && this.concurrent < this.maxConcurrent) {
const task = this.queue.shift()!
task()
}
}
}
9.3 单元测试策略
typescript复制import { HttpClient } from './httpClient'
import nock from 'nock'
describe('HttpClient', () => {
let client: HttpClient
beforeEach(() => {
client = new HttpClient({
baseURL: 'https://api.example.com'
})
nock('https://api.example.com')
.get('/test')
.reply(200, { success: true })
})
it('should make successful request', async () => {
const response = await client.request('GET', '/test')
expect(response).toEqual({ success: true })
})
it('should handle timeout', async () => {
nock('https://api.example.com')
.get('/slow')
.delay(1000)
.reply(200, {})
await expect(
client.request('GET', '/slow', {}, { timeout: 500 })
).rejects.toThrow('AbortError')
})
})
测试覆盖要点:
- 正常请求流程
- 各种错误场景(超时、网络错误、4xx/5xx)
- 拦截器链式调用
- 取消请求行为
- 重试逻辑验证
10. 性能优化深度实践
10.1 请求合并策略
对于高频触发的事件(如实时搜索),可以使用请求合并:
javascript复制class RequestBatcher {
private batch: Array<{
key: string
params: any
resolve: (value: any) => void
reject: (reason?: any) => void
}> = []
private timer: any = null
constructor(private delay: number = 100) {}
async add(key: string, params: any) {
return new Promise((resolve, reject) => {
this.batch.push({ key, params, resolve, reject })
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.delay)
}
})
}
private async flush() {
const currentBatch = [...this.batch]
this.batch = []
this.timer = null
try {
const grouped = groupBy(currentBatch, 'key')
const results = await Promise.all(
Object.entries(grouped).map(([key, items]) =>
this.executeBatch(key, items.map(i => i.params))
)
)
currentBatch.forEach((item, index) => {
item.resolve(results.find(r => r.key === item.key)?.data)
})
} catch (error) {
currentBatch.forEach(item => item.reject(error))
}
}
private async executeBatch(key: string, paramsList: any[]) {
// 实际批量请求逻辑
const response = await fetch(`/api/batch/${key}`, {
method: 'POST',
body: JSON.stringify(paramsList)
})
return response.json()
}
}
10.2 预加载与预连接
html复制<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- 预连接 -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- 预加载关键API -->
<link rel="preload" href="/api/critical-data" as="fetch" crossorigin>
动态预加载:
javascript复制function prefetchAPI(url) {
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = url
link.as = 'fetch'
document.head.appendChild(link)
}
// 鼠标悬停时预加载
searchInput.addEventListener('mouseenter', () => {
prefetchAPI('/api/suggestions')
})
10.3 压缩与二进制传输
使用Protocol Buffers替代JSON:
javascript复制// 前端使用protobuf.js
import { load } from 'protobufjs'
const root = await load('message.proto')
const Message = root.lookupType('package.Message')
const payload = { name: 'John', age: 30 }
const message = Message.create(payload)
const buffer = Message.encode(message).finish()
// 发送二进制数据
fetch('/api/protobuf', {
method: 'POST',
headers: {
'Content-Type': 'application/x-protobuf'
},
body: buffer
})
性能对比:
- JSON:{ "name": "John", "age": 30 } → 22字节
- Protobuf:0A044A6F686E101E → 8字节(压缩率63%)
11. 跨平台请求方案
11.1 React Native网络层
javascript复制// 使用内置Fetch API
fetch('https://api.example.com', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: 'John',
lastName: 'Doe'
})
})
// 使用Axios(需要额外配置)
import axios from 'axios'
import { Platform } from 'react-native'
const instance = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
adapter: Platform.OS === 'android' ? require('axios/lib/adapters/http') : undefined
})
特殊处理:
- Android需要明确指定HTTP适配器
- iOS需要配置ATS例外
- 使用
FormData上传文件时需要特殊处理
11.2 小程序网络请求
微信小程序示例:
javascript复制wx.request({
url: 'https://api.example.com',
method: 'POST',
data: {
key: 'value'
},
header: {
'content-type': 'application/json'
},
success(res) {
console.log(res.data)
},
fail(err) {
console.error(err)
}
})
注意事项:
- 需要配置合法域名
- 并发请求限制(微信小程序最多10个)
- 没有完整的Fetch API支持
- 文件上传需要使用专用API
11.3 Electron应用中的网络请求
主进程中使用Node.js的http/https模块:
javascript复制const https = require('https')
function makeRequest(url) {
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => resolve(JSON.parse(data)))
})
req.on('error', reject)
})
}
渲染进程中使用Browser的Fetch API,但需要注意:
- 启用
nodeIntegration时的安全风险 - 跨域问题需要通过预加载脚本处理
- 文件下载需要使用主进程桥接
12. 微前端架构中的请求处理
12.1 请求隔离方案
javascript复制// 为每个微应用创建独立的axios实例
function createMicroAppAxios(baseURL) {
const instance = axios.create({
baseURL,
withCredentials: true,
headers: {
'X-Micro-App': 'app-name'
}
})
// 添加微应用特定的拦截器
instance.interceptors.request.use(config => {
if (!isValidToken(localStorage.getItem('token'))) {
throw new Error('Invalid token')
}
return config
})
return instance
}
// 在主应用中
const authAxios = createMicroAppAxios('https://auth.app.com')
const productAxios = createMicroAppAxios('https://product.app.com')
12.2 共享请求管理
javascript复制class SharedRequestManager {
private sharedCache = new Map()
async fetchSharedData(key, url) {
if (this.sharedCache.has(key)) {
return this.sharedCache.get(key)
}
const data = await fetch(url).then(r => r.json())
this.sharedCache.set(key, data)
// 5分钟后过期
setTimeout(() => {
this.sharedCache.delete(key)
}, 300000)
return data
}
}
// 在不同微应用间共享
window.__sharedRequestManager = new SharedRequestManager()
12.3 跨应用通信方案
通过自定义事件实现微应用间的请求协调:
javascript复制// 发布请求
window.dispatchEvent(new CustomEvent('microapp-request', {
detail: {
type: 'getUserData',
payload: { userId: 123 },
requestId: 'uuid'
}
}))
// 订阅响应
window.addEventListener('microapp-response', (event) => {
if (event.detail.requestId === currentRequestId) {
// 处理响应数据
}
})
13. 大型项目中的请求治理
13.1 API版本控制策略
URL路径版本控制:
code复制https://api.example.com/v1/users
https://api.example.com/v2/users
请求头版本控制:
javascript复制fetch('/api/users', {
headers: {
'Accept-Version': '2.0.0'
}
})
最佳实践:
- 始终保留至少一个旧版本
- 使用语义化版本控制
- 提供清晰的弃用时间表
- 监控旧版本使用情况
13.2 请求监控与分析
关键监控指标:
- 成功率(2xx/3xx vs 4xx/5xx)
- 响应时间分布(P50/P90/P99)
- 流量趋势(QPS)
- 错误类型分布
实现方案:
javascript复制// 在请求拦截器中收集指标
axios.interceptors.response.use(response => {
const metrics = {
path: response.config.url,
status: response.status,
duration: Date.now() - response.config.metadata.startTime
}
// 发送到监控系统
sendMetrics(metrics)
return response
})
13.3 熔断与降级机制
javascript复制class CircuitBreaker {
constructor(private request, private options = {}) {
this.state = 'CLOSED'
this.failureCount = 0
this.nextAttempt = Date.now()
}
async fire() {
if (this.state === 'OPEN') {
if (this.nextAttempt <= Date.now()) {
this.state = 'HALF-OPEN'
} else {
throw new Error('Circuit is currently open')
}
}
try {
const response = await this.request()
this.reset()
return response
} catch (error) {
this.failureCount++
if (this.failureCount >= this.options.failureThreshold) {
this.state = 'OPEN'
this.nextAttempt = Date.now() + this.options.resetTimeout
}
throw error
}
}
reset() {
this.failureCount = 0
this.state = 'CLOSED'
}
}
14. 前沿技术探索
14.1 WebAssembly加速请求处理
javascript复制// 加载Wasm模块
const wasmModule = await WebAssembly.instantiateStreaming(
fetch('optimized.wasm'),
{ env: { memory: new WebAssembly.Memory({ initial: 1 }) } }
)
// 调用Wasm处理函数
const inputString = JSON.stringify(payload)
const inputBuffer = new TextEncoder().encode(inputString)
const wasmMemory = wasmModule.instance.exports.memory
const wasmInputPtr = wasmModule.instance.exports.alloc(inputBuffer.length)
new Uint8Array(wasmMemory.buffer).set(
inputBuffer,
wasmInputPtr
)
const outputPtr = wasmModule.instance.exports.process_data(
wasmInputPtr,
inputBuffer.length
)
const outputSize = wasmModule.instance.exports.get_output_size()
const outputBuffer = new Uint8Array(
wasmMemory.buffer,
outputPtr,
outputSize
)
const outputString = new TextDecoder().decode(outputBuffer)
const result = JSON.parse(outputString)
14.2 WebRTC实现P2P通信
javascript复制// 创建RTCPeerConnection
const pc = new RTCPeerConnection()
// 建立数据通道
const dc = pc.createDataChannel('apiChannel')
dc.onmessage = event => {
console.log('Received:', event.data)
}
// 发送请求
function sendRequest(data) {
if (dc.readyState === 'open') {
dc.send(JSON.stringify(data))
} else {
console.error('Data channel not ready')
}
}
14.3 边缘计算与请求分流
javascript复制// 检测边缘计算节点
async function getEdgeEndpoint() {
const resp = await fetch('https://edge-lb.example.com/nodes')
const nodes = await resp.json()
// 选择延迟最低的节点
const latencyTests = await Promise.all(
nodes.map(async node => ({
node,
latency: await measureLatency(node.pingUrl)
}))
)
return latencyTests.sort((a, b) => a.latency - b.latency)[0].node.apiUrl
}
// 使用边缘节点
const edgeUrl = await getEdgeEndpoint()
fetch(`${edgeUrl}/api/data`)
15. 终极实战:构建全功能请求库
15.1 架构设计图
code复制┌───────────────────────────────────────────────────────┐
│ HttpCoreLibrary │
├─────────────┬───────────────┬─────────────┬──────────┤
│ Request │ Interceptors │ Adapters │ Plugins │
│ Builder │ (req/res) │ (Fetch/XHR)│ (Cache, │
└─────────────┴───────────────┴─────────────┴──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌───────────────────────────────────────────────────────┐
│ Core Engine │
│ ┌────────────────────────────────────────────────┐ │
│ │ Request Queue │ Retry Logic │ Timeout │ │
│ └────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ Response Handler │
│ ┌───────────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ Transform │ │ Validate │ │ Error │ │
│ │ (JSON/Protobuf) │ (Schema) │ │ Handling │ │
│ └───────────────┘ └─────────────┘ └────────────┘ │
└───────────────────────────────────────────────────────┘
15.2 完整实现代码
typescript复制type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
interface RequestConfig {
url: string
method?: Method
baseURL?: string
headers?: Record<string, string>
params?: Record<string, any>
data?: any
timeout?: number
withCredentials?: boolean
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer'
validateStatus?: (status: number) => boolean
}
interface Response<T = any> {
data: T
status: number
statusText: string
headers: Record<string, string>
config: RequestConfig
}
class HttpError extends Error {
constructor(
public message: string,
public code?: string,
public response?: Response
) {
super(message)
}
}
class HttpCore {
private interceptors = {
request: [] as Array<(config: RequestConfig) => RequestConfig>,
response: [] as Array<(response: Response) => Response>
}
constructor(private defaults: Partial<RequestConfig> = {}) {}
async request<T = any>(config: RequestConfig): Promise<Response<T>> {
// 合并默认配置
const mergedConfig: RequestConfig = {
...this.defaults,
...config,
headers: {
...this.defaults.headers,
...config.headers
}
}
// 执行请求拦截器
let requestConfig = mergedConfig
for (const interceptor of this.interceptors.request) {
requestConfig = interceptor(requestConfig)
}
// 构建完整URL
const url = new URL(
requestConfig.url,
requestConfig.baseURL || window.location.origin
)
// 添加查询参数
if (requestConfig.params) {
Object.entries(requestConfig.params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, String(value))
}
})
}
// 创建AbortController
const controller = new AbortController()
const timeout = requestConfig.timeout || 30000
// 设置超时
const timeoutId = setTimeout(() => {
controller.abort()
}, timeout)
try {
// 执行请求
const response = await fetch(url.toString(), {
method: requestConfig.method || 'GET',
headers: requestConfig.headers,
body: requestConfig.data ? JSON.stringify(requestConfig.data) : undefined,
signal: controller.signal,
credentials: requestConfig.withCredentials ? 'include' : 'same-origin'
})
clearTimeout(timeoutId)
// 处理响应数据
let responseData
switch (requestConfig.responseType || 'json') {
case 'json':
responseData = await response.json()
break
case 'text':
responseData = await response.text()
break
case 'blob':
responseData = await response.blob()
break
case 'arraybuffer':
responseData = await response.arrayBuffer()
break
}
// 构建响应对象
const responseObj: Response = {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
config: requestConfig
}
// 验证状态码
const validateStatus = requestConfig.validateStatus ||
(status => status >= 200 && status < 300)
if (!validateStatus(response.status)) {
throw new HttpError(
`Request failed with status code ${response.status}`,
'ERR_BAD_RESPONSE',
responseObj
)
}
// 执行响应拦截器
let processedResponse = responseObj
for (const interceptor of this.interceptors.response) {
processedResponse = interceptor(processedResponse)
}
return processedResponse
} catch (error) {
clearTimeout(timeoutId)
if (error.name === 'AbortError')