酷秀  - kxiu.cn

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

admin的头像admin1个月前A I92热度

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

一、类简介

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

二、快速上手

1. 初始化实例

// 基础配置
const http = new HttpRequest({
    baseUrl: 'https://api.example.com', // 接口基础路径
    timeout: 15000, // 默认超时时间
    retryCount: 2, // 请求失败重试次数
    retryDelay: 1000, // 重试间隔(ms)
    debug: true, // 开启调试日志
    debounceTime: 200, // 全局防抖延迟(ms)
    poolMax: 8, // 请求池最大实例数
    poolIdleTimeout: 20000, // 实例闲置超时销毁时间(ms)
    headers: { // 全局请求头
        'Content-Type': 'application/json',
        'Authorization': 'Bearer your-token'
    }
});

2. 发送 GET 请求

// 获取用户列表
const { promise, cancel } = http.get('/user/list', {
    data: { page: 1, size: 10 }, // 请求参数
    cancelRepeat: true, // 取消重复请求
    debounceTime: 300 // 单次请求防抖时间(覆盖全局)
});

promise.then(res => {
    console.log('请求成功:', res.data);
}).catch(err => {
    console.error('请求失败:', err.message);
});

// 手动取消请求(如组件卸载时)
// cancel();

3. 发送 POST 请求(JSON 格式)

// 添加用户
http.post('/user/add', {
    data: { username: 'test', password: '123456' },
    headers: { 'X-Request-ID': 'xxx' } // 单次请求头
}).promise.then(res => {
    console.log('添加成功:', res);
});

4. 文件上传(FormData 格式)

const uploadFile = (file) => {
    const formData = new FormData();
    formData.append('file', file);

    http.post('/upload', {
        data: formData,
        headers: { 'Content-Type': 'multipart/form-data' },
        onUploadProgress: (e) => {
            console.log(`上传进度: ${(e.loaded / e.total) * 100}%`);
        }
    }).promise.then(res => {
        console.log('上传成功:', res.data);
    });
};

// 调用示例:绑定文件选择框
document.querySelector('#file-input').addEventListener('change', (e) => {
    uploadFile(e.target.files[0]);
});

5. 文件下载(Blob 格式)

const downloadFile = () => {
    http.get('/file/export', {
        responseType: 'blob', // 响应类型为 Blob
        onDownloadProgress: (e) => {
            console.log(`下载进度: ${(e.loaded / e.total) * 100}%`);
        }
    }).promise.then(res => {
        // 生成下载链接
        const blobUrl = URL.createObjectURL(res.data);
        const a = document.createElement('a');
        a.href = blobUrl;
        a.download = 'export.xlsx'; // 文件名
        a.click();
        URL.revokeObjectURL(blobUrl); // 释放资源
    });
};

三、核心功能

1. 拦截器

支持注册请求拦截器(请求发送前修改配置)和响应拦截器(响应返回后统一处理)。

// 1. 请求拦截器:添加 Token
http.useRequestInterceptor(config => {
    config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
    return config;
});

// 2. 响应拦截器:统一错误处理
http.useResponseInterceptor(res => {
    // 登录失效处理
    if (res.data?.code === 401) {
        localStorage.removeItem('token');
        location.href = '/login';
    }
    return res;
});

2. 防抖功能

适用于高频请求场景(如输入框实时搜索),防抖窗口期内重复请求会重置计时。

// 输入框搜索防抖
document.querySelector('#search-input').addEventListener('input', (e) => {
    const keyword = e.target.value.trim();
    if (!keyword) return;

    http.get('/search', {
        data: { keyword },
        debounceTime: 300 // 300ms 内输入停止后再请求
    }).promise.then(res => {
        console.log('搜索结果:', res.data);
    });
});

// 手动取消防抖
// http.cancelDebounce('GET-/search-keyword=xxx');

3. 请求池化

复用 XMLHttpRequest 实例,减少高频请求下的实例创建/销毁开销,默认开启。

// 高频轮询场景(池化效果显著)
setInterval(() => {
    http.get('/status', {
        usePool: true // 启用池化(默认开启)
    }).promise.then(res => {
        console.log('服务状态:', res.data);
    });
}, 1000);

4. 重试机制

针对网络错误、超时等场景自动重试,可配置重试次数和间隔。

// 初始化时配置全局重试
const http = new HttpRequest({ retryCount: 2, retryDelay: 1000 });

// 单次请求覆盖全局配置
http.get('/data', { retryCount: 3 }).promise.then(res => {
    console.log(res);
});

