summaryrefslogtreecommitdiff
path: root/src/index.ts
diff options
context:
space:
mode:
authorLi Zhineng <[email protected]>2025-07-23 17:35:38 +0800
committerLi Zhineng <[email protected]>2025-07-23 17:35:38 +0800
commit45d2b6ad1d490817edb98df933779b19753a2e48 (patch)
tree1fa187ae3f765753285f3587c928a6feec07e228 /src/index.ts
downloadwave-45d2b6ad1d490817edb98df933779b19753a2e48.tar.gz
wave-45d2b6ad1d490817edb98df933779b19753a2e48.zip
first commit
Diffstat (limited to 'src/index.ts')
-rw-r--r--src/index.ts347
1 files changed, 347 insertions, 0 deletions
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<string, string>
+type QueryString = Record<string, string>
+type HttpMethod = WechatMiniprogram.RequestOption['method']
+type RequestTask = WechatMiniprogram.RequestTask | WechatMiniprogram.UploadTask
+type RequestData = Record<string, unknown>
+type ResponseData = WechatMiniprogram.RequestSuccessCallbackResult['data']
+type MiddlewareNext<T extends ResponseData> = (
+ request: PendingRequest
+) => RequestPromise<Response<SuccessCallbackResult<T>, T>>
+type Middleware<T extends ResponseData> = (
+ request: PendingRequest,
+ next: MiddlewareNext<T>
+) => RequestPromise<Response<SuccessCallbackResult<T>, T>>
+type SuccessCallbackResult<T extends ResponseData> =
+ | WechatMiniprogram.RequestSuccessCallbackResult<T>
+ | WechatMiniprogram.UploadFileSuccessCallbackResult
+type RequestPromise<T> = Promise<T> & {
+ task: RequestTask
+}
+
+export class Response<
+ T extends SuccessCallbackResult<U>,
+ 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<infer U> ? U : never {
+ return this.#response.data as T extends SuccessCallbackResult<infer U>
+ ? 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<any>[] = []
+
+ 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<any> | Middleware<any>[]) {
+ const request = this.#clone()
+ const handlers = Array.isArray(fn) ? fn : [fn]
+ for (const handler of handlers) {
+ request.#middlewares.push(handler)
+ }
+ return request
+ }
+
+ get<T extends ResponseData>(url: string, data: QueryString = {}) {
+ return this.method('GET').url(url).withQuery(data).#send<T>()
+ }
+
+ post<T extends ResponseData>(url: string, data: RequestData = {}) {
+ return this.method('POST').url(url).withBody(data).#send<T>()
+ }
+
+ put<T extends ResponseData>(url: string, data: RequestData = {}) {
+ return this.method('PUT').url(url).withBody(data).#send<T>()
+ }
+
+ delete<T extends ResponseData>(url: string, data: QueryString = {}) {
+ return this.method('DELETE').url(url).withQuery(data).#send<T>()
+ }
+
+ 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<WechatMiniprogram.UploadFileSuccessCallbackResult, string>
+ >
+
+ 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<T extends ResponseData>(): RequestPromise<
+ Response<SuccessCallbackResult<T>, T>
+ > {
+ return this.#throughMiddlewares<T>(this)(this.#dispatchRequest<T>)
+ }
+
+ #dispatchRequest<T extends ResponseData>(request: PendingRequest) {
+ let resolveHandler: (
+ value: Response<WechatMiniprogram.RequestSuccessCallbackResult<T>, T>
+ ) => void
+ let rejectHandler: (error: WechatMiniprogram.RequestFailCallbackErr) => void
+
+ const promise = new Promise((resolve, reject) => {
+ resolveHandler = resolve
+ rejectHandler = reject
+ }) as RequestPromise<
+ Response<WechatMiniprogram.RequestSuccessCallbackResult<T>, T>
+ >
+
+ promise.task = wx.request<T>({
+ 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<T extends ResponseData>(request: PendingRequest) {
+ return (handler: MiddlewareNext<T>) =>
+ this.#middlewares.reduceRight(
+ (next: MiddlewareNext<T>, 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<any>[] = []
+
+ new() {
+ return new PendingRequest()
+ .baseUrl(this.#baseUrl)
+ .use(this.#middlewares)
+ .asJson()
+ }
+
+ baseUrl(baseUrl: string) {
+ this.#baseUrl = baseUrl
+ return this
+ }
+
+ use(fn: Middleware<any> | Middleware<any>[]) {
+ const handlers = Array.isArray(fn) ? fn : [fn]
+ for (const handler of handlers) {
+ this.#middlewares.push(handler)
+ }
+ return this
+ }
+}
+
+export const Client = new Factory()