@cloudbase/utilities
Version:
cloudbase javascript sdk utilities
242 lines (230 loc) • 7.48 kB
text/typescript
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 }