酷秀博客
首页/使用Ai封装的JS 异步http请求类A I

使用Ai封装的JS 异步http请求类

admin的头像admin3个月前A I121热度

HttpRequest 原生异步请求类使用文档


一、类简介


HttpRequest 是一款无依赖、高性能的原生异步请求类,基于 XMLHttpRequest 实现,支持请求/响应拦截、请求取消、重复请求处理、防抖、重试、文件上传下载、请求池化等核心功能,适用于对性能和兼容性要求较高的生产环境。

二、快速上手


1. 初始化实例


// 基础配置<br />
const http = new HttpRequest({<br />
    baseUrl: 'https://api.example.com', // 接口基础路径<br />
    timeout: 15000, // 默认超时时间<br />
    retryCount: 2, // 请求失败重试次数<br />
    retryDelay: 1000, // 重试间隔(ms)<br />
    debug: true, // 开启调试日志<br />
    debounceTime: 200, // 全局防抖延迟(ms)<br />
    poolMax: 8, // 请求池最大实例数<br />
    poolIdleTimeout: 20000, // 实例闲置超时销毁时间(ms)<br />
    headers: { // 全局请求头<br />
        'Content-Type': 'application/json',<br />
        'Authorization': 'Bearer your-token'<br />
    }<br />
});<br />
```<br />
<br />
### 2. 发送 GET 请求<br />
<br />
```javascript<br />
// 获取用户列表<br />
const { promise, cancel } = http.get('/user/list', {<br />
    data: { page: 1, size: 10 }, // 请求参数<br />
    cancelRepeat: true, // 取消重复请求<br />
    debounceTime: 300 // 单次请求防抖时间(覆盖全局)<br />
});<br />
<br />
promise.then(res => {<br />
    console.log('请求成功:', res.data);<br />
}).catch(err => {<br />
    console.error('请求失败:', err.message);<br />
});<br />
<br />
// 手动取消请求(如组件卸载时)<br />
// cancel();<br />
```<br />
<br />
### 3. 发送 POST 请求(JSON 格式)<br />
<br />
```javascript<br />
// 添加用户<br />
http.post('/user/add', {<br />
    data: { username: 'test', password: '123456' },<br />
    headers: { 'X-Request-ID': 'xxx' } // 单次请求头<br />
}).promise.then(res => {<br />
    console.log('添加成功:', res);<br />
});<br />
```<br />
<br />
### 4. 文件上传(FormData 格式)<br />
<br />
```javascript<br />
const uploadFile = (file) => {<br />
    const formData = new FormData();<br />
    formData.append('file', file);<br />
<br />
    http.post('/upload', {<br />
        data: formData,<br />
        headers: { 'Content-Type': 'multipart/form-data' },<br />
        onUploadProgress: (e) => {<br />
            console.log(`上传进度: ${(e.loaded / e.total) * 100}%`);<br />
        }<br />
    }).promise.then(res => {<br />
        console.log('上传成功:', res.data);<br />
    });<br />
};<br />
<br />
// 调用示例:绑定文件选择框<br />
document.querySelector('#file-input').addEventListener('change', (e) => {<br />
    uploadFile(e.target.files[0]);<br />
});<br />
```<br />
<br />
### 5. 文件下载(Blob 格式)<br />
<br />
```javascript<br />
const downloadFile = () => {<br />
    http.get('/file/export', {<br />
        responseType: 'blob', // 响应类型为 Blob<br />
        onDownloadProgress: (e) => {<br />
            console.log(`下载进度: ${(e.loaded / e.total) * 100}%`);<br />
        }<br />
    }).promise.then(res => {<br />
        // 生成下载链接<br />
        const blobUrl = URL.createObjectURL(res.data);<br />
        const a = document.createElement('a');<br />
        a.href = blobUrl;<br />
        a.download = 'export.xlsx'; // 文件名<br />
        a.click();<br />
        URL.revokeObjectURL(blobUrl); // 释放资源<br />
    });<br />
};<br />
```<br />
<br />
## 三、核心功能<br />
<br />
### 1. 拦截器<br />
<br />
支持注册**请求拦截器**(请求发送前修改配置)和**响应拦截器**(响应返回后统一处理)。<br />
<br />
```javascript<br />
// 1. 请求拦截器:添加 Token<br />
http.useRequestInterceptor(config => {<br />
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;<br />
    return config;<br />
});<br />
<br />
// 2. 响应拦截器:统一错误处理<br />
http.useResponseInterceptor(res => {<br />
    // 登录失效处理<br />
    if (res.data?.code === 401) {<br />
        localStorage.removeItem('token');<br />
        location.href = '/login';<br />
    }<br />
    return res;<br />
});<br />
```<br />
<br />
### 2. 防抖功能<br />
<br />
适用于高频请求场景(如输入框实时搜索),防抖窗口期内重复请求会重置计时。<br />
<br />
```javascript<br />
// 输入框搜索防抖<br />
document.querySelector('#search-input').addEventListener('input', (e) => {<br />
    const keyword = e.target.value.trim();<br />
    if (!keyword) return;<br />
<br />
    http.get('/search', {<br />
        data: { keyword },<br />
        debounceTime: 300 // 300ms 内输入停止后再请求<br />
    }).promise.then(res => {<br />
        console.log('搜索结果:', res.data);<br />
    });<br />
});<br />
<br />
// 手动取消防抖<br />
// http.cancelDebounce('GET-/search-keyword=xxx');<br />
```<br />
<br />
### 3. 请求池化<br />
<br />
复用 `XMLHttpRequest` 实例,减少高频请求下的实例创建/销毁开销,默认开启。<br />
<br />
```javascript<br />
// 高频轮询场景(池化效果显著)<br />
setInterval(() => {<br />
    http.get('/status', {<br />
        usePool: true // 启用池化(默认开启)<br />
    }).promise.then(res => {<br />
        console.log('服务状态:', res.data);<br />
    });<br />
}, 1000);<br />
```<br />
<br />
### 4. 重试机制<br />
<br />
针对网络错误、超时等场景自动重试,可配置重试次数和间隔。<br />
<br />
```javascript<br />
// 初始化时配置全局重试<br />
const http = new HttpRequest({ retryCount: 2, retryDelay: 1000 });<br />
<br />
// 单次请求覆盖全局配置<br />
http.get('/data', { retryCount: 3 }).promise.then(res => {<br />
    console.log(res);<br />
});<br />
```<br />
<br />
## 四、配置项说明<br />
<br />
### 1. 构造函数配置项<br />
<br />
| 配置项 | 类型 | 默认值 | 说明 |<br />
|--------|------|--------|------|<br />
| `baseUrl` | String | `''` | 接口基础路径 |<br />
| `timeout` | Number | `10000` | 请求超时时间(ms) |<br />
| `headers` | Object | `{ 'Content-Type': 'application/json' }` | 全局请求头 |<br />
| `retryCount` | Number | `0` | 请求失败重试次数 |<br />
| `retryDelay` | Number | `1000` | 重试间隔时间(ms) |<br />
| `debug` | Boolean | `false` | 是否开启调试日志 |<br />
| `debounceTime` | Number | `0` | 全局防抖延迟(ms),0 为关闭 |<br />
| `serializeCacheMax` | Number | `100` | 参数序列化缓存最大数量 |<br />
| `poolMax` | Number | `5` | 请求池最大实例数 |<br />
| `poolIdleTimeout` | Number | `30000` | 池实例闲置超时销毁时间(ms) |<br />
<br />
### 2. `request` 方法参数<br />
<br />
| 参数 | 类型 | 默认值 | 说明 |<br />
|------|------|--------|------|<br />
| `method` | String | - | 请求方法(GET/POST/PUT/DELETE) |<br />
| `url` | String | - | 接口路径(拼接 baseUrl) |<br />
| `options.data` | Object/FormData | `{}` | 请求参数 |<br />
| `options.headers` | Object | `{}` | 单次请求头(优先级高于全局) |<br />
| `options.timeout` | Number | 全局 `timeout` | 单次请求超时时间 |<br />
| `options.retryCount` | Number | 全局 `retryCount` | 单次请求重试次数 |<br />
| `options.retryDelay` | Number | 全局 `retryDelay` | 单次请求重试间隔 |<br />
| `options.cancelRepeat` | Boolean | `true` | 是否取消重复请求 |<br />
| `options.responseType` | String | `''` | 响应类型(如 blob/json) |<br />
| `options.debounceTime` | Number | 全局 `debounceTime` | 单次请求防抖延迟 |<br />
| `options.onUploadProgress` | Function | - | 上传进度回调 |<br />
| `options.onDownloadProgress` | Function | - | 下载进度回调 |<br />
| `options.usePool` | Boolean | `true` | 是否启用请求池化 |<br />
<br />
## 五、方法说明<br />
<br />
| 方法 | 说明 | 返回值 |<br />
|------|------|--------|<br />
| `get(url, options)` | 发送 GET 请求 | `{ promise, cancel, cancelDebounce }` |<br />
| `post(url, options)` | 发送 POST 请求 | `{ promise, cancel, cancelDebounce }` |<br />
| `put(url, options)` | 发送 PUT 请求 | `{ promise, cancel, cancelDebounce }` |<br />
| `delete(url, options)` | 发送 DELETE 请求 | `{ promise, cancel, cancelDebounce }` |<br />
| `useRequestInterceptor(fn)` | 注册请求拦截器 | - |<br />
| `useResponseInterceptor(fn)` | 注册响应拦截器 | - |<br />
| `cancelDebounce(reqKey)` | 手动取消防抖 | - |<br />
<br />
## 六、注意事项<br />
<br />
1. 当请求参数为 `FormData` 时,需手动设置请求头为 `multipart/form-data`,且框架会自动跳过 `Content-Type` 的手动设置(由浏览器自动添加边界符)。<br />
2. 防抖功能仅对**相同请求标识**的请求生效,请求标识由 `method + url + 参数` 生成。<br />
3. 请求池化功能在高频请求场景下性能提升明显,低频请求可关闭以节省内存。<br />
4. 调试日志(`debug: true`)仅建议在开发环境开启,生产环境关闭以减少性能开销。<br />
<br />
 <br />
```javascript<br />
class HttpRequest {<br />
    static METHODS = Object.freeze({<br />
        GET: 'GET',<br />
        POST: 'POST',<br />
        PUT: 'PUT',<br />
        DELETE: 'DELETE'<br />
    });<br />
<br />
    constructor(baseConfig = {}) {<br />
        this.config = Object.freeze({<br />
            baseUrl: baseConfig.baseUrl || '',<br />
            timeout: baseConfig.timeout || 10000,<br />
            headers: Object.freeze(baseConfig.headers || { 'Content-Type': 'application/json' }),<br />
            retryCount: baseConfig.retryCount || 0,<br />
            retryDelay: baseConfig.retryDelay || 1000,<br />
            debug: baseConfig.debug || false,<br />
            debounceTime: baseConfig.debounceTime || 0,<br />
            serializeCacheMax: baseConfig.serializeCacheMax || 100,<br />
            // 池化配置<br />
            poolMax: baseConfig.poolMax || 5, // 最大实例数<br />
            poolIdleTimeout: baseConfig.poolIdleTimeout || 30000 // 实例最大闲置时间(ms)<br />
        });<br />
<br />
        this.requestInterceptors = [];<br />
        this.responseInterceptors = [];<br />
        this.requestCache = new WeakMap();<br />
        this.debounceCache = new Map();<br />
        this.serializeCache = new Map();<br />
        // 请求池:key为请求方法,value为闲置实例队列<br />
        this.xhrPool = new Map();<br />
        // 初始化池队列<br />
        Object.values(HttpRequest.METHODS).forEach(method => {<br />
            this.xhrPool.set(method, []);<br />
        });<br />
    }<br />
<br />
    /**<br />
     * 从池内获取XHR实例<br />
     * @param {String} method 请求方法<br />
     * @returns {XMLHttpRequest}<br />
     */<br />
    getXhrFromPool(method) {<br />
        const queue = this.xhrPool.get(method) || [];<br />
        while (queue.length > 0) {<br />
            const xhr = queue.shift();<br />
            // 清除闲置定时器<br />
            clearTimeout(xhr.idleTimer);<br />
            // 重置实例状态<br />
            xhr.onload = null;<br />
            xhr.onerror = null;<br />
            xhr.ontimeout = null;<br />
            xhr.onabort = null;<br />
            if (xhr.upload) xhr.upload.onprogress = null;<br />
            return xhr;<br />
        }<br />
        // 池内无闲置实例,且未达最大容量则创建新实例<br />
        if (queue.length < this.config.poolMax) {<br />
            return new XMLHttpRequest();<br />
        }<br />
        // 超过最大容量,等待50ms后重试(避免阻塞)<br />
        return new Promise(resolve => {<br />
            setTimeout(() => resolve(this.getXhrFromPool(method)), 50);<br />
        });<br />
    }<br />
<br />
    /**<br />
     * 将XHR实例放回池内<br />
     * @param {String} method 请求方法<br />
     * @param {XMLHttpRequest} xhr 实例<br />
     */<br />
    putXhrToPool(method, xhr) {<br />
        const queue = this.xhrPool.get(method) || [];<br />
        if (queue.length >= this.config.poolMax) {<br />
            // 超过最大容量,直接销毁<br />
            return;<br />
        }<br />
        // 设置闲置超时定时器<br />
        xhr.idleTimer = setTimeout(() => {<br />
            const index = queue.indexOf(xhr);<br />
            index > -1 && queue.splice(index, 1);<br />
        }, this.config.poolIdleTimeout);<br />
        queue.push(xhr);<br />
        this.xhrPool.set(method, queue);<br />
    }<br />
<br />
    static serializeParams(data, maxCache = 100, cache = new Map()) {<br />
        if (typeof data !== 'object' || data === null) return '';<br />
        const cacheKey = JSON.stringify(data);<br />
        if (cache.has(cacheKey)) {<br />
            const value = cache.get(cacheKey);<br />
            cache.delete(cacheKey);<br />
            cache.set(cacheKey, value);<br />
            return value;<br />
        }<br />
<br />
        let paramsArr = [];<br />
        const keys = Object.keys(data);<br />
        for (let i = 0, len = keys.length; i < len; i++) {<br />
            const key = keys[i];<br />
            const value = data[key];<br />
            if (value === undefined || value === null || value === '') continue;<br />
            paramsArr.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);<br />
        }<br />
        const paramsStr = paramsArr.join('&');<br />
<br />
        if (cache.size >= maxCache) {<br />
            const firstKey = cache.keys().next().value;<br />
            cache.delete(firstKey);<br />
        }<br />
        cache.set(cacheKey, paramsStr);<br />
        return paramsStr;<br />
    }<br />
