type MessageHeader = Record type QueryString = Record type HttpMethod = WechatMiniprogram.RequestOption['method'] type RequestTask = WechatMiniprogram.RequestTask | WechatMiniprogram.UploadTask type RequestData = Record type ResponseData = | WechatMiniprogram.RequestSuccessCallbackResult['data'] | WechatMiniprogram.UploadFileSuccessCallbackResult['data'] type MiddlewareNext = ( request: PendingRequest ) => RequestPromise, T>> type Middleware = ( request: PendingRequest, next: MiddlewareNext ) => RequestPromise, T>> type SuccessCallbackResult = | WechatMiniprogram.RequestSuccessCallbackResult | WechatMiniprogram.UploadFileSuccessCallbackResult type RequestPromise = Promise & { task: RequestTask } export class Response< T extends SuccessCallbackResult, U extends ResponseData > { #response constructor(response: T) { this.#response = response } status() { return this.#response.statusCode } ok() { return this.status() === 200 } successful() { const status = this.status() return status >= 200 && status < 300 } clientError() { const status = this.status() return status >= 400 && status < 500 } serverError() { const status = this.status() return status >= 500 && status < 600 } data(): T extends SuccessCallbackResult ? U : never { return this.#response.data as T extends SuccessCallbackResult ? U : never } json() { const data = this.data() if (typeof data === 'string') { try { return JSON.parse(data) } catch { return {} } } return data } raw() { return this.#response } } class PendingRequest { #httpMethod!: HttpMethod #baseUrl: string | null = null #url!: string #query: QueryString = {} #header: MessageHeader = {} #data: RequestData = {} #middlewares: Middleware[] = [] method(method: HttpMethod) { const request = this.#clone() request.#httpMethod = method return request } baseUrl(baseUrl: string | null) { const request = this.#clone() request.#baseUrl = baseUrl return request } url(url: string) { const request = this.#clone() request.#url = url return request } withQuery(query: QueryString) { const request = this.#clone() request.#query = query return request } hasHeader(name: string) { return name in this.#header } withHeader(name: string, value: string) { const request = this.#clone() request.#header[name] = value return request } getHeaderLine(name: string) { return this.#header[name] ?? null } header() { return this.#header } asJson() { return this.withHeader('content-type', 'application/json') } asForm() { return this.withHeader('content-type', 'application/x-www-form-urlencoded') } asMultipart() { return this.withHeader('content-type', 'multipart/form-data') } isJson() { return this.getHeaderLine('content-type') === 'application/json' } withBody(data: RequestData) { const request = this.#clone() request.#data = data return request } body() { return this.#data } use(fn: Middleware | Middleware[]) { const request = this.#clone() const handlers = Array.isArray(fn) ? fn : [fn] for (const handler of handlers) { request.#middlewares.push(handler) } return request } get(url: string, data: QueryString = {}) { return this.method('GET').url(url).withQuery(data).#send() } post(url: string, data: RequestData = {}) { return this.method('POST').url(url).withBody(data).#send() } put(url: string, data: RequestData = {}) { return this.method('PUT').url(url).withBody(data).#send() } delete(url: string, data: QueryString = {}) { return this.method('DELETE').url(url).withQuery(data).#send() } upload( option: Pick< WechatMiniprogram.UploadFileOption, 'url' | 'filePath' | 'name' > ) { const request = this.url(option.url).asMultipart() return this.#throughMiddlewares(request)( this.#dispatchUpload(option.name, option.filePath) ) as RequestPromise< Response< WechatMiniprogram.UploadFileSuccessCallbackResult, WechatMiniprogram.UploadFileSuccessCallbackResult['data'] > > } #dispatchUpload(name: string, filePath: string) { return (request: PendingRequest) => { let resolveHandler: ( value: Response< WechatMiniprogram.UploadFileSuccessCallbackResult, string > ) => void let rejectHandler: ( error: WechatMiniprogram.GeneralCallbackResult ) => void const promise = new Promise((resolve, reject) => { resolveHandler = resolve rejectHandler = reject }) as RequestPromise< Response > promise.task = wx.uploadFile({ url: request.#buildUrl(), filePath, name, header: request.#header, enableHttp2: true, success(res) { resolveHandler(new Response(res)) }, fail(err) { rejectHandler(err) } }) return promise } } #send() { return this.#throughMiddlewares(this)( this.#dispatchRequest ) as RequestPromise< Response, T> > } #dispatchRequest(request: PendingRequest) { let resolveHandler: ( value: Response, T> ) => void let rejectHandler: (error: WechatMiniprogram.RequestFailCallbackErr) => void const promise = new Promise((resolve, reject) => { resolveHandler = resolve rejectHandler = reject }) as RequestPromise< Response, T> > promise.task = wx.request({ url: request.#buildUrl(), data: request.body(), header: request.#header, method: request.#httpMethod, dataType: request.isJson() ? 'json' : '其他', responseType: 'text', useHighPerformanceMode: true, success: (res) => { resolveHandler(new Response(res)) }, fail: (err) => { rejectHandler(err) } }) return promise } #throughMiddlewares(request: PendingRequest) { return (handler: MiddlewareNext) => this.#middlewares.reduceRight( (next: MiddlewareNext, middleware) => (request: PendingRequest) => (middleware as Middleware)(request, next), handler )(request) } #clone() { const request = new PendingRequest() request.#httpMethod = this.#httpMethod request.#baseUrl = this.#baseUrl request.#url = this.#url request.#query = { ...this.#query } request.#header = { ...this.#header } request.#data = { ...this.#data } request.#middlewares = [...this.#middlewares] return request } #buildUrl() { let url = this.#url.endsWith('/') ? this.#url.slice(0, -1) : this.#url if (this.#baseUrl !== null && !url.match(/^[0-9a-zA-Z]+:\/\//)) { const baseUrl = this.#baseUrl.endsWith('/') ? this.#baseUrl.slice(0, -1) : this.#baseUrl const path = url.startsWith('/') ? url.slice(1) : url url = path === '' ? baseUrl : `${baseUrl}/${path}` } const query = this.#buildQueryString() return query === '' ? url : `${url}?${query}` } #buildQueryString() { return Object.keys(this.#query) .map((name) => [encodeURIComponent(name), encodeURIComponent(this.#query[name])].join( '=' ) ) .join('&') } } export class Factory { #baseUrl: string | null = null #middlewares: Middleware[] = [] new() { return new PendingRequest() .baseUrl(this.#baseUrl) .use(this.#middlewares) .asJson() } baseUrl(baseUrl: string) { this.#baseUrl = baseUrl return this } use(fn: Middleware | Middleware[]) { const handlers = Array.isArray(fn) ? fn : [fn] for (const handler of handlers) { this.#middlewares.push(handler) } return this } } export const Client = new Factory()