diff --git a/packages/uni-api/src/index.ts b/packages/uni-api/src/index.ts index e0e9ca142c63c2a12654a042558721e4075253f7..d0dd7a82b98ba5f72a5b37afd64ba247b082c83b 100644 --- a/packages/uni-api/src/index.ts +++ b/packages/uni-api/src/index.ts @@ -26,6 +26,7 @@ export * from './protocols/media/getImageInfo' export * from './protocols/network/request' export * from './protocols/network/downloadFile' +export * from './protocols/network/uploadFile' export * from './protocols/route/route' diff --git a/packages/uni-api/src/protocols/network/upload-file.js b/packages/uni-api/src/protocols/network/upload-file.js deleted file mode 100644 index 2824c124a322b78726675e698f1a22707b9220be..0000000000000000000000000000000000000000 --- a/packages/uni-api/src/protocols/network/upload-file.js +++ /dev/null @@ -1,35 +0,0 @@ -// App端可以只使用files不传filePath和name -import getRealPath from 'uni-platform/helpers/get-real-path' - -export const uploadFile = { - url: { - type: String, - required: true - }, - files: { - type: Array - }, - filePath: { - type: String, - validator(value, params) { - if (value) { - params.type = getRealPath(value) - } - } - }, - name: { - type: String - }, - header: { - type: Object, - validator(value, params) { - params.header = value || {} - } - }, - formData: { - type: Object, - validator(value, params) { - params.formData = value || {} - } - } -} diff --git a/packages/uni-api/src/protocols/network/uploadFile.ts b/packages/uni-api/src/protocols/network/uploadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab2e6bcda2a2d875650f31339bee9eff88a0d059 --- /dev/null +++ b/packages/uni-api/src/protocols/network/uploadFile.ts @@ -0,0 +1,27 @@ +export const API_UPLOAD_FILE = 'uploadFile' +export type API_TYPE_UPLOAD_FILE = typeof uni.uploadFile +export const UploadFileOptions: ApiOptions = { + formatArgs: { + header(value, params) { + params.header = value || {} + }, + formData(value, params) { + params.formData = value || {} + }, + }, +} + +export const UploadFileProtocol: ApiProtocol = { + url: { + type: String, + required: true, + }, + files: { + type: Array, + }, + filePath: String, + name: String, + header: Object, + formData: Object, + timeout: Number, +} diff --git a/packages/uni-h5/dist/uni-h5.esm.js b/packages/uni-h5/dist/uni-h5.esm.js index 9c2c8f774ce3a28c74b68f353108cc4cdf44d9b3..87c2a786cfa308aa08324a98ead112c6ded3ee7b 100644 --- a/packages/uni-h5/dist/uni-h5.esm.js +++ b/packages/uni-h5/dist/uni-h5.esm.js @@ -4750,6 +4750,31 @@ const DownloadFileProtocol = { header: Object, timeout: Number }; +const API_UPLOAD_FILE = "uploadFile"; +const UploadFileOptions = { + formatArgs: { + header(value, params) { + params.header = value || {}; + }, + formData(value, params) { + params.formData = value || {}; + } + } +}; +const UploadFileProtocol = { + url: { + type: String, + required: true + }, + files: { + type: Array + }, + filePath: String, + name: String, + header: Object, + formData: Object, + timeout: Number +}; function encodeQueryString(url) { if (typeof url !== "string") { return url; @@ -10670,11 +10695,66 @@ function parseHeaders(headers) { return headersObject; } const files = {}; +function urlToFile(url, local) { + const file = files[url]; + if (file) { + return Promise.resolve(file); + } + if (/^data:[a-z-]+\/[a-z-]+;base64,/.test(url)) { + return Promise.resolve(base64ToFile(url)); + } + if (local) { + return Promise.reject(new Error("not find")); + } + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.responseType = "blob"; + xhr.onload = function() { + resolve(this.response); + }; + xhr.onerror = reject; + xhr.send(); + }); +} +function base64ToFile(base64) { + const base64Array = base64.split(","); + const res = base64Array[0].match(/:(.*?);/); + const type = res ? res[1] : ""; + const str = atob(base64Array[1]); + let n = str.length; + const array = new Uint8Array(n); + while (n--) { + array[n] = str.charCodeAt(n); + } + return blobToFile(array, type); +} +function getExtname(type) { + const extname = type.split("/")[1]; + return extname ? `.${extname}` : ""; +} function getFileName(url) { url = url.split("#")[0].split("?")[0]; const array = url.split("/"); return array[array.length - 1]; } +function blobToFile(blob, type) { + let file; + if (blob instanceof File) { + file = blob; + } else { + type = type || blob.type || ""; + const filename = `${Date.now()}${getExtname(type)}`; + try { + file = new File([blob], filename, {type}); + } catch (error) { + blob = blob instanceof Blob ? blob : new Blob([blob], {type}); + file = blob; + file.name = file.name || filename; + } + } + return file; +} function fileToUrl(file) { for (const key in files) { if (hasOwn$1(files, key)) { @@ -10718,11 +10798,7 @@ class DownloadTask { throw new Error("Method not implemented."); } } -const downloadFile = /* @__PURE__ */ defineTaskApi(API_DOWNLOAD_FILE, ({ - url, - header, - timeout = __uniConfig.networkTimeout.downloadFile -}, {resolve, reject}) => { +const downloadFile = /* @__PURE__ */ defineTaskApi(API_DOWNLOAD_FILE, ({url, header, timeout = __uniConfig.networkTimeout.downloadFile}, {resolve, reject}) => { var timer; var xhr = new XMLHttpRequest(); var downloadTask = new DownloadTask(xhr); @@ -10755,7 +10831,7 @@ const downloadFile = /* @__PURE__ */ defineTaskApi(API_DOWNLOAD_FILE, ({ }; xhr.onerror = function() { clearTimeout(timer); - reject("error"); + reject(); }; xhr.onprogress = function(event2) { downloadTask._callbacks.forEach((callback) => { @@ -10777,6 +10853,119 @@ const downloadFile = /* @__PURE__ */ defineTaskApi(API_DOWNLOAD_FILE, ({ }, timeout); return downloadTask; }, DownloadFileProtocol, DownloadFileOptions); +class UploadTask { + constructor(xhr) { + this._callbacks = []; + this._xhr = xhr; + } + onProgressUpdate(callback) { + if (typeof callback !== "function") { + return; + } + this._callbacks.push(callback); + } + offProgressUpdate(callback) { + const index2 = this._callbacks.indexOf(callback); + if (index2 >= 0) { + this._callbacks.splice(index2, 1); + } + } + abort() { + this._isAbort = true; + if (this._xhr) { + this._xhr.abort(); + delete this._xhr; + } + } + onHeadersReceived(callback) { + throw new Error("Method not implemented."); + } + offHeadersReceived(callback) { + throw new Error("Method not implemented."); + } +} +const uploadFile = /* @__PURE__ */ defineTaskApi(API_UPLOAD_FILE, ({ + url, + file, + filePath, + name, + files: files2, + header, + formData, + timeout = __uniConfig.networkTimeout.uploadFile +}, {resolve, reject}) => { + var uploadTask = new UploadTask(); + if (!Array.isArray(files2) || !files2.length) { + files2 = [ + { + name, + file, + uri: filePath + } + ]; + } + function upload(realFiles) { + var xhr = new XMLHttpRequest(); + var form = new FormData(); + var timer; + Object.keys(formData).forEach((key) => { + form.append(key, formData[key]); + }); + Object.values(files2).forEach(({name: name2}, index2) => { + const file2 = realFiles[index2]; + form.append(name2 || "file", file2, file2.name || `file-${Date.now()}`); + }); + xhr.open("POST", url); + Object.keys(header).forEach((key) => { + xhr.setRequestHeader(key, header[key]); + }); + xhr.upload.onprogress = function(event2) { + uploadTask._callbacks.forEach((callback) => { + var totalBytesSent = event2.loaded; + var totalBytesExpectedToSend = event2.total; + var progress = Math.round(totalBytesSent / totalBytesExpectedToSend * 100); + callback({ + progress, + totalBytesSent, + totalBytesExpectedToSend + }); + }); + }; + xhr.onerror = function() { + clearTimeout(timer); + reject(); + }; + xhr.onabort = function() { + clearTimeout(timer); + reject("abort"); + }; + xhr.onload = function() { + clearTimeout(timer); + const statusCode = xhr.status; + resolve({ + statusCode, + data: xhr.responseText || xhr.response + }); + }; + if (!uploadTask._isAbort) { + timer = setTimeout(function() { + xhr.upload.onprogress = xhr.onload = xhr.onabort = xhr.onerror = null; + uploadTask.abort(); + reject("timeout"); + }, timeout); + xhr.send(form); + uploadTask._xhr = xhr; + } else { + reject("abort"); + } + } + Promise.all(files2.map(({file: file2, uri}) => file2 instanceof Blob ? Promise.resolve(blobToFile(file2)) : urlToFile(uri))).then(upload).catch(() => { + setTimeout(() => { + reject("file error"); + }, 0); + }); + return uploadTask; +}, UploadFileProtocol, UploadFileOptions); const navigateBack = /* @__PURE__ */ defineAsyncApi(API_NAVIGATE_BACK, ({delta}, {resolve, reject}) => { let canBack = true; if (invokeHook("onBackPress") === true) { @@ -10982,6 +11171,7 @@ var api = /* @__PURE__ */ Object.freeze({ getImageInfo, request, downloadFile, + uploadFile, navigateBack, navigateTo, redirectTo, @@ -12039,4 +12229,4 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { ]); } _sfc_main.render = _sfc_render; -export {_sfc_main$1 as AsyncErrorComponent, _sfc_main as AsyncLoadingComponent, _sfc_main$n as Audio, index$5 as Button, _sfc_main$m as Canvas, _sfc_main$l as Checkbox, _sfc_main$k as CheckboxGroup, _sfc_main$j as Editor, index$6 as Form, index$4 as Icon, _sfc_main$h as Image, _sfc_main$g as Input, _sfc_main$f as Label, index$1 as LayoutComponent, _sfc_main$e as MovableView, _sfc_main$d as Navigator, index as PageComponent, _sfc_main$c as Progress, _sfc_main$b as Radio, _sfc_main$a as RadioGroup, _sfc_main$i as ResizeSensor, _sfc_main$9 as RichText, _sfc_main$8 as ScrollView, _sfc_main$7 as Slider, _sfc_main$6 as SwiperItem, _sfc_main$5 as Switch, index$3 as Text, _sfc_main$4 as Textarea, UniServiceJSBridge$1 as UniServiceJSBridge, UniViewJSBridge$1 as UniViewJSBridge, _sfc_main$3 as Video, index$2 as View, addInterceptor, arrayBufferToBase64, base64ToArrayBuffer, canIUse, createIntersectionObserver, createSelectorQuery, createVideoContext, cssBackdropFilter, cssConstant, cssEnv, cssVar, downloadFile, getApp$1 as getApp, getCurrentPages$1 as getCurrentPages, getImageInfo, getNetworkType, getSystemInfo, getSystemInfoSync, hideNavigationBarLoading, hideTabBar, hideTabBarRedDot, makePhoneCall, navigateBack, navigateTo, offNetworkStatusChange, onNetworkStatusChange, onTabBarMidButtonTap, openDocument, index$7 as plugin, promiseInterceptor, reLaunch, redirectTo, removeInterceptor, removeTabBarBadge, request, setNavigationBarColor, setNavigationBarTitle, setTabBarBadge, setTabBarItem, setTabBarStyle, showNavigationBarLoading, showTabBar, showTabBarRedDot, switchTab, uni$1 as uni, upx2px, useSubscribe}; +export {_sfc_main$1 as AsyncErrorComponent, _sfc_main as AsyncLoadingComponent, _sfc_main$n as Audio, index$5 as Button, _sfc_main$m as Canvas, _sfc_main$l as Checkbox, _sfc_main$k as CheckboxGroup, _sfc_main$j as Editor, index$6 as Form, index$4 as Icon, _sfc_main$h as Image, _sfc_main$g as Input, _sfc_main$f as Label, index$1 as LayoutComponent, _sfc_main$e as MovableView, _sfc_main$d as Navigator, index as PageComponent, _sfc_main$c as Progress, _sfc_main$b as Radio, _sfc_main$a as RadioGroup, _sfc_main$i as ResizeSensor, _sfc_main$9 as RichText, _sfc_main$8 as ScrollView, _sfc_main$7 as Slider, _sfc_main$6 as SwiperItem, _sfc_main$5 as Switch, index$3 as Text, _sfc_main$4 as Textarea, UniServiceJSBridge$1 as UniServiceJSBridge, UniViewJSBridge$1 as UniViewJSBridge, _sfc_main$3 as Video, index$2 as View, addInterceptor, arrayBufferToBase64, base64ToArrayBuffer, canIUse, createIntersectionObserver, createSelectorQuery, createVideoContext, cssBackdropFilter, cssConstant, cssEnv, cssVar, downloadFile, getApp$1 as getApp, getCurrentPages$1 as getCurrentPages, getImageInfo, getNetworkType, getSystemInfo, getSystemInfoSync, hideNavigationBarLoading, hideTabBar, hideTabBarRedDot, makePhoneCall, navigateBack, navigateTo, offNetworkStatusChange, onNetworkStatusChange, onTabBarMidButtonTap, openDocument, index$7 as plugin, promiseInterceptor, reLaunch, redirectTo, removeInterceptor, removeTabBarBadge, request, setNavigationBarColor, setNavigationBarTitle, setTabBarBadge, setTabBarItem, setTabBarStyle, showNavigationBarLoading, showTabBar, showTabBarRedDot, switchTab, uni$1 as uni, uploadFile, upx2px, useSubscribe}; diff --git a/packages/uni-h5/src/helpers/file.ts b/packages/uni-h5/src/helpers/file.ts index d563b9468128746c97b08617726cac6debd8c505..66789e1d88421be7a83e17f2f88c08bb87119d36 100644 --- a/packages/uni-h5/src/helpers/file.ts +++ b/packages/uni-h5/src/helpers/file.ts @@ -76,7 +76,10 @@ interface FileLike extends Blob { * @param {string} type * @return {File} */ -export function blobToFile(blob: Blob | Uint8Array | File, type: string): File { +export function blobToFile( + blob: Blob | Uint8Array | File, + type?: string +): File { let file: File | FileLike if (blob instanceof File) { file = blob diff --git a/packages/uni-h5/src/service/api/index.ts b/packages/uni-h5/src/service/api/index.ts index cc8f21bcd4ee2f412d5de32d2d4a47e323e3df46..ce18014c09490251b275f5f5218fbca7a71f4487 100644 --- a/packages/uni-h5/src/service/api/index.ts +++ b/packages/uni-h5/src/service/api/index.ts @@ -11,6 +11,7 @@ export * from './media/getImageInfo' export * from './network/request' export * from './network/downloadFile' +export * from './network/uploadFile' export * from './route/navigateBack' export * from './route/navigateTo' diff --git a/packages/uni-h5/src/service/api/network/uploadFile.ts b/packages/uni-h5/src/service/api/network/uploadFile.ts new file mode 100644 index 0000000000000000000000000000000000000000..99bb9beceaf48e28ff373ae38766b6acfe23990a --- /dev/null +++ b/packages/uni-h5/src/service/api/network/uploadFile.ts @@ -0,0 +1,157 @@ +import { urlToFile, blobToFile } from '../../../helpers/file' +import { + defineTaskApi, + API_UPLOAD_FILE, + API_TYPE_UPLOAD_FILE, + UploadFileProtocol, + UploadFileOptions, +} from '@dcloudio/uni-api' +/** + * 上传任务 + */ +class UploadTask implements UniApp.UploadTask { + _xhr?: XMLHttpRequest + _isAbort?: boolean + _callbacks: Function[] = [] + constructor(xhr?: XMLHttpRequest) { + this._xhr = xhr + } + + /** + * 监听上传进度 + * @param callback 回调 + */ + onProgressUpdate(callback: (result: any) => void) { + if (typeof callback !== 'function') { + return + } + this._callbacks.push(callback) + } + + offProgressUpdate(callback: (result: any) => void) { + const index = this._callbacks.indexOf(callback) + if (index >= 0) { + this._callbacks.splice(index, 1) + } + } + + /** + * 中断上传任务 + */ + abort() { + this._isAbort = true + if (this._xhr) { + this._xhr.abort() + delete this._xhr + } + } + onHeadersReceived(callback: (result: any) => void): void { + throw new Error('Method not implemented.') + } + offHeadersReceived(callback: (result: any) => void): void { + throw new Error('Method not implemented.') + } +} + +export const uploadFile = defineTaskApi( + API_UPLOAD_FILE, + ( + { + url, + file, + filePath, + name, + files, + header, + formData, + timeout = __uniConfig.networkTimeout.uploadFile, + }, + { resolve, reject } + ) => { + var uploadTask = new UploadTask() + if (!Array.isArray(files) || !files.length) { + files = [ + { + name, + file, + uri: filePath, + }, + ] + } + function upload(realFiles: File[]) { + var xhr = new XMLHttpRequest() + var form = new FormData() + var timer: number + Object.keys(formData).forEach((key) => { + form.append(key, formData[key]) + }) + Object.values(files).forEach(({ name }, index) => { + const file = realFiles[index] + form.append(name || 'file', file, file.name || `file-${Date.now()}`) + }) + xhr.open('POST', url) + Object.keys(header).forEach((key) => { + xhr.setRequestHeader(key, header[key]) + }) + xhr.upload.onprogress = function (event) { + uploadTask._callbacks.forEach((callback) => { + var totalBytesSent = event.loaded + var totalBytesExpectedToSend = event.total + var progress = Math.round( + (totalBytesSent / totalBytesExpectedToSend) * 100 + ) + callback({ + progress, + totalBytesSent, + totalBytesExpectedToSend, + }) + }) + } + xhr.onerror = function () { + clearTimeout(timer) + reject() + } + xhr.onabort = function () { + clearTimeout(timer) + reject('abort') + } + xhr.onload = function () { + clearTimeout(timer) + const statusCode = xhr.status + resolve({ + statusCode, + data: xhr.responseText || xhr.response, + }) + } + if (!uploadTask._isAbort) { + timer = setTimeout(function () { + xhr.upload.onprogress = xhr.onload = xhr.onabort = xhr.onerror = null + uploadTask.abort() + reject('timeout') + }, timeout) + xhr.send(form) + uploadTask._xhr = xhr + } else { + reject('abort') + } + } + + Promise.all( + files.map(({ file, uri }) => + file instanceof Blob + ? Promise.resolve(blobToFile(file)) + : urlToFile(uri) + ) + ) + .then(upload) + .catch(() => { + setTimeout(() => { + reject('file error') + }, 0) + }) + + return uploadTask + }, + UploadFileProtocol, + UploadFileOptions +)