提交 8ec8b19c 编写于 作者: Q qiang

feat(h5): downloadFile

上级 3971ef83
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dcloudio/types": "^2.0.27", "@dcloudio/types": "^2.0.28",
"@microsoft/api-extractor": "^7.13.2", "@microsoft/api-extractor": "^7.13.2",
"@rollup/plugin-alias": "^3.1.1", "@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
......
...@@ -25,6 +25,7 @@ export * from './protocols/media/chooseVideo' ...@@ -25,6 +25,7 @@ export * from './protocols/media/chooseVideo'
export * from './protocols/media/getImageInfo' export * from './protocols/media/getImageInfo'
export * from './protocols/network/request' export * from './protocols/network/request'
export * from './protocols/network/downloadFile'
export * from './protocols/route/route' export * from './protocols/route/route'
......
...@@ -14,4 +14,5 @@ export const DownloadFileProtocol: ApiProtocol<API_TYPE_DOWNLOAD_FILE> = { ...@@ -14,4 +14,5 @@ export const DownloadFileProtocol: ApiProtocol<API_TYPE_DOWNLOAD_FILE> = {
required: true, required: true,
}, },
header: Object, header: Object,
timeout: Number,
} }
...@@ -4734,6 +4734,22 @@ const RequestOptions = { ...@@ -4734,6 +4734,22 @@ const RequestOptions = {
} }
} }
}; };
const API_DOWNLOAD_FILE = "downloadFile";
const DownloadFileOptions = {
formatArgs: {
header(value, params) {
params.header = value || {};
}
}
};
const DownloadFileProtocol = {
url: {
type: String,
required: true
},
header: Object,
timeout: Number
};
function encodeQueryString(url) { function encodeQueryString(url) {
if (typeof url !== "string") { if (typeof url !== "string") {
return url; return url;
...@@ -10653,6 +10669,114 @@ function parseHeaders(headers) { ...@@ -10653,6 +10669,114 @@ function parseHeaders(headers) {
}); });
return headersObject; return headersObject;
} }
const files = {};
function getFileName(url) {
url = url.split("#")[0].split("?")[0];
const array = url.split("/");
return array[array.length - 1];
}
function fileToUrl(file) {
for (const key in files) {
if (hasOwn$1(files, key)) {
const oldFile = files[key];
if (oldFile === file) {
return key;
}
}
}
var url = (window.URL || window.webkitURL).createObjectURL(file);
files[url] = file;
return url;
}
class DownloadTask {
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() {
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 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);
xhr.open("GET", url, true);
Object.keys(header).forEach((key) => {
xhr.setRequestHeader(key, header[key]);
});
xhr.responseType = "blob";
xhr.onload = function() {
clearTimeout(timer);
const statusCode = xhr.status;
const blob = this.response;
let filename;
const contentDisposition = xhr.getResponseHeader("content-disposition");
if (contentDisposition) {
const res = contentDisposition.match(/filename="?(\S+)"?\b/);
if (res) {
filename = res[1];
}
}
blob.name = filename || getFileName(url);
resolve({
statusCode,
tempFilePath: fileToUrl(blob)
});
};
xhr.onabort = function() {
clearTimeout(timer);
reject("abort");
};
xhr.onerror = function() {
clearTimeout(timer);
reject("error");
};
xhr.onprogress = function(event2) {
downloadTask._callbacks.forEach((callback) => {
var totalBytesWritten = event2.loaded;
var totalBytesExpectedToWrite = event2.total;
var progress = Math.round(totalBytesWritten / totalBytesExpectedToWrite * 100);
callback({
progress,
totalBytesWritten,
totalBytesExpectedToWrite
});
});
};
xhr.send();
timer = setTimeout(function() {
xhr.onprogress = xhr.onload = xhr.onabort = xhr.onerror = null;
downloadTask.abort();
reject("timeout");
}, timeout);
return downloadTask;
}, DownloadFileProtocol, DownloadFileOptions);
const navigateBack = /* @__PURE__ */ defineAsyncApi(API_NAVIGATE_BACK, ({delta}, {resolve, reject}) => { const navigateBack = /* @__PURE__ */ defineAsyncApi(API_NAVIGATE_BACK, ({delta}, {resolve, reject}) => {
let canBack = true; let canBack = true;
if (invokeHook("onBackPress") === true) { if (invokeHook("onBackPress") === true) {
...@@ -10857,6 +10981,7 @@ var api = /* @__PURE__ */ Object.freeze({ ...@@ -10857,6 +10981,7 @@ var api = /* @__PURE__ */ Object.freeze({
openDocument, openDocument,
getImageInfo, getImageInfo,
request, request,
downloadFile,
navigateBack, navigateBack,
navigateTo, navigateTo,
redirectTo, redirectTo,
...@@ -11914,4 +12039,4 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { ...@@ -11914,4 +12039,4 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
]); ]);
} }
_sfc_main.render = _sfc_render; _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, 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, upx2px, useSubscribe};
import { hasOwn } from '@vue/shared'
/**
* 暂存的文件对象
*/
const files: { [key: string]: File } = {}
/**
* 从url读取File
* @param {string} url
* @param {boolean} local
* @param {Promise}
*/
export function urlToFile(url: string, local?: boolean): Promise<File> {
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()
})
}
/**
* base64转File
* @param {string} base64
* @return {File}
*/
export function base64ToFile(base64: string): File {
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)
}
/**
* 简易获取扩展名
* @param {string} type
* @return {string}
*/
function getExtname(type: string): string {
const extname = type.split('/')[1]
return extname ? `.${extname}` : ''
}
/**
* 简易获取文件名
* @param {string} url
*/
export function getFileName(url: string): string {
url = url.split('#')[0].split('?')[0]
const array = url.split('/')
return array[array.length - 1]
}
interface FileLike extends Blob {
name?: string
}
/**
* blob转File
* @param {Blob} blob
* @param {string} type
* @return {File}
*/
export function blobToFile(blob: Blob | Uint8Array | File, type: string): File {
let file: File | FileLike
if (blob instanceof File) {
file = blob
} else {
type = type || (<Blob>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 = <FileLike>blob
file.name = file.name || filename
}
}
return <File>file
}
/**
* 从本地file或者blob对象创建url
* @param {Blob|File} file
* @return {string}
*/
export function fileToUrl(file: Blob | File): string {
for (const key in files) {
if (hasOwn(files, key)) {
const oldFile = files[key]
if (oldFile === file) {
return key
}
}
}
var url = (window.URL || window.webkitURL).createObjectURL(file)
files[url] = <File>file
return url
}
export function getSameOriginUrl(url: string): Promise<string> {
const a = document.createElement('a')
a.href = url
if (a.origin === location.origin) {
return Promise.resolve(url)
}
return urlToFile(url).then(fileToUrl)
}
export function revokeObjectURL(url: string): void {
const URL = window.URL || window.webkitURL
URL.revokeObjectURL(url)
delete files[url]
}
...@@ -10,6 +10,7 @@ export * from './file/openDocument' ...@@ -10,6 +10,7 @@ export * from './file/openDocument'
export * from './media/getImageInfo' export * from './media/getImageInfo'
export * from './network/request' export * from './network/request'
export * from './network/downloadFile'
export * from './route/navigateBack' export * from './route/navigateBack'
export * from './route/navigateTo' export * from './route/navigateTo'
......
import { fileToUrl, getFileName } from '../../../helpers/file'
import {
defineTaskApi,
API_DOWNLOAD_FILE,
API_TYPE_DOWNLOAD_FILE,
DownloadFileProtocol,
DownloadFileOptions,
} from '@dcloudio/uni-api'
/**
* 下载任务
*/
class DownloadTask implements UniApp.DownloadTask {
private _xhr?: XMLHttpRequest
_callbacks: Function[] = []
constructor(xhr: XMLHttpRequest) {
this._xhr = xhr
}
/**
* 监听下载进度
* @param {Function} 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() {
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.')
}
}
/**
* 下载文件
* @param {*} param0
* @param {string} callbackId
* @return {DownloadTask}
*/
export const downloadFile = defineTaskApi<API_TYPE_DOWNLOAD_FILE>(
API_DOWNLOAD_FILE,
(
{ url, header, timeout = __uniConfig.networkTimeout.downloadFile },
{ resolve, reject }
) => {
var timer: number
var xhr = new XMLHttpRequest()
var downloadTask = new DownloadTask(xhr)
xhr.open('GET', url, true)
Object.keys(header).forEach((key) => {
xhr.setRequestHeader(key, header[key])
})
xhr.responseType = 'blob'
xhr.onload = function () {
clearTimeout(timer)
const statusCode = xhr.status
const blob = this.response
let filename
// 使用 getResponseHeader 跨域时会出现警告,但相比 getAllResponseHeaders 更方便
const contentDisposition = xhr.getResponseHeader('content-disposition')
if (contentDisposition) {
// 暂时仅解析 filename 不解析 filename*
const res = contentDisposition.match(/filename="?(\S+)"?\b/)
if (res) {
filename = res[1]
}
}
blob.name = filename || getFileName(url)
resolve({
statusCode,
tempFilePath: fileToUrl(blob),
})
}
xhr.onabort = function () {
clearTimeout(timer)
reject('abort')
}
xhr.onerror = function () {
clearTimeout(timer)
reject()
}
xhr.onprogress = function (event) {
downloadTask._callbacks.forEach((callback) => {
var totalBytesWritten = event.loaded
var totalBytesExpectedToWrite = event.total
var progress = Math.round(
(totalBytesWritten / totalBytesExpectedToWrite) * 100
)
callback({
progress,
totalBytesWritten,
totalBytesExpectedToWrite,
})
})
}
xhr.send()
timer = setTimeout(function () {
xhr.onprogress = xhr.onload = xhr.onabort = xhr.onerror = null
downloadTask.abort()
reject('timeout')
}, timeout)
return downloadTask
},
DownloadFileProtocol,
DownloadFileOptions
)
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册