实现方案:
http-client-module.ts
export interface HttpClientProps {
concurrency: number;
request: (url: string, init?: RequestInit) => [fetchReq: Promise, abort: () => void];
keyRender: (url: string, init?: RequestInit) => string;
}
export interface FetchQueueItem {
key: string;
url: string;
init: RequestInit & { abortHandler?: HttpAbortHandler }
resolve: (res: any) => void;
reject: (err: any) => void;
status: 'init' | 'fetch' | 'done' | 'error';
}
export class HttpClient {
private props = defaultClientProps
constructor (props?: Partial) {
if (props) {
this.props = Object.assign(defaultClientProps, props)
}
}
private queue: FetchQueueItem[] = []
private __do_fetch__ = () => {
const { queue, props: { concurrency, request }, __do_fetch__ } = this
let fetching = 0
let first_init_item: FetchQueueItem | null = null
for (let i = 0; i < queue.length; i++) {
const item = queue[i];
switch (item.status) {
case 'init':
first_init_item = first_init_item || item
break;
case 'fetch':
fetching++;
break;
}
}
if (fetching < concurrency && first_init_item) {
(function (item) {
const { url, init, resolve, reject } = item
item.status = 'fetch'
const [ fetchReq, abort ] = request(url, init)
init.abortHandler && init.abortHandler.setAbort(abort)
fetchReq.then(function (res) {
resolve?.(res)
item.status = 'done'
})
.catch(function (err) {
item.status = 'error'
reject?.(err)
})
.finally(function () {
__do_fetch__()
})
})(first_init_item)
}
}
request = (url: string, init: RequestInit & { abortHandler?: HttpAbortHandler }) => {
const { props: { keyRender }, queue, __do_fetch__ } = this
const key = keyRender(url, init)
const has = queue.find(q => q.key === key)
if (has) {
throw new Error('fetch key duplicated!')
}
const item = new Promise(function (resolve, reject) {
queue.push({
key, url, init,
status: 'init',
resolve, reject,
})
__do_fetch__();
})
return item
}
get = (url: string, init: { abortHandler?: HttpAbortHandler } = {}) => this.request(url, init)
post = (url: string, init: RequestInit & { abortHandler?: HttpAbortHandler } = {}) => this.request(url, { ...init, method: 'POST' })
}
export class HttpAbortHandler {
abort: () => void
setAbort = (abort: () => void) => this.abort = abort
}
/** 基于fetch的封装 */
export const REQUEST_FETCH: HttpClientProps['request'] = (url: string, init: RequestInit = {}) => {
const controller = new AbortController()
const fetchReq = fetch(url, {
...init,
signal: controller.signal
}).then(res => res.json())
return [ fetchReq, function () { controller.abort() } ]
}
/** 基于xhr的封装 */
export const REQUEST_XHR:HttpClientProps['request'] = (url: string, init: RequestInit = {}) => {
const xhr = new XMLHttpRequest()
const fetchReq = new Promise(function (resolve, reject) {
xhr.addEventListener('readystatechange', function (e) {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const res = JSON.stringify(xhr.responseText)
resolve(res)
} catch (e) {
reject(e)
}
} else {
reject(xhr.status)
}
}
})
xhr.addEventListener('abort', reject)
xhr.open(init?.method || 'GET', url)
xhr.send(init?.body as XMLHttpRequestBodyInit)
})
return [ fetchReq, function () { xhr.abort() } ]
}
测试用例
test.ts
import { HttpAbortHandler, HttpClient, HttpClientProps } from "./http-client-module"
const logger = {
log: (...args: any[]) => {
console.log.apply(console, [new Date().toLocaleString(), ...args])
}
}
/** 基于setTimeout封装 */
const REQUEST_TIMEOUT: HttpClientProps['request'] = (url: string, init: RequestInit = {}) => {
let timer: number
let _reject: (err: any) => void
const abort = function () {
clearTimeout(timer)
_reject?.('abort!')
}
const fetchReq = new Promise(function (resolve, reject) {
const mat = url.match(/([0-9.]+)$/)
const timeout = mat ? Number(mat[1]) : 2000
_reject = reject
timer = setTimeout(function () {
resolve(timeout)
}, timeout)
})
return [ fetchReq, abort ]
}
// TEST
const test = function () {
const client = new HttpClient({
concurrency: 2,
request: REQUEST_TIMEOUT,
})
client.get('/path-1?t=1000').then(() => logger.log('/path-1'))
client.get('/path-2?t=3000').then(() => logger.log('/path-2'))
client.get('/path-3?t=2050').then(() => logger.log('/path-3'))
client.get('/path-4?t=1000').then(() => logger.log('/path-4'))
client.get('/path-5?t=4000').then(() => logger.log('/path-5'))
const handler = new HttpAbortHandler()
client.get('/path-6?t=3000', {
abortHandler: handler
})
.then(() => logger.log('/path-6'))
.catch(err => logger.log('/path-6', err))
setTimeout(() => {
handler.abort()
}, 4200);
}
test()