From cb42754124133c4f517334e472c5fed50772dfad Mon Sep 17 00:00:00 2001 From: DCloud_LXH <283700113@qq.com> Date: Wed, 23 Jun 2021 15:59:54 +0800 Subject: [PATCH] feat(App): request --- .../src/service/api/base/index.ts | 1 + .../service/api/base/requireNativePlugin.ts | 11 + .../uni-app-plus/src/service/api/index.ts | 1 + .../src/service/api/network/request.ts | 370 ++++++++++++++++++ 4 files changed, 383 insertions(+) create mode 100644 packages/uni-app-plus/src/service/api/base/index.ts create mode 100644 packages/uni-app-plus/src/service/api/base/requireNativePlugin.ts create mode 100644 packages/uni-app-plus/src/service/api/network/request.ts diff --git a/packages/uni-app-plus/src/service/api/base/index.ts b/packages/uni-app-plus/src/service/api/base/index.ts new file mode 100644 index 000000000..f60f76719 --- /dev/null +++ b/packages/uni-app-plus/src/service/api/base/index.ts @@ -0,0 +1 @@ +export * from './requireNativePlugin' diff --git a/packages/uni-app-plus/src/service/api/base/requireNativePlugin.ts b/packages/uni-app-plus/src/service/api/base/requireNativePlugin.ts new file mode 100644 index 000000000..fdca59d1e --- /dev/null +++ b/packages/uni-app-plus/src/service/api/base/requireNativePlugin.ts @@ -0,0 +1,11 @@ +declare const weex: any +declare const __requireNativePlugin__: any + +export function requireNativePlugin(pluginName: string) { + /* eslint-disable no-undef */ + if (typeof weex !== 'undefined') { + return weex.requireModule(pluginName) + } + /* eslint-disable no-undef */ + return __requireNativePlugin__(pluginName) +} diff --git a/packages/uni-app-plus/src/service/api/index.ts b/packages/uni-app-plus/src/service/api/index.ts index dff7efd07..92a255b59 100644 --- a/packages/uni-app-plus/src/service/api/index.ts +++ b/packages/uni-app-plus/src/service/api/index.ts @@ -11,6 +11,7 @@ export * from './media/getVideoInfo' export * from './keyboard/keyboard' export * from './network/downloadFile' +export * from './network/request' export * from './context/createInnerAudioContext' diff --git a/packages/uni-app-plus/src/service/api/network/request.ts b/packages/uni-app-plus/src/service/api/network/request.ts new file mode 100644 index 000000000..7b7390e3e --- /dev/null +++ b/packages/uni-app-plus/src/service/api/network/request.ts @@ -0,0 +1,370 @@ +import { hasOwn, isPlainObject } from '@vue/shared' +import { + API_REQUEST, + API_TYPE_REQUEST, + defineTaskApi, + RequestOptions, + RequestProtocol, +} from '@dcloudio/uni-api' +import { requireNativePlugin } from '../base' +import { base64ToArrayBuffer } from '@dcloudio/uni-api' + +type Type = 'base64' | 'text' +type Headers = Record +type Options = UniApp.RequestOptions & { + tls: any + headers: Headers + type: Type + body?: string | Data +} +interface RequestTasks { + abort: Function +} +type RequestTaskState = { + requestTaskId: number + state: 'success' | 'fail' + statusCode: number + data?: string | ArrayBuffer + header?: any + errMsg?: string + cookies?: string[] +} +type requestTaskOptions = { + args: UniApp.RequestOptions + resolve: Function + reject: Function +} + +let requestTaskId = 0 +const requestTasks: Record = {} +const requestTaskOptions: Record = {} + +const publishStateChange = (res: RequestTaskState) => { + onRequestTaskStateChange(res) + delete requestTasks[requestTaskId] +} + +const cookiesParse = (header: Record) => { + let cookiesStr = header['Set-Cookie'] || header['set-cookie'] + let cookiesArr = [] + if (!cookiesStr) { + return [] + } + if (cookiesStr[0] === '[' && cookiesStr[cookiesStr.length - 1] === ']') { + cookiesStr = cookiesStr.slice(1, -1) + } + const handleCookiesArr = cookiesStr.split(';') + for (let i = 0; i < handleCookiesArr.length; i++) { + if ( + handleCookiesArr[i].indexOf('Expires=') !== -1 || + handleCookiesArr[i].indexOf('expires=') !== -1 + ) { + cookiesArr.push(handleCookiesArr[i].replace(',', '')) + } else { + cookiesArr.push(handleCookiesArr[i]) + } + } + cookiesArr = cookiesArr.join(';').split(',') + + return cookiesArr +} + +function createRequestTaskById( + requestTaskId: number, + { + url, + data, + header, + method = 'GET', + responseType, + sslVerify = true, + firstIpv4 = false, + // @ts-ignore + tls, + timeout = (__uniConfig.networkTimeout && + __uniConfig.networkTimeout.request) || + 60 * 1000, + }: UniApp.RequestOptions +) { + const stream = requireNativePlugin('stream') + const headers: Headers = {} + + let abortTimeout: number + let aborted: boolean + let hasContentType = false + for (const name in header) { + if (!hasContentType && name.toLowerCase() === 'content-type') { + hasContentType = true + headers['Content-Type'] = header[name] + // TODO 需要重构 + if ( + method !== 'GET' && + header[name].indexOf('application/x-www-form-urlencoded') === 0 && + typeof data !== 'string' && + !(data instanceof ArrayBuffer) + ) { + const bodyArray = [] + for (const key in data) { + if (hasOwn(data, key)) { + bodyArray.push( + encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) + ) + } + } + data = bodyArray.join('&') + } + } else { + headers[name] = header[name] + } + } + + if (!hasContentType && method === 'POST') { + headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' + } + + if (timeout) { + abortTimeout = setTimeout(() => { + aborted = true + publishStateChange({ + requestTaskId, + state: 'fail', + statusCode: 0, + errMsg: 'timeout', + }) + }, timeout + 200) // TODO +200 发消息到原生层有时间开销,以后考虑由原生层回调超时 + } + const options: Options = { + method, + url: url.trim(), + // weex 官方文档有误,headers 类型实际 object,用 string 类型会无响应 + headers, + type: responseType === 'arraybuffer' ? 'base64' : 'text', + // weex 官方文档未说明实际支持 timeout,单位:ms + timeout: timeout || 6e5, + // 配置和weex模块内相反 + sslVerify: !sslVerify, + firstIpv4: firstIpv4, + tls, + } + if (method !== 'GET') { + options.body = typeof data === 'string' ? data : JSON.stringify(data) + } + try { + stream.fetch( + options, + ({ + ok, + status, + data, + headers, + errorMsg, + }: { + ok: boolean + status: number + data: string + headers: Headers + errorMsg: string + }) => { + if (aborted) { + return + } + if (abortTimeout) { + clearTimeout(abortTimeout) + } + const statusCode = status + if (statusCode > 0) { + publishStateChange({ + requestTaskId, + state: 'success', + data: + ok && responseType === 'arraybuffer' + ? base64ToArrayBuffer(data) + : data, + statusCode, + header: headers, + cookies: cookiesParse(headers), + }) + } else { + let errMsg = 'abort statusCode:' + statusCode + if (errorMsg) { + errMsg = errMsg + ' ' + errorMsg + } + publishStateChange({ + requestTaskId, + state: 'fail', + statusCode, + errMsg, + }) + } + } + ) + requestTasks[requestTaskId] = { + abort() { + aborted = true + if (abortTimeout) { + clearTimeout(abortTimeout) + } + publishStateChange({ + requestTaskId, + state: 'fail', + statusCode: 0, + errMsg: 'abort', + }) + }, + } + } catch (e) { + return { + requestTaskId, + errMsg: 'createRequestTask:fail', + } + } + return { + requestTaskId, + errMsg: 'createRequestTask:ok', + } +} + +function createRequestTask(args: UniApp.RequestOptions) { + return createRequestTaskById(++requestTaskId, args) +} + +function operateRequestTask({ + requestTaskId, + operationType, +}: { + requestTaskId: number + operationType: string +}) { + const requestTask = requestTasks[requestTaskId] + if (requestTask && operationType === 'abort') { + requestTask.abort() + return { + errMsg: 'operateRequestTask:ok', + } + } + return { + errMsg: 'operateRequestTask:fail', + } +} + +function formatResponse( + res: Omit, + args: UniApp.RequestOptions +) { + if (typeof res.data === 'string' && res.data.charCodeAt(0) === 65279) { + res.data = res.data.substr(1) + } + + res.statusCode = parseInt(String(res.statusCode), 10) + + if (isPlainObject(res.header)) { + res.header = Object.keys(res.header).reduce(function (ret, key) { + const value = res.header[key] + if (Array.isArray(value)) { + ;(ret as any)[key] = value.join(',') + } else if (typeof value === 'string') { + ;(ret as any)[key] = value + } + return ret + }, {}) + } + + if (args.dataType && args.dataType.toLowerCase() === 'json') { + try { + res.data = JSON.parse(res.data as string) + } catch (e) {} + } + + return res +} + +function onRequestTaskStateChange({ + requestTaskId, + state, + data, + statusCode, + header, + errMsg, + cookies, +}: RequestTaskState) { + const { args, resolve, reject } = requestTaskOptions[requestTaskId] || {} + + if (!args) { + return + } + + delete requestTaskOptions[requestTaskId] + + switch (state) { + case 'success': + resolve( + formatResponse( + { + data, + statusCode, + header, + errMsg: 'request:ok', + cookies, + }, + args + ) + ) + break + case 'fail': + reject(errMsg) + break + } +} + +/** + * 请求任务类 + */ +class RequestTask implements UniApp.RequestTask { + id: number + + constructor(id: number) { + this.id = id + } + + abort() { + operateRequestTask({ + requestTaskId: this.id, + operationType: 'abort', + }) + } + + offHeadersReceived() {} + + onHeadersReceived() {} +} + +export const request = defineTaskApi( + API_REQUEST, + (args, { resolve, reject }) => { + let contentType + for (const name in args.header) { + if (name.toLowerCase() === 'content-type') { + contentType = args.header[name] + break + } + } + if ( + args.method !== 'GET' && + contentType.indexOf('application/json') === 0 && + isPlainObject(args.data) + ) { + args.data = JSON.stringify(args.data) + } + const { requestTaskId } = createRequestTask(args) + + requestTaskOptions[requestTaskId] = { + args, + resolve, + reject, + } + + return new RequestTask(requestTaskId) + }, + RequestProtocol, + RequestOptions +) -- GitLab