<br />
    generateReqKey(method, url, data) {<br />
        return [<br />
            method,<br />
            url,<br />
            HttpRequest.serializeParams(data, this.config.serializeCacheMax, this.serializeCache)<br />
        ].join('-');<br />
    }<br />
<br />
    useRequestInterceptor(interceptor) {<br />
        typeof interceptor === 'function' && this.requestInterceptors.push(interceptor);<br />
    }<br />
<br />
    useResponseInterceptor(interceptor) {<br />
        typeof interceptor === 'function' && this.responseInterceptors.push(interceptor);<br />
    }<br />
<br />
    cancelDebounce(reqKey) {<br />
        if (this.debounceCache.has(reqKey)) {<br />
            clearTimeout(this.debounceCache.get(reqKey));<br />
            this.debounceCache.delete(reqKey);<br />
            this.config.debug && console.log(`[Debounce Canceled] ${reqKey}`);<br />
        }<br />
    }<br />
<br />
    request(method, url, options = {}) {<br />
        const config = this.config;<br />
        const {<br />
            data = {},<br />
            headers = {},<br />
            timeout = config.timeout,<br />
            retryCount = config.retryCount,<br />
            retryDelay = config.retryDelay,<br />
            cancelRepeat = true,<br />
            responseType = '',<br />
            debounceTime = config.debounceTime,<br />
            onUploadProgress,<br />
            onDownloadProgress,<br />
            // 新增:是否启用池化<br />
            usePool = true<br />
        } = options;<br />
<br />
        let requestConfig = { method, url, data, headers, timeout, responseType };<br />
        const reqInterceptorLen = this.requestInterceptors.length;<br />
        for (let i = 0; i < reqInterceptorLen; i++) {<br />
            requestConfig = this.requestInterceptors[i](requestConfig) || requestConfig;<br />
        }<br />
        const { method: finalMethod, url: finalUrl, data: finalData, headers: finalHeaders, responseType: finalResponseType } = requestConfig;<br />
<br />
        const methodUpper = HttpRequest.METHODS[finalMethod.toUpperCase()] || finalMethod.toUpperCase();<br />
        const reqKey = this.generateReqKey(methodUpper, finalUrl, finalData);<br />
<br />
        let cancel;<br />
        let controller;<br />
        let promise;<br />
<br />
        const handleDebounce = () => {<br />
            this.cancelDebounce(reqKey);<br />
            if (debounceTime <= 0) return null;<br />
            return new Promise((resolve, reject) => {<br />
                const timer = setTimeout(() => {<br />
                    this.debounceCache.delete(reqKey);<br />
                    sendRealRequest().then(resolve).catch(reject);<br />
                }, debounceTime);<br />
                this.debounceCache.set(reqKey, timer);<br />
                config.debug && console.log(`[Debounce Start] ${reqKey} (delay: ${debounceTime}ms)`);<br />
            });<br />
        };<br />
<br />
        const sendRealRequest = async () => {<br />
            if (cancelRepeat) {<br />
                this.requestCache.has(reqKey) && this.requestCache.get(reqKey).abort();<br />
                controller = new AbortController();<br />
                cancel = () => {<br />
                    controller.abort();<br />
                    this.requestCache.delete(reqKey);<br />
                };<br />
                this.requestCache.set(reqKey, controller);<br />
            }<br />
<br />
            const fullUrl = config.baseUrl + finalUrl;<br />
            const isGet = methodUpper === HttpRequest.METHODS.GET;<br />
            const paramsStr = HttpRequest.serializeParams(finalData, config.serializeCacheMax, this.serializeCache);<br />
            const requestUrl = isGet && paramsStr ? `${fullUrl}?${paramsStr}` : fullUrl;<br />
            const allHeaders = Object.assign({}, config.headers, finalHeaders);<br />
            const startTime = config.debug ? Date.now() : 0;<br />
<br />
            const hasUploadProgress = typeof onUploadProgress === 'function';<br />
            const hasDownloadProgress = typeof onDownloadProgress === 'function';<br />
<br />
            const sendRequest = async (count) => {<br />
                // 获取XHR实例<br />
                let xhr = usePool ? await this.getXhrFromPool(methodUpper) : new XMLHttpRequest();<br />
                return new Promise((resolve, reject) => {<br />
                    xhr.open(methodUpper, requestUrl, true);<br />
                    xhr.timeout = timeout;<br />
                    xhr.responseType = finalResponseType;<br />
                    controller && (xhr.signal = controller.signal);<br />
<br />
                    const headerKeys = Object.keys(allHeaders);<br />
                    const headerLen = headerKeys.length;<br />
                    for (let i = 0; i < headerLen; i++) {<br />
                        const key = headerKeys[i];<br />
                        if (!(finalData instanceof FormData) || key !== 'Content-Type') {<br />
                            xhr.setRequestHeader(key, allHeaders[key]);<br />
                        }<br />
                    }<br />
<br />
                    xhr.onload = () => {<br />
                        cancelRepeat && this.requestCache.delete(reqKey);<br />
                        // 实例放回池内<br />
                        usePool && this.putXhrToPool(methodUpper, xhr);<br />
<br />
                        if (config.debug) {<br />
                            console.log(`[Request End] ${reqKey}`, {<br />
                                status: xhr.status,<br />
                                duration: `${Date.now() - startTime}ms`<br />
                            });<br />
                        }<br />
<br />
                        if (xhr.status >= 200 && xhr.status < 300) {<br />
                            let response = { data: xhr.response, status: xhr.status, statusText: xhr.statusText };<br />
                            const resInterceptorLen = this.responseInterceptors.length;<br />
                            for (let i = 0; i < resInterceptorLen; i++) {<br />
                                response = this.responseInterceptors[i](response) || response;<br />
                            }<br />
                            resolve(response);<br />
                        } else {<br />
                            reject(new Error(`Request failed with status ${xhr.status}`));<br />
                        }<br />
                    };<br />
<br />
                    const handleError = (errMsg) => {<br />
                        cancelRepeat && this.requestCache.delete(reqKey);<br />
                        // 实例放回池内<br />
                        usePool && this.putXhrToPool(methodUpper, xhr);<br />
                        reject(new Error(errMsg));<br />
                    };<br />
                    xhr.onerror = () => handleError('Network request failed');<br />
                    xhr.ontimeout = () => handleError(`Request timeout (${timeout}ms)`);<br />
                    xhr.onabort = () => handleError('Request aborted');<br />
<br />
                    hasUploadProgress && xhr.upload && (xhr.upload.onprogress = onUploadProgress);<br />
                    hasDownloadProgress && (xhr.onprogress = onDownloadProgress);<br />
<br />
                    let sendData = finalData;<br />
                    if (!isGet && !(sendData instanceof FormData)) {<br />
                        sendData = allHeaders['Content-Type'] === 'application/json' ? JSON.stringify(sendData) : paramsStr;<br />
                    }<br />
                    xhr.send(sendData);<br />
                }).catch(err => {<br />
                    if (count > 0) {<br />
                        config.debug && console.log(`[Request Retry] ${reqKey} (${count} times left)`);<br />
                        return new Promise(resolve => setTimeout(resolve, retryDelay)).then(() => sendRequest(count - 1));<br />
                    }<br />
                    return Promise.reject(err);<br />
                });<br />
            };<br />
<br />
            return sendRequest(retryCount);<br />
        };<br />
<br />
        if (debounceTime > 0) {<br />
            promise = handleDebounce();<br />
        } else {<br />
            promise = sendRealRequest();<br />
        }<br />
<br />
        return {<br />
            promise,<br />
            cancel: () => {<br />
                this.cancelDebounce(reqKey);<br />
                cancel && cancel();<br />
            },<br />
            cancelDebounce: () => this.cancelDebounce(reqKey)<br />
        };<br />
    }<br />
<br />
    get(url, options) {<br />
        return this.request(HttpRequest.METHODS.GET, url, options);<br />
    }<br />
<br />
    post(url, options) {<br />
        return this.request(HttpRequest.METHODS.POST, url, options);<br />
    }<br />
<br />
    put(url, options) {<br />
        return this.request(HttpRequest.METHODS.PUT, url, options);<br />
    }<br />
<br />
    delete(url, options) {<br />
        return this.request(HttpRequest.METHODS.DELETE, url, options);<br />
    }<br />
}<br />
<br />
签名: 最忠诚的BUG开发者来自: 重庆市. Chrome浏览器
文章目录