From 45d2b6ad1d490817edb98df933779b19753a2e48 Mon Sep 17 00:00:00 2001 From: Li Zhineng Date: Wed, 23 Jul 2025 17:35:38 +0800 Subject: first commit --- src/index.ts | 347 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/index.ts (limited to 'src/index.ts') diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..04a1870 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,347 @@ +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'] +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) + ) + } + + #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(): RequestPromise< + Response, T> + > { + return this.#throughMiddlewares(this)(this.#dispatchRequest) + } + + #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(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() -- cgit v1.2.3