UNPKG

@jianghujs/jianghu

Version:

Progressive Enterprise Framework

529 lines (480 loc) 17.4 kB
<!-- jianghuAxios.html >>>>>>>>>>>>> --> <script src="/<$ ctx.app.config.appId $>/public/lib/spark-md5/spark-md5.min.js"></script> <script> (function () { const KB = 1024; const MB = 1024 * KB; const GB = 1024 * MB; const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; class BizError extends Error { constructor({errorCode, errorReason, errorReasonSupplement, response}) { super(JSON.stringify({errorCode, errorReason, errorReasonSupplement})); this.name = 'BizError'; this.errorCode = errorCode; this.errorReason = errorReason; this.errorReasonSupplement = errorReasonSupplement; this.response = response; } } window.jianghuAxios = axios.create({ headers: { 'Content-Type': 'application/json' }, timeout: 30000 }); /** * desc: 请求拦截器 ==> 数据填充 * [axios文档](https://axios-http.com/zh/docs/interceptors) */ window.jianghuAxios.interceptors.request.use(async config => { const data = config.data; if (!data.appData) { data.appData = {}; } const { pageId, actionId } = data.appData; const resourceId = `${pageId}.${actionId}`; // config.data 的 优先级更高 const packageId = `${Date.now()}_${_.random(1000000, 9999999)}`; if (!data.packageId) { data.packageId = packageId; } const packageType = 'httpRequest'; if (!data.packageType) { data.packageType = packageType; } const { appId, userAgent } = window.appInfo; if (!data.appData.appId) { data.appData.appId = appId; } if (!data.appData.userAgent) { data.appData.userAgent = userAgent; } const authToken = localStorage.getItem(`${window.appInfo.authTokenKey}_authToken`); if (!data.appData.authToken) { data.appData.authToken = authToken; } const baseURL = `/${appId}/resource?resourceId=${resourceId}`; const method = 'post'; return { ...config, baseURL, method, data }; }, err => { return Promise.reject(err); }); /** * desc: 响应拦截器 ==> 异常处理 * [axios文档](https://axios-http.com/zh/docs/interceptors) */ window.jianghuAxios.interceptors.response.use(async response => { const data = response.data || {}; const responseData = data.appData || {}; const {errorCode, errorReason} = responseData; if (errorCode === 'request_token_invalid' || errorCode === 'request_user_not_exist' || errorCode === 'request_token_expired' || errorCode === 'user_banned') { localStorage.removeItem(`${window.appInfo.authTokenKey}_authToken`); window.vtoast.fail({ message: errorReason }); if (location.pathname === `/${window.appInfo.appId}/page/login`) { throw new BizError({ errorCode, errorReason, response }); } location.href = '<$ ctx.app.config.loginPage $>' || `/${window.appInfo.appId}/page/login`; return response; } // 需要把 errorCode 转成异常抛出来 if (errorCode) { if (response.data.appData.actionId !== 'getConstantList') { window.vtoast.fail({ message: errorReason }); } throw new BizError({ errorCode, errorReason, response }); } return response; }, err => { const {code, message, response} = err; let {errorCode, errorReason} = err; if (!errorCode && code) { errorCode = code; } if (!errorReason && message) { errorReason = message; } return Promise.reject(new BizError({errorCode, errorReason, response})); }); // =============上传下载相关============= window.jianghuAxios.base64ToFile = ({ base64, name }) => { var arr = base64.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } const theBlob = new Blob([u8arr], { type: mime }); theBlob.lastModifiedDate = new Date(); theBlob.name = name; return theBlob; } window.jianghuAxios.base64ToUint8Array = function (base64) { const bstr = window.atob(base64) let n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return u8arr; } window.jianghuAxios.fileToBase64 = async (file) => { const base64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.error = function(err) { reject(err); } reader.onload = function() { resolve(this.result); } }) return base64; } window.jianghuAxios.downloadBufferToChrome = ({ buffer, filename }) => { let url = window.URL.createObjectURL(new Blob( [buffer], {type: "arraybuffer"}) ) const link = document.createElement('a'); link.style.display = 'none'; link.href = url; link.setAttribute('download', filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); } // base64 下载 window.jianghuAxios.downloadBase64ToChrome = ({ base64, filename }) => { var arr = base64.split(','), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } const theBlob = new Blob([u8arr], { type: mime }); theBlob.lastModifiedDate = new Date(); theBlob.name = name; let url = window.URL.createObjectURL(theBlob) const link = document.createElement('a'); link.style.display = 'none'; link.href = url; link.setAttribute('download', filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); } // http 流上传 window.jianghuAxios.httpUploadByStream = async({ file, fileDirectory, fileDesc, filenameStorage, onProgress = () =>{} }) => { const filename = file.name.replace(/ /g, '').replace(/\([^\)]*\)/g,""); const uploadFunc = async ({ chunFile, chunkSize, indexString, hash, total }) => { const forms = new FormData(); const { appId, userAgent } = window.appInfo; const packageId = `${Date.now()}_${_.random(1000000, 9999999)}`; const data = { packageId, packageType: 'httpRequest', appData: { appId, pageId: 'allPage', actionId: 'httpUploadByStream', userAgent, authToken: localStorage.getItem(`${window.appInfo.authTokenKey}_authToken`), actionData: { chunFile, chunkSize, indexString, hash, total, filename, } } }; forms.append('files', chunFile); forms.append('body', JSON.stringify(data)); return axios.post(`/${window.appInfo.appId}/resource?resourceId=${data.appData.pageId}.${data.appData.actionId}`, forms, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000, }); }; const uploadDoneFunc = async ({ hash, total, chunkSize }) => { return await window.jianghuAxios({ data: { appData: { pageId: 'allPage', actionId: 'uploadFileDone', actionData: { hash, total, chunkSize, filename, filenameStorage, fileDirectory, fileDesc } } } }); } return await upload({file, uploadFunc, uploadDoneFunc, onProgress}); }; // http base64上传 window.jianghuAxios.httpUploadByBase64 = async({ file, fileDirectory, fileDesc, filenameStorage, onProgress = () =>{} }) => { const filename = file.name.replace(/ /g, '').replace(/\([^\)]*\)/g,""); const uploadFunc = async ({ chunFile, chunkSize, indexString, hash, total }) => { const fileBase64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(chunFile); reader.error = function(err) { reject(err); } reader.onload = function() { resolve(this.result); } }) return await window.jianghuAxios({ data: { appData: { pageId: 'allPage', actionId: 'httpUploadByBase64', actionData: { chunkSize, indexString, hash, total, filename, fileBase64, } } } }); }; const uploadDoneFunc = async ({ hash, total, chunkSize }) => { return await window.jianghuAxios({ data: { appData: { pageId: 'allPage', actionId: 'uploadFileDone', actionData: { hash, total, chunkSize, filename, filenameStorage, fileDirectory, fileDesc } } } }); } return await upload({file, uploadFunc, uploadDoneFunc, onProgress}); }; // websocket 使用 base64 编码上传 window.jianghuAxios.socketUploadByBase64 = async({ file, fileDirectory, fileDesc, filenameStorage, onProgress = () =>{}}) => { const filename = file.name.replace(/ /g, '').replace(/\([^\)]*\)/g,""); const uploadFunc = async ({ chunFile, chunkSize, indexString, hash, total }) => { const fileBase64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(chunFile); reader.error = function(err) { reject(err); } reader.onload = function() { resolve(this.result); } }) return await window.socket.emit("resource", { data: { packageType: 'socketRequest', appData: { pageId: 'allPage', actionId: 'socketUploadByBase64', actionData: { chunFile, chunkSize, indexString, hash, total, filename, fileBase64, } } } }); }; const uploadDoneFunc = async ({ hash, total, chunkSize }) => { return await window.socket.emit("resource", { data: { packageType: 'socketRequest', appData: { pageId: 'allPage', actionId: 'uploadFileDone', actionData: { hash, total, chunkSize, filename, fileDirectory, filenameStorage, fileDesc } } } }); } return await upload({file, uploadFunc, uploadDoneFunc, onProgress}); } // http 流下载 window.jianghuAxios.httpDownloadByStream = async({ downloadPath, filename, onProgress = () => {}}) => { const oReq = new XMLHttpRequest(); const url = `/${window.appInfo.appId}/upload/${encodeURI(downloadPath)}`; oReq.open("GET", url, true); oReq.responseType = "arraybuffer"; return new Promise((resolve, reject) => { oReq.onprogress = function (event) { if (oReq.status === 404) { reject(new BizError({errorCode: 'file_download_error', errorReason: '文件不存在'})); return; } else if (oReq.status !== 200) { reject(new BizError({errorCode: 'file_download_error', errorReason: '文件下载失败' })); return; } if (event.lengthComputable) { onProgress(event.total, event.loaded); } } oReq.onload = function (oEvent) { const arrayBuffer = oReq.response; resolve(arrayBuffer); } oReq.send(null); }) } // websocket 使用 base64 编码下载 window.jianghuAxios.socketDownloadByBase64 = async({ downloadPath, filename, onProgress = () => {}}) => { const downloadFunc = async function ({ fileSize, total, chunkSize, hash, downloadPath, filename, success = () => {}}) { let allFileBase64 = ""; const mergedArray = new Uint8Array(fileSize); let offset = 0; for (let i = 0; i < total; i++) { const { fileBase64 } = (await window.socket.emit("resource", { data: { packageType: 'socketRequest', appData: { pageId: 'allPage', actionId: 'socketDownloadByBase64', actionData: { total, index: i, chunkSize, hash, downloadPath } } } })).data.appData; allFileBase64 = allFileBase64 + fileBase64 let base64 = fileBase64 if (i == 0) { const arr = base64.split(','); base64 = arr[1]; } const part = window.jianghuAxios.base64ToUint8Array(base64) mergedArray.set(part, offset) offset += part.length; success(); } return {base64: allFileBase64, buffer: mergedArray.buffer}; } const chunkInfo = (await window.socket.emit("resource", { data: { packageType: 'socketRequest', appData: { pageId: 'allPage', actionId: 'getChunkInfo', actionData: { downloadPath } } } })).data.appData; const { total, chunkSize, hash, fileSize } = chunkInfo; let loaded = 0; const {base64, buffer} = await downloadFunc({ fileSize, total, chunkSize, hash, downloadPath, filename, success: () => { loaded++; onProgress(total, loaded); if (loaded === total) { return true; } } }); const spark = new SparkMD5.ArrayBuffer() spark.append(buffer) const download_hash = spark.end() if (download_hash != hash) { throw new BizError({ errorCode: 'download_fail', errorReason: "文件下载失败!请重试." }) } return base64; } // http 流分块上传 async function upload({ file, uploadFunc, uploadDoneFunc, onProgress = () => {}}) { const { hash, total, chunkSize } = await getChunkInfo(file); let loaded = 0; await uploadFileChunkListByStream({ file, total, chunkSize, hash, uploadFunc, success: () => { loaded++; onProgress(total+2, loaded); } }); const result = await uploadDoneFunc({hash, total, chunkSize }); onProgress(total+2, loaded+2); return result; } // http 流分块上传 async function uploadFileChunkListByStream({ total, chunkSize, file, hash, uploadFunc, success }) { const requestArr = []; const loaded = 0; const dataArr = [] const filename = file.name.replace(/ /g, '').replace(/\([^\)]*\)/g,""); for (let i = 0; i < total; i++) { // 构建需要上传的分片数据 const start = i * chunkSize; const end = Math.min(file.size, start + chunkSize); const indexString = i.toString().padStart(4, "0"); if (indexString.length < 3) { indexString = `0${indexString}`; } if (indexString.length < 3) { indexString = `0${indexString}`; } if (indexString.length < 3) { indexString = `0${indexString}`; } if (indexString.length < 3) { indexString = `0${indexString}`; } dataArr.push({ index: i, indexString, chunFile: blobSlice.call(file, start, end) }) } const arr = _.chunk(dataArr, 4); for (const dataChunkArr of arr) { const requestArr = dataChunkArr.map(item => { return new Promise(async (resolve, reject) => { const maxRetryTimes = 3; for (let time = 0; time <= maxRetryTimes; time++) { try { const {indexString, chunFile} = item; const result = await uploadFunc({chunFile, chunkSize, indexString, hash, total}); if (result.data.status != 'success') { throw new BizError(result.data.appData); } success(); resolve(result); break; } catch (err) { console.error(`重试机制, ${filename}${item.index}分片 第${time}次上传失败;`, err); } if (time === maxRetryTimes) { console.error(`!!重试机制, ${filename}${item.index}分片 ${maxRetryTimes}次重试全部失败;`); throw new BizError({ errorCode: 'upload_fail', errorReason: "上传失败!请重试." }); } } }) }) await Promise.all(requestArr) } return true; } // 读取文件并按大小切割成 chunk function getChunkInfo(file) { const fileSize = file.size; let chunkSize = 3 * MB if (fileSize > 100 * MB) { chunkSize = 5 * MB; } else if (fileSize > 500 * MB) { chunkSize = 8 * MB; } return new Promise((resolve, reject) => { const chunksNum = Math.ceil(file.size / chunkSize); let currentChunk = 0; const spark = new SparkMD5.ArrayBuffer(); const fileReader = new FileReader(); fileReader.onerror = function (err) { reject(err); }; function loadNext() { const start = currentChunk * chunkSize; const end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); } fileReader.onload = function (e) { spark.append(e.target.result); // Append array buffer currentChunk++; if (currentChunk < chunksNum) { loadNext(); } else { const hash = spark.end(); if (chunksNum === 0) { resolve({ total: 1, hash, chunkSize }); } else { resolve({ total: chunksNum, hash, chunkSize }); } } }; loadNext(); }) } })(); </script> <!-- <<<<<<<<<<<<< jianghuAxios.html -->