UNPKG

@cloudbase/utilities

Version:
242 lines (230 loc) 7.48 kB
import { SDKAdapterInterface, AbstractSDKRequest, IRequestOptions, ResponseObject, IUploadRequestOptions, IRequestConfig, IRequestMethod, IFetchOptions, } from '@cloudbase/adapter-interface' import { isFormData, formatUrl, toQueryString } from '../../libs/util' import { getProtocol } from '../../constants/common' /** * @class WebRequest */ class WebRequest extends AbstractSDKRequest { // 默认不限超时 private readonly timeout: number // 超时提示文案 private readonly timeoutMsg: string // 超时受限请求类型,默认所有请求均受限 private readonly restrictedMethods: Array<IRequestMethod> constructor(config: IRequestConfig) { super() const { timeout, timeoutMsg, restrictedMethods } = config this.timeout = timeout || 0 this.timeoutMsg = timeoutMsg || '请求超时' this.restrictedMethods = restrictedMethods || ['get', 'post', 'upload', 'download'] } public get(options: IRequestOptions): Promise<ResponseObject> { return this.request( { ...options, method: 'get', }, this.restrictedMethods.includes('get'), ) } public post(options: IRequestOptions): Promise<ResponseObject> { return this.request( { ...options, method: 'post', }, this.restrictedMethods.includes('post'), ) } public put(options: IRequestOptions): Promise<ResponseObject> { return this.request({ ...options, method: 'put', }) } public upload(options: IUploadRequestOptions): Promise<ResponseObject> { const { data, file, name, method, headers = {} } = options const reqMethod = { post: 'post', put: 'put' }[method?.toLowerCase()] || 'put' // 上传方式为post时,需转换为FormData const formData = new FormData() if (reqMethod === 'post') { Object.keys(data).forEach((key) => { formData.append(key, data[key]) }) formData.append('key', name) formData.append('file', file) return this.request( { ...options, data: formData, method: reqMethod, }, this.restrictedMethods.includes('upload'), ) } return this.request( { ...options, method: 'put', headers, body: file, }, this.restrictedMethods.includes('upload'), ) } public async download(options: IRequestOptions): Promise<any> { try { const { data } = await this.get({ ...options, headers: {}, // 下载资源请求不经过service,header清空 responseType: 'blob', }) const url = window.URL.createObjectURL(new Blob([data])) const fileName = decodeURIComponent(new URL(options.url).pathname.split('/').pop() || '') const link = document.createElement('a') link.href = url link.setAttribute('download', fileName) link.style.display = 'none' document.body.appendChild(link) link.click() // 回收内存 window.URL.revokeObjectURL(url) document.body.removeChild(link) } catch (e) {} return new Promise((resolve) => { resolve({ statusCode: 200, tempFilePath: options.url, }) }) } async fetch(options: IFetchOptions & { shouldThrowOnError?: boolean }): Promise<ResponseObject> { const abortController = new AbortController() const { url, enableAbort = false, stream = false, signal, timeout: _timeout, shouldThrowOnError = true } = options const timeout = _timeout ?? this.timeout if (signal) { if (signal.aborted) abortController.abort() signal.addEventListener('abort', () => abortController.abort()) } let timer = null if (enableAbort && timeout) { timer = setTimeout(() => { console.warn(this.timeoutMsg) abortController.abort(new Error(this.timeoutMsg)) }, timeout) } const res = await fetch(url, { ...options, signal: abortController.signal, }) .then(async (response) => { clearTimeout(timer) if (shouldThrowOnError) { // 404 等等也会进 resolve,所以要再通过 ok 判断 return response.ok ? response : Promise.reject(await response.json()) } return response }) .catch((x) => { clearTimeout(timer) if (shouldThrowOnError) { return Promise.reject(x) } }) return { // eslint-disable-next-line no-nested-ternary data: stream ? res.body : res.headers.get('content-type')?.includes('application/json') ? res.json() : res.text(), statusCode: res.status, header: res.headers, } } /** * @param {IRequestOptions} options * @param {boolean} enableAbort 是否超时中断请求 */ protected request(options: IRequestOptions, enableAbort = false): Promise<ResponseObject> { const method = String(options.method).toLowerCase() || 'get' return new Promise((resolve) => { const { url, headers = {}, data, responseType, withCredentials, body, onUploadProgress } = options const realUrl = formatUrl(getProtocol(), url, method === 'get' ? data : {}) const ajax = new XMLHttpRequest() ajax.open(method, realUrl) responseType && (ajax.responseType = responseType) Object.keys(headers).forEach((key) => { ajax.setRequestHeader(key, headers[key]) }) let timer if (onUploadProgress) { ajax.upload.addEventListener('progress', onUploadProgress) } ajax.onreadystatechange = () => { const result: ResponseObject = {} if (ajax.readyState === 4) { const headers = ajax.getAllResponseHeaders() const arr = headers.trim().split(/[\r\n]+/) // Create a map of header names to values const headerMap = {} arr.forEach((line) => { const parts = line.split(': ') const header = parts.shift().toLowerCase() const value = parts.join(': ') headerMap[header] = value }) result.header = headerMap result.statusCode = ajax.status try { // 上传post请求返回数据格式为xml,此处容错 result.data = responseType === 'blob' ? ajax.response : JSON.parse(ajax.responseText) } catch (e) { result.data = responseType === 'blob' ? ajax.response : ajax.responseText } clearTimeout(timer) resolve(result) } } if (enableAbort && this.timeout) { timer = setTimeout(() => { console.warn(this.timeoutMsg) ajax.abort() }, this.timeout) } // 处理 payload let payload if (isFormData(data)) { // FormData,不处理 payload = data } else if (headers['content-type'] === 'application/x-www-form-urlencoded') { payload = toQueryString(data) } else if (body) { payload = body } else { // 其它情况 payload = data ? JSON.stringify(data) : undefined } if (withCredentials) { ajax.withCredentials = true } ajax.send(payload) }) } } function genAdapter() { const adapter: SDKAdapterInterface & { type?: 'default' | '' } = { type: 'default', root: window, reqClass: WebRequest, wsClass: WebSocket, localStorage, } return adapter } export { genAdapter, WebRequest }