php自动获取favicon源代码(支持ico,png,svg等格式)教程
45天前 > 功能介绍 该工具是一款零依赖、极致性能的 PHP 版网站 favicon(网站图标...
HttpRequest 原生异步请求类使用文档HttpRequest 是一款无依赖、高性能的原生异步请求类,基于 XMLHttpRequest 实现,支持请求/响应拦截、请求取消、重复请求处理、防抖、重试、文件上传下载、请求池化等核心功能,适用于对性能和兼容性要求较高的生产环境。
// 基础配置
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'
}
});
// 获取用户列表
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();
// 添加用户
http.post('/user/add', {
data: { username: 'test', password: '123456' },
headers: { 'X-Request-ID': 'xxx' } // 单次请求头
}).promise.then(res => {
console.log('添加成功:', res);
});
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]);
});
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. 请求拦截器:添加 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;
});
适用于高频请求场景(如输入框实时搜索),防抖窗口期内重复请求会重置计时。
// 输入框搜索防抖
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');
复用 XMLHttpRequest 实例,减少高频请求下的实例创建/销毁开销,默认开启。
// 高频轮询场景(池化效果显著)
setInterval(() => {
http.get('/status', {
usePool: true // 启用池化(默认开启)
}).promise.then(res => {
console.log('服务状态:', res.data);
});
}, 1000);
针对网络错误、超时等场景自动重试,可配置重试次数和间隔。
// 初始化时配置全局重试
const http = new HttpRequest({ retryCount: 2, retryDelay: 1000 });
// 单次请求覆盖全局配置
http.get('/data', { retryCount: 3 }).promise.then(res => {
console.log(res);
});
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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) |
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) |
手动取消防抖 | - |
FormData 时,需手动设置请求头为 multipart/form-data,且框架会自动跳过 Content-Type 的手动设置(由浏览器自动添加边界符)。method + url + 参数 生成。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);
}
}
#免责声明#

45天前 > 功能介绍 该工具是一款零依赖、极致性能的 PHP 版网站 favicon(网站图标...

44天前 > 本篇文章主要用于不修改数据库编码,可以支持emoji 表情显示支持。例如...

1个月前 用HTML构建广告与内容结构,CSS固定首屏全屏广告,JS用setInterval倒计时,结...

3个月前 好的,没问题!我已经帮你把整个插件系统整合好了,这是一个可以直接运...

2个月前 Typecho_Http_Client 类功能非常强大,它封装了 PHP 的 cURL 扩展,提供了简洁的 ...

3个月前 通过AI把xiunobbs中的数据库操作方式封装成一个类,方面其他框架程序引用使...