四、配置项说明

1. 构造函数配置项

配置项 类型 默认值 说明
baseUrl String '' 接口基础路径
timeout Number 10000 请求超时时间(ms)
headers Object { 'Content-Type': 'application/json' } 全局请求头
retryCount Number 0 请求失败重试次数
retryDelay Number 1000 重试间隔时间(ms)
debug Boolean false 是否开启调试日志
debounceTime Number 0 全局防抖延迟(ms),0 为关闭
serializeCacheMax Number 100 参数序列化缓存最大数量
poolMax Number 5 请求池最大实例数
poolIdleTimeout Number 30000 池实例闲置超时销毁时间(ms)

2. request 方法参数

参数 类型 默认值 说明
method String - 请求方法(GET/POST/PUT/DELETE)
url String - 接口路径(拼接 baseUrl)
options.data Object/FormData {} 请求参数
options.headers Object {} 单次请求头(优先级高于全局)
options.timeout Number 全局 timeout 单次请求超时时间
options.retryCount Number 全局 retryCount 单次请求重试次数
options.retryDelay Number 全局 retryDelay 单次请求重试间隔
options.cancelRepeat Boolean true 是否取消重复请求
options.responseType String '' 响应类型(如 blob/json)
options.debounceTime Number 全局 debounceTime 单次请求防抖延迟
options.onUploadProgress Function - 上传进度回调
options.onDownloadProgress Function - 下载进度回调
options.usePool Boolean true 是否启用请求池化

五、方法说明

方法 说明 返回值
get(url, options) 发送 GET 请求 { promise, cancel, cancelDebounce }
post(url, options) 发送 POST 请求 { promise, cancel, cancelDebounce }
put(url, options) 发送 PUT 请求 { promise, cancel, cancelDebounce }
delete(url, options) 发送 DELETE 请求 { promise, cancel, cancelDebounce }
useRequestInterceptor(fn) 注册请求拦截器 -
useResponseInterceptor(fn) 注册响应拦截器 -
cancelDebounce(reqKey) 手动取消防抖 -

六、注意事项

  1. 当请求参数为 FormData 时,需手动设置请求头为 multipart/form-data,且框架会自动跳过 Content-Type 的手动设置(由浏览器自动添加边界符)。
  2. 防抖功能仅对相同请求标识的请求生效,请求标识由 method + url + 参数 生成。
  3. 请求池化功能在高频请求场景下性能提升明显,低频请求可关闭以节省内存。
  4. 调试日志(debug: true)仅建议在开发环境开启,生产环境关闭以减少性能开销。
class HttpRequest {
    static METHODS = Object.freeze({
        GET: 'GET',
        POST: 'POST',
        PUT: 'PUT',
        DELETE: 'DELETE'
    });

    constructor(baseConfig = {}) {
        this.config = Object.freeze({
            baseUrl: baseConfig.baseUrl || '',
            timeout: baseConfig.timeout || 10000,
            headers: Object.freeze(baseConfig.headers || { 'Content-Type': 'application/json' }),
            retryCount: baseConfig.retryCount || 0,
            retryDelay: baseConfig.retryDelay || 1000,
            debug: baseConfig.debug || false,
            debounceTime: baseConfig.debounceTime || 0,
            serializeCacheMax: baseConfig.serializeCacheMax || 100,
            // 池化配置
            poolMax: baseConfig.poolMax || 5, // 最大实例数
            poolIdleTimeout: baseConfig.poolIdleTimeout || 30000 // 实例最大闲置时间(ms)
        });

        this.requestInterceptors = [];
        this.responseInterceptors = [];
        this.requestCache = new WeakMap();
        this.debounceCache = new Map();
        this.serializeCache = new Map();
        // 请求池:key为请求方法,value为闲置实例队列
        this.xhrPool = new Map();
        // 初始化池队列
        Object.values(HttpRequest.METHODS).forEach(method => {
            this.xhrPool.set(method, []);
        });
    }

    /**
     * 从池内获取XHR实例
     * @param {String} method 请求方法
     * @returns {XMLHttpRequest}
     */
    getXhrFromPool(method) {
        const queue = this.xhrPool.get(method) || [];
        while (queue.length > 0) {
            const xhr = queue.shift();
            // 清除闲置定时器
            clearTimeout(xhr.idleTimer);
            // 重置实例状态
            xhr.onload = null;
            xhr.onerror = null;
            xhr.ontimeout = null;
            xhr.onabort = null;
            if (xhr.upload) xhr.upload.onprogress = null;
            return xhr;
        }
        // 池内无闲置实例,且未达最大容量则创建新实例
        if (queue.length < this.config.poolMax) {
            return new XMLHttpRequest();
        }
        // 超过最大容量,等待50ms后重试(避免阻塞)
        return new Promise(resolve => {
            setTimeout(() => resolve(this.getXhrFromPool(method)), 50);
        });
    }

