yuumi-request
Version:
request queue for browser
191 lines (168 loc) • 6.08 kB
text/typescript
import { Queue } from "./queue/queue"
import { Job, ResolveFun, RejectFun } from "./queue/job"
import { RequestMethod } from "./request/type"
import { XHR, XHR_RequestOption } from "./request/xhr"
import { paramStringify } from "./utils"
export interface YuumiRequestInterface {
baseURI: string
headers: Record<string, string>
concurrency: number
timeout: number
}
export type YuumiRequestCtorOption = {
[K in keyof YuumiRequestInterface]?: YuumiRequestInterface[K] extends Function ? never : YuumiRequestInterface[K]
} & {
paramStringify?: (value: any) => string
xhr?: <T>(option: XHR_RequestOption) => Promise<T>
}
export type RequestOption = {
path: string
method: RequestMethod
async?: boolean
headers?: Record<string, string>
params?: Record<string, string|number>
data?: Record<string, any>
cancelToken?: (cancel?: () => void) => void
timeout?: number
enforce?: "pre"|"normal"
uploader?: { [key: string]: EventListener }
}
export type MethodRequestOptions = {
[K in keyof RequestOption as Exclude<K, "path"|"method">]: RequestOption[K];
}
export class YuumiRequestInterceptor {
public requestInterceptors: [ResolveFun, RejectFun?][] = []
public responseInterceptors: [ResolveFun, RejectFun?][] = []
constructor() {}
request(resolve:ResolveFun, reject?: RejectFun) {
this.requestInterceptors.push([resolve, reject])
}
response(resolve:ResolveFun, reject?: RejectFun) {
this.responseInterceptors.push([resolve, reject])
}
}
export class YuumiRequest implements YuumiRequestInterface {
readonly baseURI!: string
readonly headers!: Record<string, string>
readonly concurrency!: number
readonly timeout!: number
readonly paramStringify?: (value: any) => string
readonly xhr!: <T>(option: XHR_RequestOption) => Promise<T>
// 拦截器
readonly interceptor: YuumiRequestInterceptor = new YuumiRequestInterceptor()
// 请求队列
readonly queue!: Queue
constructor(option?: YuumiRequestCtorOption) {
const _option = Object.assign({
baseURI: "",
headers: {},
concurrency: 4,
timeout: 0,
xhr: (config: XHR_RequestOption) => new XHR(config).exec()
}, option)
this.baseURI = _option.baseURI
this.headers = _option.headers
this.concurrency = _option.concurrency
this.timeout = _option.timeout
this.paramStringify = _option.paramStringify
this.xhr = _option.xhr
this.queue = new Queue([], { concurrency: this.concurrency, autoStart: true })
}
public request<T>(option: RequestOption): Promise<T> {
let _url = `${this.baseURI}${option.path}`
const query = this.paramStringify ? this.paramStringify(option.params) : paramStringify(option.params)
if (query) {
_url = /\?/.test(_url) ? `${_url}&${query}` : `${_url}?${query}`
}
const config: XHR_RequestOption = {
url: _url,
method: option.method,
async: option.async,
headers: Object.assign({}, this.headers, option.headers),
data: option.data,
encodeData: this.paramStringify ? this.paramStringify(option.data) : paramStringify(option.data),
cancelToken: option.cancelToken,
timeout: option.timeout || this.timeout,
uploader: option.uploader
}
const job = new Job()
job.addTask(() => Promise.resolve(config))
// request interceptors
this.interceptor.requestInterceptors.forEach((item) => {
job.addTask(...item)
})
// request
job.addTask((value: any) => {
// 拼接拦截器添加的params
if (value.params) {
const _query = this.paramStringify ? this.paramStringify(value.params) : paramStringify(value.params);
if (_query) {
value.url = /\?/.test(_url) ? `${_url}&${_query}` : `${_url}?${_query}`;
}
}
return this.xhr(value as XHR_RequestOption).finally(() => {
job.dispatch("afterXHR")
})
})
// response interceptors
this.interceptor.responseInterceptors.forEach((item) => {
job.addTask(...item)
})
const result = new Promise<any>((resolve, reject) => {
// 当job在queue中等待时的cancelToken, 取消job直接返回
if(typeof config.cancelToken === 'function') {
config.cancelToken(() => {
job.cancel()
reject({ code: -1, message: "request:aborted" })
})
}
job.addTask(resolve, reject)
})
if (option.enforce === "pre") {
this.queue.unshift(job)
} else {
this.queue.push(job)
}
return result
}
public get<T>(path: string, params?: { [key: string]: number|string }, options?: MethodRequestOptions) {
const _options = Object.assign({}, options, {
path: path,
method: 'GET',
params: Object.assign({}, options?.params, params)
}) as RequestOption
return <Promise<T>>this.request.call(this, _options)
}
public post<T>(path: string, data?: any, options?: MethodRequestOptions) {
const _options = Object.assign({}, options, {
path: path,
method: 'POST',
data: Object.assign({}, options?.data, data)
}) as RequestOption
return <Promise<T>>this.request.call(this, _options)
}
public patch<T>(path: string, data?: any, options?: MethodRequestOptions) {
const _options = Object.assign({}, options, {
path: path,
method: 'PATCH',
data: Object.assign({}, options?.data, data)
}) as RequestOption
return <Promise<T>>this.request.call(this, _options)
}
public put<T>(path: string, data?: any, options?: MethodRequestOptions) {
const _options = Object.assign({}, options, {
path: path,
method: 'PUT',
data: Object.assign({}, options?.data, data)
}) as RequestOption
return <Promise<T>>this.request.call(this, _options)
}
public delete<T>(path: string, data?: any, options?: MethodRequestOptions) {
const _options = Object.assign({}, options, {
path: path,
method: 'DELETE',
data: Object.assign({}, options?.data, data)
}) as RequestOption
return <Promise<T>>this.request.call(this, _options)
}
}