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>
|