    /**
     * 将XHR实例放回池内
     * @param {String} method 请求方法
     * @param {XMLHttpRequest} xhr 实例
     */
    putXhrToPool(method, xhr) {
        const queue = this.xhrPool.get(method) || [];
        if (queue.length >= this.config.poolMax) {
            // 超过最大容量,直接销毁
            return;
        }
        // 设置闲置超时定时器
        xhr.idleTimer = setTimeout(() => {
            const index = queue.indexOf(xhr);
            index > -1 && queue.splice(index, 1);
        }, this.config.poolIdleTimeout);
        queue.push(xhr);
        this.xhrPool.set(method, queue);
    }

    static serializeParams(data, maxCache = 100, cache = new Map()) {
        if (typeof data !== 'object' || data === null) return '';
        const cacheKey = JSON.stringify(data);
        if (cache.has(cacheKey)) {
            const value = cache.get(cacheKey);
            cache.delete(cacheKey);
            cache.set(cacheKey, value);
            return value;
        }

        let paramsArr = [];
        const keys = Object.keys(data);
        for (let i = 0, len = keys.length; i < len; i++) {
            const key = keys[i];
            const value = data[key];
            if (value === undefined || value === null || value === '') continue;
            paramsArr.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
        }
        const paramsStr = paramsArr.join('&');

        if (cache.size >= maxCache) {
            const firstKey = cache.keys().next().value;
            cache.delete(firstKey);
        }
        cache.set(cacheKey, paramsStr);
        return paramsStr;
    }

    generateReqKey(method, url, data) {
        return [
            method,
            url,
            HttpRequest.serializeParams(data, this.config.serializeCacheMax, this.serializeCache)
        ].join('-');
    }

    useRequestInterceptor(interceptor) {
        typeof interceptor === 'function' && this.requestInterceptors.push(interceptor);
    }

    useResponseInterceptor(interceptor) {
        typeof interceptor === 'function' && this.responseInterceptors.push(interceptor);
    }

    cancelDebounce(reqKey) {
        if (this.debounceCache.has(reqKey)) {
            clearTimeout(this.debounceCache.get(reqKey));
            this.debounceCache.delete(reqKey);
            this.config.debug && console.log(`[Debounce Canceled] ${reqKey}`);
        }
    }

