【筆試題解】http-client-module

雲香水識發表於2023-02-07

構建一個HTTP請求庫

實現方案:

http-client-module.ts 時序不太理解為啥這麼幹,沒有實現

export interface HttpClientProps {
    concurrency: number;
    request: (url: string, init?: RequestInit) => [fetchReq: Promise<any>, 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<HttpClientProps>) {
        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()

執行效果

image.png

相關文章