并发控制
这里之前的 unUploadFileList
和 xhrMap
就派上了用场:
网络异常,图片无法展示
|
是否可以发起请求:
还有待上传的文件并且
xhrMap
中正在上传的请求数小于最大并发数
unUploadFileList.length > 0 && xhrMap.size < concurrency
是否结束上传:
没有待上传的文件并且
xhrMap
中没有正在进行的请求
isUploaded() { return this.unUploadFileList.length === 0 && this.xhrMap.size === 0; }
完整代码:
upload() { const { concurrency, xhrMap, unUploadFileList, xhrOptions, fileState } = this; return new Promise(resolve => { const run = async () => { while (unUploadFileList.length > 0 && xhrMap.size < concurrency) { const file = unUploadFileList.shift(); const { xhr, request } = this.requestFactory(file, xhrOptions, this.progressFactory(file)); xhrMap.set(file, xhr); request.finally(()=> { xhrMap.delete(file); if (this.isUploaded()) { resolve(); this.progressEvent('END', { fileState }); } else { run(); } }); } }; run(); }); }
取消上传
取消上传分为取消单个文件的上传和取消全部未完全上传的文件,这里用到的就是 abort
这个 api
,他的语法为:
xhrInstance.abort();
如果该请求已被发出,XMLHttpRequest.abort()
方法将终止该请求。当一个请求被终止,它的 readyState
将被置为 XMLHttpRequest.UNSENT(0)
,并且请求的 status
置为 0
。
取消上传的逻辑就是如果该请求在 xhrMap
里就 abort
掉,如果在 unUploadFileList
里就直接把它从数组里移除。
abort(file) { if (this.xhrMap.has(file)) { this.xhrMap.get(file)?.abort(); } else { const fileIndex = this.unUploadFileList.indexOf(file); this.unUploadFileList.splice(fileIndex, 1); } } abortAll() { this.xhrMap.forEach(xhr => xhr?.abort()); this.unUploadFileList = []; }
重新上传
当文件上传失败了,我们可以重新上传,这个就很简单了,就是把 file
添加到 unUploadFileList
中,当然,重新上传时有可能已经 upload
结束,此时就重新触发 upload
。
redoFile(file) { this.fileState.failCount--; this.unUploadFileList.push(file); if (this.isUploaded()) { this.upload(); } }
完整代码
class ZetaUploader { fileState = { failCount: 0, successCount: 0, abortCount: 0 } /* progressEvent arguments * @params {Object} [params] * @params {ENUM} [params?.state] FAIL PROGRESS SUCCESS END * @params {Info} [params?.info] * @params {File} [info?.file] * @params {Number} [info?.progress] * @params {FileState} [info?.fileState] */ progressEvent = () => { }; concurrency = 1; xhrMap = null; unUploadFileList = []; /* * @params {Object} [xhrOptions] * @params {String} [params.url] * @params {String} [params.method] * @params {Object} [params.headers] * @params {Function} [params.getXhrDataByFile] */ xhrOptions = null; static isRequestSuccess(progressEvent) { return String(progressEvent.target.status).startsWith('2'); } constructor(progressEvent, fileList, concurrency, xhrOptions) { const basicXhrOptions = { url: '', method: 'post', headers: [], getXhrDataByFile: file => { const formData = new FormData(); formData.append('file', file); return formData; } }; // BASIC ATTRS this.progressEvent = progressEvent; this.concurrency = concurrency; this.xhrOptions = Object.assign(basicXhrOptions, xhrOptions); // COMPUTED ATTRS this.unUploadFileList = fileList; this.xhrMap = new Map(); } } class BasicZetaUploader extends ZetaUploader { constructor(progressEvent, fileList, concurrency, xhrOptions) { super(progressEvent, fileList, concurrency, xhrOptions); } progressFactory(file) { return e => { this.progressEvent('PROGRESS', { file, progress: parseInt(String((e.loaded / e.total) * 100)) }); }; } /* * @params {File} [file] * @params {xhrOptions} [xhrOptions] * @params {Function} [onProgress] */ requestFactory(file, xhrOptions, onProgress) { const { url, method, headers, getXhrDataByFile } = xhrOptions; const xhr = new XMLHttpRequest(); xhr.open(method, url); Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key])); let _resolve = () => { }; let _reject = () => { }; xhr.onprogress = onProgress; xhr.onload = e => { // 需要加入response的判断 if (ZetaUploader.isRequestSuccess(e)) { this.fileState.successCount++; this.progressEvent('SUCCESS', { file }); _resolve({ data: e.target.response }); } else { this.fileState.failCount++; this.progressEvent('FAIL', { file }); _reject(); } }; xhr.onerror = () => { this.fileState.failCount++; this.progressEvent('FAIL', { file }); _reject(); }; xhr.onabort = () => { this.fileState.abortCount++; _resolve({ data: null }); }; const request = new Promise((resolve, reject) => { _resolve = resolve; _reject = reject; xhr.send(getXhrDataByFile(file)); }); return { xhr, request }; } upload() { const { concurrency, xhrMap, unUploadFileList, xhrOptions, fileState } = this; return new Promise(resolve => { const run = async () => { while (unUploadFileList.length > 0 && xhrMap.size < concurrency) { const file = unUploadFileList.shift(); const { xhr, request } = this.requestFactory(file, xhrOptions, this.progressFactory(file)); xhrMap.set(file, xhr); request.finally(()=> { xhrMap.delete(file); if (this.isUploaded()) { resolve(); this.progressEvent('END', { fileState }); } else { run(); } }); } }; run(); }); } isUploaded() { return this.unUploadFileList.length === 0 && this.xhrMap.size === 0; } abort(file) { if (this.xhrMap.has(file)) { this.xhrMap.get(file)?.abort(); } else { const fileIndex = this.unUploadFileList.indexOf(file); this.unUploadFileList.splice(fileIndex, 1); } } abortAll() { this.xhrMap.forEach(xhr => xhr?.abort()); this.unUploadFileList = []; } redoFile(file) { this.fileState.failCount--; this.unUploadFileList.push(file); if (this.isUploaded()) { this.upload(); } } } export { BasicZetaUploader };
结束语
网络异常,图片无法展示
|
本篇文章到这里就结束了,「本系列的」下一篇文章不出意外的话会带着大家了解文件分片与断点续传的原理和实现思路。
最后,下一篇文章见~
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
我生于长空,长于烈日;
我翱翔于风,从未远去。
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