    request(method, url, options = {}) {
        const config = this.config;
        const {
            data = {},
            headers = {},
            timeout = config.timeout,
            retryCount = config.retryCount,
            retryDelay = config.retryDelay,
            cancelRepeat = true,
            responseType = '',
            debounceTime = config.debounceTime,
            onUploadProgress,
            onDownloadProgress,
            // 新增:是否启用池化
            usePool = true
        } = options;

        let requestConfig = { method, url, data, headers, timeout, responseType };
        const reqInterceptorLen = this.requestInterceptors.length;
        for (let i = 0; i < reqInterceptorLen; i++) {
            requestConfig = this.requestInterceptors[i](requestConfig) || requestConfig;
        }
        const { method: finalMethod, url: finalUrl, data: finalData, headers: finalHeaders, responseType: finalResponseType } = requestConfig;

        const methodUpper = HttpRequest.METHODS[finalMethod.toUpperCase()] || finalMethod.toUpperCase();
        const reqKey = this.generateReqKey(methodUpper, finalUrl, finalData);

        let cancel;
        let controller;
        let promise;

        const handleDebounce = () => {
            this.cancelDebounce(reqKey);
            if (debounceTime <= 0) return null;
            return new Promise((resolve, reject) => {
                const timer = setTimeout(() => {
                    this.debounceCache.delete(reqKey);
                    sendRealRequest().then(resolve).catch(reject);
                }, debounceTime);
                this.debounceCache.set(reqKey, timer);
                config.debug && console.log(`[Debounce Start] ${reqKey} (delay: ${debounceTime}ms)`);
            });
        };

        const sendRealRequest = async () => {
            if (cancelRepeat) {
                this.requestCache.has(reqKey) && this.requestCache.get(reqKey).abort();
                controller = new AbortController();
                cancel = () => {
                    controller.abort();
                    this.requestCache.delete(reqKey);
                };
                this.requestCache.set(reqKey, controller);
            }

            const fullUrl = config.baseUrl + finalUrl;
            const isGet = methodUpper === HttpRequest.METHODS.GET;
            const paramsStr = HttpRequest.serializeParams(finalData, config.serializeCacheMax, this.serializeCache);
            const requestUrl = isGet && paramsStr ? `${fullUrl}?${paramsStr}` : fullUrl;
            const allHeaders = Object.assign({}, config.headers, finalHeaders);
            const startTime = config.debug ? Date.now() : 0;

            const hasUploadProgress = typeof onUploadProgress === 'function';
            const hasDownloadProgress = typeof onDownloadProgress === 'function';

            const sendRequest = async (count) => {
                // 获取XHR实例
                let xhr = usePool ? await this.getXhrFromPool(methodUpper) : new XMLHttpRequest();
                return new Promise((resolve, reject) => {
                    xhr.open(methodUpper, requestUrl, true);
                    xhr.timeout = timeout;
                    xhr.responseType = finalResponseType;
                    controller && (xhr.signal = controller.signal);

                    const headerKeys = Object.keys(allHeaders);
                    const headerLen = headerKeys.length;
                    for (let i = 0; i < headerLen; i++) {
                        const key = headerKeys[i];
                        if (!(finalData instanceof FormData) || key !== 'Content-Type') {
                            xhr.setRequestHeader(key, allHeaders[key]);
                        }
                    }

                    xhr.onload = () => {
                        cancelRepeat && this.requestCache.delete(reqKey);
                        // 实例放回池内
                        usePool && this.putXhrToPool(methodUpper, xhr);

                        if (config.debug) {
                            console.log(`[Request End] ${reqKey}`, {
                                status: xhr.status,
                                duration: `${Date.now() - startTime}ms`
                            });
                        }

                        if (xhr.status >= 200 && xhr.status < 300) {
                            let response = { data: xhr.response, status: xhr.status, statusText: xhr.statusText };
                            const resInterceptorLen = this.responseInterceptors.length;
                            for (let i = 0; i < resInterceptorLen; i++) {
                                response = this.responseInterceptors[i](response) || response;
                            }
                            resolve(response);
                        } else {
                            reject(new Error(`Request failed with status ${xhr.status}`));
                        }
                    };

                    const handleError = (errMsg) => {
                        cancelRepeat && this.requestCache.delete(reqKey);
                        // 实例放回池内
                        usePool && this.putXhrToPool(methodUpper, xhr);
                        reject(new Error(errMsg));
                    };
                    xhr.onerror = () => handleError('Network request failed');
                    xhr.ontimeout = () => handleError(`Request timeout (${timeout}ms)`);
                    xhr.onabort = () => handleError('Request aborted');

                    hasUploadProgress && xhr.upload && (xhr.upload.onprogress = onUploadProgress);
                    hasDownloadProgress && (xhr.onprogress = onDownloadProgress);

                    let sendData = finalData;
                    if (!isGet && !(sendData instanceof FormData)) {
                        sendData = allHeaders['Content-Type'] === 'application/json' ? JSON.stringify(sendData) : paramsStr;
                    }
                    xhr.send(sendData);
                }).catch(err => {
                    if (count > 0) {
                        config.debug && console.log(`[Request Retry] ${reqKey} (${count} times left)`);
                        return new Promise(resolve => setTimeout(resolve, retryDelay)).then(() => sendRequest(count - 1));
                    }
                    return Promise.reject(err);
                });
            };

            return sendRequest(retryCount);
        };

        if (debounceTime > 0) {
            promise = handleDebounce();
        } else {
            promise = sendRealRequest();
        }

        return {
            promise,
            cancel: () => {
                this.cancelDebounce(reqKey);
                cancel && cancel();
            },
            cancelDebounce: () => this.cancelDebounce(reqKey)
        };
    }

    get(url, options) {
        return this.request(HttpRequest.METHODS.GET, url, options);
    }

    post(url, options) {
        return this.request(HttpRequest.METHODS.POST, url, options);
    }

    put(url, options) {
        return this.request(HttpRequest.METHODS.PUT, url, options);
    }

    delete(url, options) {
        return this.request(HttpRequest.METHODS.DELETE, url, options);
    }
}
签名: 最忠诚的BUG开发者来自: 重庆市. Chrome浏览器
文章目录

新年快乐

×
新年快乐
同喜