361 lines
15 KiB
Plaintext
361 lines
15 KiB
Plaintext
|
|
import http from '@ohos.net.http'
|
|||
|
|
import buffer from '@ohos.buffer';
|
|||
|
|
import {
|
|||
|
|
Emitter,
|
|||
|
|
getCurrentMP,
|
|||
|
|
} from '@dcloudio/uni-runtime'
|
|||
|
|
import {
|
|||
|
|
RequestTask as UniRequestTask,
|
|||
|
|
RequestOptions as UniRequestOptions,
|
|||
|
|
RequestSuccess as UniRequestSuccess,
|
|||
|
|
Request,
|
|||
|
|
RequestTaskOnChunkReceivedCallback,
|
|||
|
|
RequestTaskOnChunkReceivedListenerResult,
|
|||
|
|
RequestTaskOnHeadersReceivedCallback,
|
|||
|
|
RequestTaskOnHeadersReceivedListenerResult,
|
|||
|
|
} from '../../interface.uts'
|
|||
|
|
import {
|
|||
|
|
API_REQUEST,
|
|||
|
|
RequestApiOptions,
|
|||
|
|
RequestApiProtocol,
|
|||
|
|
} from '../../protocol.uts'
|
|||
|
|
import {
|
|||
|
|
getClientCertificate,
|
|||
|
|
parseUrl,
|
|||
|
|
} from './utils.uts'
|
|||
|
|
import {
|
|||
|
|
getCookieSync,
|
|||
|
|
setCookieSync,
|
|||
|
|
} from './cookie.uts'
|
|||
|
|
|
|||
|
|
interface IUniRequestEmitter {
|
|||
|
|
on: (eventName: string, callback: Function) => void
|
|||
|
|
once: (eventName: string, callback: Function) => void
|
|||
|
|
off: (eventName: string, callback?: Function | null) => void
|
|||
|
|
emit: (eventName: string, ...args: (Object | undefined | null)[]) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface IRequestTask {
|
|||
|
|
abort: Function
|
|||
|
|
onChunkReceived: (listener: RequestTaskOnChunkReceivedCallback) => void
|
|||
|
|
offChunkReceived: (listener?: RequestTaskOnChunkReceivedCallback | null) => void
|
|||
|
|
onHeadersReceived: (listener: RequestTaskOnHeadersReceivedCallback) => void
|
|||
|
|
offHeadersReceived: (listener?: RequestTaskOnHeadersReceivedCallback | null) => void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class RequestTask implements UniRequestTask {
|
|||
|
|
__v_skip: boolean = true
|
|||
|
|
private _requestTask: IRequestTask
|
|||
|
|
private _requestOnChunkReceiveCallbackId: number = 0
|
|||
|
|
private _requestOnChunkReceiveCallbacks: Map<number, RequestTaskOnChunkReceivedCallback> = new Map()
|
|||
|
|
private _requestOnHeadersReceiveCallbackId: number = 0
|
|||
|
|
private _requestOnHeadersReceiveCallbacks: Map<number, RequestTaskOnHeadersReceivedCallback> = new Map()
|
|||
|
|
|
|||
|
|
constructor(requestTask: IRequestTask) {
|
|||
|
|
this._requestTask = requestTask
|
|||
|
|
}
|
|||
|
|
abort() {
|
|||
|
|
this._requestTask.abort()
|
|||
|
|
}
|
|||
|
|
onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) {
|
|||
|
|
this._requestTask.onChunkReceived(callback)
|
|||
|
|
this._requestOnChunkReceiveCallbackId++
|
|||
|
|
this._requestOnChunkReceiveCallbacks.set(this._requestOnChunkReceiveCallbackId, callback)
|
|||
|
|
return this._requestOnChunkReceiveCallbackId
|
|||
|
|
}
|
|||
|
|
offChunkReceived(callbackId?: RequestTaskOnChunkReceivedCallback | number | null) {
|
|||
|
|
if (callbackId === undefined || callbackId === null) {
|
|||
|
|
this._requestTask.offChunkReceived()
|
|||
|
|
this._requestOnChunkReceiveCallbacks.clear()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (typeof callbackId === 'function') {
|
|||
|
|
this._requestOnChunkReceiveCallbacks.forEach((callback, id) => {
|
|||
|
|
if (callback === callbackId) {
|
|||
|
|
this._requestOnChunkReceiveCallbacks.delete(id)
|
|||
|
|
this._requestTask.offChunkReceived(callback)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const callback = this._requestOnChunkReceiveCallbacks.get(callbackId)
|
|||
|
|
if (!callback) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this._requestOnChunkReceiveCallbacks.delete(callbackId)
|
|||
|
|
this._requestTask.offChunkReceived(callback)
|
|||
|
|
}
|
|||
|
|
onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) {
|
|||
|
|
this._requestTask.onHeadersReceived(callback)
|
|||
|
|
this._requestOnHeadersReceiveCallbackId++
|
|||
|
|
this._requestOnHeadersReceiveCallbacks.set(this._requestOnHeadersReceiveCallbackId, callback)
|
|||
|
|
return this._requestOnHeadersReceiveCallbackId
|
|||
|
|
}
|
|||
|
|
offHeadersReceived(callbackId?: RequestTaskOnHeadersReceivedCallback | number | null) {
|
|||
|
|
if (callbackId === undefined || callbackId === null) {
|
|||
|
|
this._requestTask.offHeadersReceived()
|
|||
|
|
this._requestOnHeadersReceiveCallbacks.clear()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if (typeof callbackId === 'function') {
|
|||
|
|
this._requestOnHeadersReceiveCallbacks.forEach((callback, id) => {
|
|||
|
|
if (callback === callbackId) {
|
|||
|
|
this._requestOnHeadersReceiveCallbacks.delete(id)
|
|||
|
|
this._requestTask.offHeadersReceived(callback)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const callback = this._requestOnHeadersReceiveCallbacks.get(callbackId)
|
|||
|
|
if (!callback) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
this._requestOnHeadersReceiveCallbacks.delete(callbackId)
|
|||
|
|
this._requestTask.offHeadersReceived(callback)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* http.request有5MB响应体大小限制
|
|||
|
|
* rcp.Session.fetch无响应体大小限制,但是headers、cookies处理不够完善,另外rcp需要从@hms引用,不是openharmony标准库
|
|||
|
|
* 为突破http.request的限制,现在使用http.requestInStream
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
export const request = defineTaskApi<UniRequestOptions<Object>, UniRequestSuccess<Object>, UniRequestTask>(
|
|||
|
|
API_REQUEST,
|
|||
|
|
(args: UniRequestOptions<Object>, exec: ApiExecutor<UniRequestSuccess<Object>>) => {
|
|||
|
|
let { header, method, dataType, timeout, url, responseType } = args
|
|||
|
|
let data: ESObject = args.data
|
|||
|
|
header = header || {} as ESObject
|
|||
|
|
if (!header!['Cookie'] && !header!['cookie']) {
|
|||
|
|
header!['Cookie'] = getCookieSync(url);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let contentType = ''
|
|||
|
|
|
|||
|
|
// header
|
|||
|
|
const headers = {} as Record<string, Object>
|
|||
|
|
const headerRecord = header as Object as Record<string, string>
|
|||
|
|
const headerKeys = Object.keys(headerRecord)
|
|||
|
|
for (let i = 0; i < headerKeys.length; i++) {
|
|||
|
|
const name = headerKeys[i];
|
|||
|
|
if (name.toLowerCase() === 'content-type') {
|
|||
|
|
contentType = headerRecord[name] as string
|
|||
|
|
}
|
|||
|
|
headers[name.toLowerCase()] = headerRecord[name]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!contentType && method === 'POST') {
|
|||
|
|
headers['Content-Type'] = 'application/json'
|
|||
|
|
contentType = 'application/json'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// url data
|
|||
|
|
if (method === 'GET' && data && typeof data === 'object') {
|
|||
|
|
const dataRecord = data as Record<string, Object>
|
|||
|
|
const query = Object.keys(dataRecord)
|
|||
|
|
.map((key) => {
|
|||
|
|
return (
|
|||
|
|
encodeURIComponent(key) +
|
|||
|
|
'=' +
|
|||
|
|
encodeURIComponent(dataRecord[key] as string | number | boolean)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
.join('&')
|
|||
|
|
url += query ?
|
|||
|
|
(url.indexOf('?') > -1 ? '&' : '?') + query :
|
|||
|
|
''
|
|||
|
|
data = null
|
|||
|
|
} else if (
|
|||
|
|
method !== 'GET' &&
|
|||
|
|
contentType &&
|
|||
|
|
contentType.indexOf('application/json') === 0 &&
|
|||
|
|
data &&
|
|||
|
|
typeof data !== 'string'
|
|||
|
|
) {
|
|||
|
|
data = JSON.stringify(data)
|
|||
|
|
} else if (
|
|||
|
|
method !== 'GET' &&
|
|||
|
|
contentType &&
|
|||
|
|
contentType.indexOf('application/x-www-form-urlencoded') === 0 &&
|
|||
|
|
data &&
|
|||
|
|
typeof data === 'object'
|
|||
|
|
) {
|
|||
|
|
const dataRecord = data as Record<string, Object>
|
|||
|
|
data = Object.keys(dataRecord)
|
|||
|
|
.map((key) => {
|
|||
|
|
return (
|
|||
|
|
encodeURIComponent(key) +
|
|||
|
|
'=' +
|
|||
|
|
encodeURIComponent(dataRecord[key] as number | string | boolean)
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
.join('&')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const httpRequest = http.createHttp()
|
|||
|
|
const mp = getCurrentMP()
|
|||
|
|
const userAgent = mp.userAgent.fullUserAgent
|
|||
|
|
if (userAgent && headers && !headers!['User-Agent'] && !headers!['user-agent']) {
|
|||
|
|
headers!['User-Agent'] = userAgent
|
|||
|
|
}
|
|||
|
|
const emitter = new Emitter() as IUniRequestEmitter
|
|||
|
|
const requestTask: IRequestTask = {
|
|||
|
|
abort() {
|
|||
|
|
emitter.off('headersReceive')
|
|||
|
|
httpRequest.destroy()
|
|||
|
|
},
|
|||
|
|
onHeadersReceived(callback: RequestTaskOnHeadersReceivedCallback) {
|
|||
|
|
emitter.on('headersReceive', callback)
|
|||
|
|
},
|
|||
|
|
offHeadersReceived(callback?: RequestTaskOnHeadersReceivedCallback | null) {
|
|||
|
|
emitter.off('headersReceive', callback || undefined)
|
|||
|
|
},
|
|||
|
|
onChunkReceived(callback: RequestTaskOnChunkReceivedCallback) {
|
|||
|
|
emitter.on('dataReceive', callback)
|
|||
|
|
},
|
|||
|
|
offChunkReceived(callback?: RequestTaskOnChunkReceivedCallback | null) {
|
|||
|
|
emitter.off('dataReceive', callback || undefined)
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
const realRequestTask = new RequestTask(requestTask)
|
|||
|
|
|
|||
|
|
function destroy() {
|
|||
|
|
emitter.off('headersReceive')
|
|||
|
|
httpRequest.destroy()
|
|||
|
|
}
|
|||
|
|
mp.on('beforeClose', destroy)
|
|||
|
|
|
|||
|
|
let latestHeaders: Object | null = null
|
|||
|
|
let lastUrl = url
|
|||
|
|
httpRequest.on('headersReceive', (headers: Object) => {
|
|||
|
|
const realHeaders = headers as Record<string, string | string[]>
|
|||
|
|
const setCookieHeader = realHeaders['set-cookie'] || realHeaders['Set-Cookie']
|
|||
|
|
if (setCookieHeader) {
|
|||
|
|
setCookieSync(lastUrl, setCookieHeader as string[])
|
|||
|
|
}
|
|||
|
|
latestHeaders = headers
|
|||
|
|
const location = realHeaders['location'] || realHeaders['Location']
|
|||
|
|
if (location) {
|
|||
|
|
lastUrl = location as string
|
|||
|
|
}
|
|||
|
|
// TODO headersReceive存在bug,暂不支持回调给用户。
|
|||
|
|
// emitter.emit('headersReceive', headers);
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
let headersReceiveTriggered = false
|
|||
|
|
const bufs = [] as buffer.Buffer[]
|
|||
|
|
httpRequest.on('dataReceive', (data) => {
|
|||
|
|
if (!headersReceiveTriggered) {
|
|||
|
|
headersReceiveTriggered = true
|
|||
|
|
// 注意重定向时会多次触发,但是只需要给用户回调最后一次headers
|
|||
|
|
const headers = {} as UTSJSONObject
|
|||
|
|
const realHeaders = latestHeaders as Record<string, string | string[]>
|
|||
|
|
let cookies = [] as string[]
|
|||
|
|
// 有用户反馈部分情况下headers会为null,无法复现,增加判断逻辑容错
|
|||
|
|
if (realHeaders) {
|
|||
|
|
const keys = Object.keys(realHeaders)
|
|||
|
|
for (let i = 0; i < keys.length; i++) {
|
|||
|
|
const key = keys[i]
|
|||
|
|
if (key === 'set-cookie' || key === 'Set-Cookie') {
|
|||
|
|
const setCookieHeader = realHeaders[key] as string[];
|
|||
|
|
// TODO 待确认为什么鸿蒙会返回重复的Set-Cookie,不合常理。使用 https://httpbin.org/cookies/set/nnn/123 这个接口测试
|
|||
|
|
for (let i = setCookieHeader.length - 1; i >= 0; i--) {
|
|||
|
|
const cookie = setCookieHeader[i]
|
|||
|
|
if (setCookieHeader.indexOf(cookie) !== i) {
|
|||
|
|
setCookieHeader.splice(i, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
cookies = setCookieHeader as string[];
|
|||
|
|
headers[key] = Array.isArray(setCookieHeader) ? setCookieHeader.join(',') : setCookieHeader;
|
|||
|
|
} else {
|
|||
|
|
headers[key] = realHeaders[key];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
emitter.emit('headersReceive', {
|
|||
|
|
header: headers,
|
|||
|
|
// 鸿蒙不支持在headersReceive时机获取到状态码
|
|||
|
|
statusCode: NaN,
|
|||
|
|
cookies: cookies,
|
|||
|
|
} as RequestTaskOnHeadersReceivedListenerResult);
|
|||
|
|
}
|
|||
|
|
bufs.push(buffer.from(data))
|
|||
|
|
if (args.enableChunked) {
|
|||
|
|
emitter.emit('dataReceive', {
|
|||
|
|
data: data
|
|||
|
|
} as RequestTaskOnChunkReceivedListenerResult)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
httpRequest.requestInStream(
|
|||
|
|
parseUrl(url),
|
|||
|
|
{
|
|||
|
|
header: headers,
|
|||
|
|
method: (method || 'GET').toUpperCase() as http.RequestMethod, // 仅OPTIONS不支持
|
|||
|
|
extraData: data || undefined, // 传空字符串会报错
|
|||
|
|
connectTimeout: timeout ? timeout : undefined, // 不支持仅设置一个timeout
|
|||
|
|
readTimeout: timeout ? timeout : undefined,
|
|||
|
|
clientCert: getClientCertificate(url)
|
|||
|
|
} as http.HttpRequestOptions,
|
|||
|
|
(err, statusCode) => {
|
|||
|
|
if (err) {
|
|||
|
|
/**
|
|||
|
|
* TODO abort后此处收到如下错误,待确认是否直接将此错误码转为abort错误
|
|||
|
|
* {"code":2300023,"message":"Failed writing received data to disk/application"}
|
|||
|
|
*
|
|||
|
|
* reject方法第二个参数可以传errCode,reject(err.message, { errCode: -1 })
|
|||
|
|
*/
|
|||
|
|
exec.reject(err.message)
|
|||
|
|
} else {
|
|||
|
|
const responseData = buffer.concat(bufs)
|
|||
|
|
let data: ArrayBuffer | string | object = ''
|
|||
|
|
if (responseType === 'arraybuffer') {
|
|||
|
|
data = responseData.buffer
|
|||
|
|
} else {
|
|||
|
|
data = responseData.toString('utf8')
|
|||
|
|
if (dataType === 'json') {
|
|||
|
|
try {
|
|||
|
|
// #ifdef UNI-APP-X
|
|||
|
|
data = globalThis.UTS.JSON.parse(data) || data;
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef UNI-APP-X
|
|||
|
|
data = JSON.parse(data as string);
|
|||
|
|
// #endif
|
|||
|
|
} catch (e) {
|
|||
|
|
// 与微信保持一致,不抛出异常
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const headers = latestHeaders as Record<string, string | string[]>
|
|||
|
|
// 有用户反馈部分情况下headers会为null,无法复现,增加判断逻辑容错
|
|||
|
|
const oldCookies = headers ? (headers['Set-Cookie'] || headers['set-cookie'] || []) as string[] : [] as string[]
|
|||
|
|
let newCookies = oldCookies.join(',')
|
|||
|
|
if (newCookies) {
|
|||
|
|
if (headers['Set-Cookie']) {
|
|||
|
|
headers['Set-Cookie'] = newCookies
|
|||
|
|
} else {
|
|||
|
|
headers['set-cookie'] = newCookies
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
exec.resolve({
|
|||
|
|
data,
|
|||
|
|
statusCode,
|
|||
|
|
header: latestHeaders!,
|
|||
|
|
cookies: oldCookies,
|
|||
|
|
} as UniRequestSuccess<Object>)
|
|||
|
|
}
|
|||
|
|
realRequestTask.offChunkReceived()
|
|||
|
|
realRequestTask.offHeadersReceived()
|
|||
|
|
httpRequest.destroy() // 调用完毕后必须调用destroy方法
|
|||
|
|
mp.off('beforeClose', destroy)
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
return realRequestTask
|
|||
|
|
},
|
|||
|
|
RequestApiProtocol,
|
|||
|
|
RequestApiOptions
|
|||
|
|
) as Request<Object>
|