From c1ea64b0bf9f8a13733b8049d980181c8abeb1b5 Mon Sep 17 00:00:00 2001 From: moyuderen Date: Tue, 8 Apr 2025 18:52:30 +0800 Subject: [PATCH 01/39] =?UTF-8?q?refactor(sdk):=20=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sdk/example/index.html | 43 -- packages/sdk/example/mian.js | 109 ---- .../sdk/examples/quick-start-nest/index.html | 53 ++ .../examples/quick-start-nest/quick-start.js | 143 ++++++ .../sdk/examples/quick-start-nest/request.js | 63 +++ packages/sdk/examples/quick-start/index.html | 53 ++ .../sdk/examples/quick-start/quick-start.js | 144 ++++++ packages/sdk/examples/quick-start/request.js | 63 +++ packages/sdk/{example => examples}/style.css | 22 +- packages/sdk/src/core/Chunk.js | 192 ++++--- packages/sdk/src/core/Container.js | 4 + .../src/{shared/event.js => core/Event.js} | 2 +- packages/sdk/src/core/File.js | 472 ++++++++++++------ packages/sdk/src/core/Uploader.js | 335 +++++++------ packages/sdk/src/core/constans.js | 35 -- packages/sdk/src/core/constants.js | 146 ++++++ packages/sdk/src/core/defaults.js | 82 +-- packages/sdk/src/core/request.js | 32 ++ packages/sdk/src/index.js | 6 +- packages/sdk/src/shared/hash-worker.js | 63 ++- packages/sdk/src/shared/hash.js | 106 ++-- packages/sdk/src/shared/index.js | 17 +- packages/sdk/src/shared/types.js | 4 + packages/sdk/vite.config.js | 3 +- 24 files changed, 1446 insertions(+), 746 deletions(-) delete mode 100644 packages/sdk/example/index.html delete mode 100644 packages/sdk/example/mian.js create mode 100644 packages/sdk/examples/quick-start-nest/index.html create mode 100644 packages/sdk/examples/quick-start-nest/quick-start.js create mode 100644 packages/sdk/examples/quick-start-nest/request.js create mode 100644 packages/sdk/examples/quick-start/index.html create mode 100644 packages/sdk/examples/quick-start/quick-start.js create mode 100644 packages/sdk/examples/quick-start/request.js rename packages/sdk/{example => examples}/style.css (70%) rename packages/sdk/src/{shared/event.js => core/Event.js} (97%) delete mode 100644 packages/sdk/src/core/constans.js create mode 100644 packages/sdk/src/core/constants.js create mode 100644 packages/sdk/src/core/request.js diff --git a/packages/sdk/example/index.html b/packages/sdk/example/index.html deleted file mode 100644 index 19dd7b7..0000000 --- a/packages/sdk/example/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - dev sdk - - - - - -
-
- - -
-
-
{{ file.name }}
-
{{ file.status }}
-
{{ (file.progress * 100).toFixed(2) }}%
-
- - - - -
-
-
-
-
- - - diff --git a/packages/sdk/example/mian.js b/packages/sdk/example/mian.js deleted file mode 100644 index f0bf078..0000000 --- a/packages/sdk/example/mian.js +++ /dev/null @@ -1,109 +0,0 @@ -const { createApp, ref, onMounted } = Vue - -// import { create, Status, Events, CheckStatus } from '../dist/sdk.mjs' -import { create, Status, Events, CheckStatus } from '../src' - -createApp({ - setup() { - const defaultFileList = [{ path: 'http://baidu.com', name: 'haha' }] - const uploader = ref(null) - const files = ref([]) - - uploader.value = create({ - action: 'http://localhost:3000/upload', - fileList: defaultFileList, - chunkSize: 1024 * 1024 * 10, // 10M, - // chunkSize: 1024 * 3, // 3k, - maxRetries: 0, - withCredentials: true, - async mergeRequest(file) { - const { hash, name } = file - const { data } = await axios.post('http://localhost:3000/merge', { name, hash }) - file.path = data.data - }, - async checkFileRequest(file) { - const { data } = await axios.post('http://localhost:3000/checkFile', { - // status: CheckStatus.Success - status: CheckStatus.Part - // status: CheckStatus.None - }) - return data - } - }) - files.value = uploader.value.fileList - - onMounted(() => { - uploader.value.assignBrowse(document.querySelector('.uploader-btn')) - }) - - const pause = (file) => { - uploader.value.pause(file) - } - - const resume = (file) => { - uploader.value.resume(file) - } - - const retry = (file) => { - uploader.value.retry(file) - } - - const remove = (file) => { - uploader.value.remove(file) - } - - const clear = () => { - uploader.value.clear() - } - - uploader.value.on(Events.FilesAdded, (fileList) => { - files.value = fileList - }) - - uploader.value.on(Events.FileProgress, (progress, file, fileList) => { - console.log(`${file.name}: 上传进度${(progress * 100).toFixed(2)}`) - files.value = fileList - }) - - uploader.value.on(Events.FileUploadFail, (file, fileList) => { - console.log(`${file.name}: 分片上传失败`) - files.value = fileList - }) - - uploader.value.on(Events.FileFail, (file, fileList) => { - console.log(`${file.name}: merge失败`) - files.value = fileList - }) - - uploader.value.on(Events.FileUploadSuccess, (file, fileList) => { - console.log(`${file.name}: 分片上传成功,准备merge`) - files.value = fileList - }) - - uploader.value.on(Events.FileSuccess, (file, fileList) => { - console.log(`${file.name}: 上传且合并成功`) - files.value = fileList - }) - - uploader.value.on(Events.FileRemove, (file, fileList) => { - console.log(`${file.name}: 被删除了`) - files.value = fileList - }) - - uploader.value.on(Events.AllFileSuccess, (fileList) => { - console.log(`全部上传成功`, fileList) - files.value = fileList - }) - - return { - Status, - uploader, - files, - pause, - resume, - retry, - remove, - clear - } - } -}).mount('#app') diff --git a/packages/sdk/examples/quick-start-nest/index.html b/packages/sdk/examples/quick-start-nest/index.html new file mode 100644 index 0000000..af4c23f --- /dev/null +++ b/packages/sdk/examples/quick-start-nest/index.html @@ -0,0 +1,53 @@ + + + + + + Quick Start nest + + + + + +
+
+ +
Uploader Drag
+ +
+
+
{{ file.name }}
+
{{ file.status }}
+
Read {{ file.readProgress.toFixed(2) }}%
+
Upload {{ (file.progress * 100).toFixed(2) }}%
+
{{ file.renderSize }}
+
+ + + + +
+
+
+ + + +
+
+ + + diff --git a/packages/sdk/examples/quick-start-nest/quick-start.js b/packages/sdk/examples/quick-start-nest/quick-start.js new file mode 100644 index 0000000..165319c --- /dev/null +++ b/packages/sdk/examples/quick-start-nest/quick-start.js @@ -0,0 +1,143 @@ +const { createApp, ref, onMounted } = Vue + +// dist +// import { create, FileStatus, ChunkStatus, CheckStatus, Callbacks } from '../dist/sdk.mjs' + +// dev +import { create, FileStatus, ChunkStatus, CheckStatus, Callbacks } from '../../src/index.js' +import { requestSucceed, customRequest, checkRequest, mergeRequest } from './request.js' + +createApp({ + setup() { + const uploader = ref(null) + const files = ref([]) + + onMounted(() => { + uploader.value.assignBrowse(document.querySelector('.uploader-btn')) + uploader.value.assignBrowse(document.querySelector('.uploader-drag')) + uploader.value.assignDrop(document.querySelector('.uploader-drag')) + }) + + uploader.value = create({ + limit: 5, + chunkSize: 1024 * 3, + customRequest, + requestSucceed, + checkRequest, + mergeRequest + }) + + uploader.value.on(Callbacks.Exceed, (file, fileList) => { + console.log(`Exceed ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileChange, (file, fileList) => { + console.log(`FileChange ---- ${file.name} ---- ${file.status}`) + files.value = fileList + }) + + uploader.value.on(Callbacks.FileAdded, (file, fileList) => { + console.log(`FileAdded ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FilesAdded, (fileList) => { + console.log(`FilesAdded ----`, fileList) + }) + + uploader.value.on(Callbacks.FileReadStart, (file, fileList) => { + console.log(`FileReadStart ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileReadProgress, (file, fileList) => { + console.log(`FileReadProgress ---- ${file.name} ---- ${file.status}---- ${file.readProgress}`) + }) + + uploader.value.on(Callbacks.FileReadEnd, (file, fileList) => { + console.log(`FileReadEnd ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileRemove, (file, fileList) => { + console.log(`FileRemove ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileProgress, (file, fileList) => { + console.log(`FileProgress ---- ${file.name} ---- ${file.status}---- ${file.progress}`) + }) + + uploader.value.on(Callbacks.FileFail, (file, fileList) => { + console.log(`FileFail ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileUploadFail, (file, fileList) => { + console.log(`FileUploadFail ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileUploadSuccess, (file, fileList) => { + console.log(`FileUploadSuccess ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileSuccess, (file, fileList) => { + console.log(`FileSuccess ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FilePause, (file, fileList) => { + console.log(`FilePause ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileResume, (file, fileList) => { + console.log(`FileResume ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.AllFileSuccess, (fileList) => { + console.log(`AllFileSuccess ---- `, fileList) + }) + + uploader.value.setDefaultFileList([ + { + id: '1', + name: 'baidu.png', + url: 'http://baidu.com' + }, + { + id: '2', + name: 'google.png', + url: 'http://google.com' + } + ]) + + const submit = () => { + uploader.value.submit() + } + + const clear = () => { + uploader.value.clear() + } + + const remove = (file) => { + uploader.value.remove(file) + } + + const pause = (file) => { + uploader.value.pause(file) + } + + const resume = (file) => { + uploader.value.resume(file) + } + + const retry = (file) => { + uploader.value.retry(file) + } + + return { + FileStatus, + files, + submit, + clear, + remove, + pause, + resume, + retry + } + } +}).mount('#app') diff --git a/packages/sdk/examples/quick-start-nest/request.js b/packages/sdk/examples/quick-start-nest/request.js new file mode 100644 index 0000000..31e1ec3 --- /dev/null +++ b/packages/sdk/examples/quick-start-nest/request.js @@ -0,0 +1,63 @@ +import { CheckStatus } from '../../src/core/constants' + +const sleep = (time = 1000) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) +} + +export const requestSucceed = (response) => { + const { status } = response + if (status >= 200 && status < 300) { + return true + } + return false +} + +export const customRequest = (options) => { + const { action, formData, data, headers, withCredentials, onSuccess, onFail, onProgress } = + options + + Object.keys(data).forEach((key) => { + formData.append(key, data[key]) + }) + const CancelToken = axios.CancelToken + const source = CancelToken.source() + + axios({ + url: 'http://localhost:3000/upload', + method: 'POST', + data: formData, + headers: headers, + cancelToken: source.token, + withCredentials: withCredentials, + onUploadProgress: onProgress + }) + .then((res) => { + onSuccess(action, res) + }) + .catch((e) => { + onFail(e) + }) + + return { + abort() { + source.cancel('Operation canceled by the user.') + } + } +} + +export const checkRequest = async (file) => { + const data = await fetch( + `http://localhost:3000/check?hash=${file.hash}&filename=${file.name}&status=none`, + {} + ) + return data +} + +export const mergeRequest = async (file) => { + await fetch(`http://localhost:3000/merge?hash=${file.hash}&filename=${file.name}`, {}) + return true +} diff --git a/packages/sdk/examples/quick-start/index.html b/packages/sdk/examples/quick-start/index.html new file mode 100644 index 0000000..95c5cbc --- /dev/null +++ b/packages/sdk/examples/quick-start/index.html @@ -0,0 +1,53 @@ + + + + + + Quick Start + + + + + +
+
+ +
Uploader Drag
+ +
+
+
{{ file.name }}
+
{{ file.status }}
+
Read {{ file.readProgress.toFixed(2) }}%
+
Upload {{ (file.progress * 100).toFixed(2) }}%
+
{{ file.renderSize }}
+
+ + + + +
+
+
+ + + +
+
+ + + diff --git a/packages/sdk/examples/quick-start/quick-start.js b/packages/sdk/examples/quick-start/quick-start.js new file mode 100644 index 0000000..64af5b7 --- /dev/null +++ b/packages/sdk/examples/quick-start/quick-start.js @@ -0,0 +1,144 @@ +const { createApp, ref, onMounted } = Vue + +// dist +// import { create, FileStatus, ChunkStatus, CheckStatus, Callbacks } from '../dist/sdk.mjs' + +// dev +import { create, FileStatus, ChunkStatus, CheckStatus, Callbacks } from '../../src/index.js' +import { requestSucceed, customRequest, checkRequest, mergeRequest } from './request.js' + +createApp({ + setup() { + const uploader = ref(null) + const files = ref([]) + + onMounted(() => { + uploader.value.assignBrowse(document.querySelector('.uploader-btn')) + uploader.value.assignBrowse(document.querySelector('.uploader-drag')) + uploader.value.assignDrop(document.querySelector('.uploader-drag')) + }) + + uploader.value = create({ + action: 'https://jsonplaceholder.typicode.com/posts', + limit: 5, + chunkSize: 1024 * 3, + customRequest, + requestSucceed, + checkRequest, + mergeRequest + }) + + uploader.value.on(Callbacks.Exceed, (file, fileList) => { + console.log(`Exceed ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileChange, (file, fileList) => { + console.log(`FileChange ---- ${file.name} ---- ${file.status}`) + files.value = fileList + }) + + uploader.value.on(Callbacks.FileAdded, (file, fileList) => { + console.log(`FileAdded ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FilesAdded, (fileList) => { + console.log(`FilesAdded ----`, fileList) + }) + + uploader.value.on(Callbacks.FileReadStart, (file, fileList) => { + console.log(`FileReadStart ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileReadProgress, (file, fileList) => { + console.log(`FileReadProgress ---- ${file.name} ---- ${file.status}---- ${file.readProgress}`) + }) + + uploader.value.on(Callbacks.FileReadEnd, (file, fileList) => { + console.log(`FileReadEnd ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileRemove, (file, fileList) => { + console.log(`FileRemove ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileProgress, (file, fileList) => { + console.log(`FileProgress ---- ${file.name} ---- ${file.status}---- ${file.progress}`) + }) + + uploader.value.on(Callbacks.FileFail, (file, fileList) => { + console.log(`FileFail ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileUploadFail, (file, fileList) => { + console.log(`FileUploadFail ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileUploadSuccess, (file, fileList) => { + console.log(`FileUploadSuccess ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileSuccess, (file, fileList) => { + console.log(`FileSuccess ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FilePause, (file, fileList) => { + console.log(`FilePause ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.FileResume, (file, fileList) => { + console.log(`FileResume ---- ${file.name} ---- ${file.status}`) + }) + + uploader.value.on(Callbacks.AllFileSuccess, (fileList) => { + console.log(`AllFileSuccess ---- `, fileList) + }) + + uploader.value.setDefaultFileList([ + { + id: '1', + name: 'baidu.png', + url: 'http://baidu.com' + }, + { + id: '2', + name: 'google.png', + url: 'http://google.com' + } + ]) + + const submit = () => { + uploader.value.submit() + } + + const clear = () => { + uploader.value.clear() + } + + const remove = (file) => { + uploader.value.remove(file) + } + + const pause = (file) => { + uploader.value.pause(file) + } + + const resume = (file) => { + uploader.value.resume(file) + } + + const retry = (file) => { + uploader.value.retry(file) + } + + return { + FileStatus, + files, + submit, + clear, + remove, + pause, + resume, + retry + } + } +}).mount('#app') diff --git a/packages/sdk/examples/quick-start/request.js b/packages/sdk/examples/quick-start/request.js new file mode 100644 index 0000000..e4f4dc1 --- /dev/null +++ b/packages/sdk/examples/quick-start/request.js @@ -0,0 +1,63 @@ +import { CheckStatus } from '../../src/core/constants' + +const sleep = (time = 1000) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, time) + }) +} + +export const requestSucceed = (response) => { + const { status } = response + if (status >= 200 && status < 300) { + return true + } + return false +} + +export const customRequest = (options) => { + const { action, formData, data, headers, withCredentials, onSuccess, onFail, onProgress } = + options + + Object.keys(data).forEach((key) => { + formData.append(key, data[key]) + }) + const CancelToken = axios.CancelToken + const source = CancelToken.source() + + axios({ + url: action, + method: 'POST', + data: formData, + headers: headers, + cancelToken: source.token, + withCredentials: withCredentials, + onUploadProgress: onProgress + }) + .then((res) => { + onSuccess(action, res) + }) + .catch((e) => { + onFail(e) + }) + + return { + abort() { + source.cancel('Operation canceled by the user.') + } + } +} + +export const checkRequest = async (file) => { + const data = await fetch(`/api/check?hash=${file.hash}&filename=${file.name}`, {}) + return { + status: CheckStatus.Part, + data: [0, 2, 4, 6, 8, 10] // data是已经上传成功chunk的chunkIndex + } +} + +export const mergeRequest = async (file) => { + await fetch(`/api/merge?hash=${file.hash}&filename=${file.name}`, {}) + return true +} diff --git a/packages/sdk/example/style.css b/packages/sdk/examples/style.css similarity index 70% rename from packages/sdk/example/style.css rename to packages/sdk/examples/style.css index 592dca3..f2aeec8 100644 --- a/packages/sdk/example/style.css +++ b/packages/sdk/examples/style.css @@ -7,9 +7,19 @@ margin-right: var(--default-margin); } +.uploader-drag { + width: 300px; + height: 160px; + display: flex; + justify-content: center; + align-items: center; + border: 1px dashed #000; + margin-top: 10px; + border-radius: 3px; +} + .file-list { margin-top: 10px; - max-width: 660px; } .file { @@ -24,7 +34,7 @@ } .file-name { - width: 200px; + width: 300px; overflow: hidden; text-overflow: ellipsis; margin-right: var(--default-margin); @@ -37,14 +47,18 @@ } .file-progress { - width: 100px; + width: 160px; } .action-container { - min-width: 200px; + min-width: 300px; text-align: right; } .action { margin-right: var(--default-margin); } + +.uploader-submit { + margin-right: 20px; +} diff --git a/packages/sdk/src/core/Chunk.js b/packages/sdk/src/core/Chunk.js index 47c5195..c53942e 100644 --- a/packages/sdk/src/core/Chunk.js +++ b/packages/sdk/src/core/Chunk.js @@ -1,135 +1,129 @@ -import { generateUid, each } from '@/shared' -import { Status } from './constans.js' +import { generateUid } from '../shared' +import { ChunkStatus, FileStatus } from './constants' +import { request } from './request' export default class Chunk { constructor(file, index) { this.uploader = file.uploader - this.opts = file.uploader.opts + this.options = file.uploader.options + this.file = file - this.filename = file.name + this.rawFile = file.rawFile this.fileId = file.uid + this.fileHash = file.hash + this.filename = file.name this.totalSize = file.size - this.chunkSize = this.opts.chunkSize + this.chunkSize = this.options.chunkSize - this.fileHash = this.file.hash - this.uid = generateUid('chunk_id') + this.uid = generateUid() + this.chunkIndex = index + this.status = ChunkStatus.Ready this.stardByte = this.chunkSize * index this.endByte = Math.min(this.stardByte + this.chunkSize, this.totalSize) this.size = this.endByte - this.stardByte - this.chunkIndex = index - this.maxRetries = this.opts.maxRetries - this.xhr = null - this.promise = null - this.status = Status.Ready + this.maxRetries = this.options.maxRetries + this.progress = 0 this.fakeProgress = 0 this.timer = null - } - prepareXhr() { - const data = new FormData() - this.xhr.responseType = 'json' - this.xhr.withCredentials = this.opts.withCredentials - const blob = this.file.rawFile.slice(this.stardByte, this.endByte) + this.request = null + this.customRequest = this.options.customRequest || request + } - data.append(this.opts.name, blob) - if (this.fileHash) { - data.append('hash', this.fileHash) + onSuccess(e, response, resolve) { + if (this.options.requestSucceed(response, this)) { + this.status = Chunk.Success + this.file.removeUploadingChunk(this) + if (this.file.isUploading()) { + this.file.upload() + } + resolve(this) + } else { + this.onFail(e) } - data.append('id', this.uid) - data.append('fileId', this.fileId) - data.append('index', this.chunkIndex) - data.append('filename', this.filename) - data.append('size', this.size) - data.append('totalSize', this.totalSize) - data.append('timestamp', Date.now()) - each(this.opts.data, (val, key) => { - data.append(key, val) - }) - this.xhr.open('POST', this.opts.action, true) + } - // 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED - if ('setRequestHeader' in this.xhr) { - each(this.opts.headers, (val, key) => { - this.xhr.setRequestHeader(key, val) - }) + onFail(e, reject) { + this.progress = 0 + this.file.setProgress(this) + if (this.request.canceled) { + return + } + if (this.maxRetries <= 0) { + this.file.removeUploadingChunk(this) + this.status = ChunkStatus.Fail + if (this.file.isUploading()) { + this.file.upload() + } + reject(this) + } else { + this.timer = setTimeout(() => { + this.send() + this.maxRetries-- + clearTimeout(this.timer) + }, this.options.retryInterval) } + } - return data + onProgress(e) { + this.progress = Math.min(1, e.loaded / e.total) + this.fakeProgress = Math.max(this.progress, this.fakeProgress) + + this.status = ChunkStatus.Uploading + this.file.changeStatus(FileStatus.Uploading) + this.file.setProgress(this) } - async send() { - return new Promise(async (resolve, reject) => { - this.status = Status.Pending - - // eslint-disable-next-line no-unused-vars - const failHandler = (e) => { - this.progress = 0 - this.file.setProgress(this) - if (this.maxRetries <= 0) { - this.file.removeUploadingQueue(this) - this.status = Status.Fail - if (this.file.isUploading()) { - this.file.uploadFile() - } - - reject(this) - } else { - this.timer = setTimeout(() => { - this.send() - this.maxRetries-- - clearTimeout(this.timer) - }, this.uploader.opts.retryInterval) - } - } - const doneHandler = (e) => { - if (this.uploader.opts.requestSucceed(this.xhr)) { - this.status = Status.Success - this.file.removeUploadingQueue(this) - if (this.file.isUploading()) { - this.file.uploadFile() - } - resolve(this) - } else { - failHandler(e) - } - } + prepare() { + const formData = new FormData() + const blob = this.file.rawFile.slice(this.stardByte, this.endByte) - const progressHandler = (e) => { - this.progress = Math.min(1, e.loaded / e.total) - this.fakeProgress = Math.max(this.progress, this.fakeProgress) + formData.append(this.options.name, blob) - this.status = Status.Uploading - this.file.changeStatus(Status.Uploading) - this.file.setProgress(this) - } + if (this.fileHash) { + formData.append('hash', this.fileHash) + } + + formData.append('id', this.uid) + formData.append('fileId', this.fileId) + formData.append('index', this.chunkIndex) + formData.append('filename', this.filename) + formData.append('size', this.size) + formData.append('totalSize', this.totalSize) + formData.append('timestamp', Date.now()) + + return formData + } - this.xhr = new XMLHttpRequest() - const data = this.prepareXhr() - this.xhr.upload.addEventListener('progress', progressHandler) - this.xhr.addEventListener('load', doneHandler, false) - this.xhr.addEventListener('error', failHandler, false) - this.xhr.send(data) + send() { + this.status = ChunkStatus.Pending + return new Promise((resolve, reject) => { + this.request = this.customRequest({ + formData: this.prepare(), + action: this.options.action, + data: this.options.data, + headers: this.options.headers, + withCredentials: this.options.withCredentials, + name: this.options.name, + onSuccess: (e, response) => this.onSuccess(e, response, resolve), + onFail: (e) => this.onFail(e, reject), + onProgress: (e) => this.onProgress(e) + }) + this.request.canceled = false }) } abort() { - this.status = Status.Ready - if (this.xhr) { - this.xhr.abort() - // this.xhr = null 避免缓存xhr,使用旧的xhr重新发起导致请求失败 - this.xhr = null + this.status = ChunkStatus.Ready + if (this.request) { + this.request.canceled = true + this.request.abort() + // this.request = null } if (this.timer) { clearTimeout(this.timer) } } - - cancel() { - if (this.status === Status.Pending || this.status === Status.Uploading) { - this.status = Status.Ready - this.abort() - } - } } diff --git a/packages/sdk/src/core/Container.js b/packages/sdk/src/core/Container.js index 455dc0c..287aebc 100644 --- a/packages/sdk/src/core/Container.js +++ b/packages/sdk/src/core/Container.js @@ -7,6 +7,10 @@ class Container { assignBrowse(domNode, attributes) { let input + if (!domNode) { + console.warn('domNode is not exist') + return + } if (domNode.tagName === 'INPUT' && domNode.type === 'file') { input = domNode } else { diff --git a/packages/sdk/src/shared/event.js b/packages/sdk/src/core/Event.js similarity index 97% rename from packages/sdk/src/shared/event.js rename to packages/sdk/src/core/Event.js index a55751b..fd2ebf5 100644 --- a/packages/sdk/src/shared/event.js +++ b/packages/sdk/src/core/Event.js @@ -1,4 +1,4 @@ -export class Event { +export default class Event { constructor() { this.event = {} } diff --git a/packages/sdk/src/core/File.js b/packages/sdk/src/core/File.js index 6f9086f..4807066 100644 --- a/packages/sdk/src/core/File.js +++ b/packages/sdk/src/core/File.js @@ -1,150 +1,276 @@ -import Chunk from './Chunk.js' -import { isFunction, generateUid, asyncComputedHash, isPromise, each } from '@/shared' -import { Status, Events, CheckStatus } from './constans.js' +import Chunk from './Chunk' +import { FileStatus, Callbacks, CheckStatus, ChunkStatus } from './constants' +import { + generateUid, + isFunction, + asyncCancellableComputedHash, + each, + isPromise, + throttle +} from '../shared' export default class File { constructor(file, uploader) { - this.uploader = uploader || { opts: {} } - this.opts = this.uploader.opts + this.uploader = uploader || { options: {} } + this.options = this.uploader.options + this.uid = this.generateId() + + this.prevStatusLastRecord = [] + // this.status - FileStatus.Init this.rawFile = file - if (isFunction(this.opts.customGenerateUid)) { - this.uid = this.opts.customGenerateUid(file) || generateUid('fid') - } else { - this.uid = generateUid('fid') - } - this.hash = '' - this.size = file.size this.name = file.name || file.fileName + this.size = file.size this.type = file.type - this.chunkSize = this.opts.chunkSize - this.changeStatus(file.status || Status.Init) + this.hash = '' + this.url = file.url || '' this.progress = file.progress || 0 + this.chunkSize = this.options.chunkSize this.chunks = [] - this.uploadingQueue = new Set() + this.totalChunks = 0 + this.uploadingChunks = new Set() this.readProgress = 0 - this.path = file.path || '' + this.errorMessage = '' + + this.changeStatus(file.status || FileStatus.Init) + } + + generateId() { + const customGenerateUid = this.options.customGenerateUid + if (!customGenerateUid) { + return generateUid() + } + + if (!isFunction(customGenerateUid)) { + console.warn('customGenerateUid must be a function') + return generateUid() + } + + return customGenerateUid(this) || generateUid() + } + + setErrorMessage(message) { + this.errorMessage = message + return true + } + + get renderSize() { + const value = this.size + const ONE_KB = 1024 + if (null == value || value == '') { + return '0 Bytes' + } + var unitArr = new Array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB') + var index = 0 + var srcsize = parseFloat(value) + index = Math.floor(Math.log(srcsize) / Math.log(ONE_KB)) + var size = srcsize / Math.pow(ONE_KB, index) + size = size.toFixed(2) //保留的小数位数 + return size + unitArr[index] + } + + changeStatus(newStatus) { + if (newStatus !== this.status || newStatus === FileStatus.Reading) { + this.prevStatusLastRecord.push(this.status) + this.status = newStatus + // 兼容默认值时没有uploader实例 + if (this.uploader && this.uploader.emitCallback) { + this.uploader.emitCallback(Callbacks.FileChange, this) + } + } + } + + isInit() { + return this.status === FileStatus.Init + } + + isAddFail() { + return this.status === FileStatus.AddFail + } + + isReading() { + return this.status === FileStatus.Reading + } + + isReady() { + return this.status === FileStatus.Ready + } + + isCheckFail() { + return this.status === FileStatus.CheckFail + } + + isUploading() { + return this.status === FileStatus.Uploading + } + + isUploadSuccess() { + return this.status === FileStatus.UploadSuccess + } + + isUploadFail() { + return this.status === FileStatus.UploadFail + } + + isSuccess() { + return this.status === FileStatus.Success + } + + isFail() { + return this.status === FileStatus.Fail + } + + isPause() { + return this.status === FileStatus.Pause + } + + isResume() { + return this.status === FileStatus.Resume + } + + createChunks() { + const totalChunks = (this.totalChunks = Math.ceil(this.size / this.chunkSize)) + for (let i = 0; i < totalChunks; i++) { + this.chunks.push(new Chunk(this, i)) + } } - async start() { - await this.computedHash() + async read() { + if (this.options.withHash) { + this.uploader.emitCallback(Callbacks.FileReadStart, this) + this.changeStatus(FileStatus.Reading) + try { + const startTime = Date.now() + const { hash } = await this.computedHash() + this.hash = hash + this.uploader.emitCallback(Callbacks.FileReadEnd, this) + this.abortRead = null + const endTime = Date.now() + console.log( + `${this.options.useWebWoker ? 'Web Worker' : 'Normal'} Read file cost`, + (endTime - startTime) / 1000, + 's' + ) + } catch (e) { + this.abortRead = null + this.changeStatus(FileStatus.Init) + throw new Error((e && e.message) || 'read file error') + } + } this.createChunks() - if (this.opts.checkFileRequest) { - await this.checkRequest() + this.changeStatus(FileStatus.Ready) + } + + async computedHash() { + const updateReadProgress = (readProgress) => { + this.readProgress = readProgress + this.uploader.emitCallback(Callbacks.FileReadProgress, this) } + + let throttlReadProgressleHandle = throttle(updateReadProgress, 100) + + const { promise, abort } = asyncCancellableComputedHash( + { + file: this.rawFile, + chunkSize: this.chunkSize, + useWebWoker: this.options.useWebWoker + }, + ({ progress: readProgress }) => { + throttlReadProgressleHandle(readProgress) + } + ) + this.abortRead = abort + const hashResult = await promise + updateReadProgress(hashResult.progress) + return hashResult } async checkRequest() { - try { - const { status: checkStatus, data } = await this.opts.checkFileRequest(this) + const check = this.options.checkRequest + const checkStatusFn = (checkStatus, data, resolve) => { if (checkStatus === CheckStatus.Part) { this.chunks.forEach((chunk) => { if (data.includes(chunk.chunkIndex)) { - chunk.status = Status.Success + chunk.status = ChunkStatus.Success chunk.progress = 1 chunk.fakeProgress = 1 } }) } if (checkStatus === CheckStatus.Success) { - this.success() this.chunks.forEach((chunk) => { - chunk.status = Status.success + chunk.status = ChunkStatus.success }) this.path = data } - } catch { - // + resolve() } - } - computedHash() { - return new Promise((resolve, reject) => { - if (!this.opts.withHash) { - resolve() - } - this.changeStatus(Status.Reading) - asyncComputedHash( - { - file: this.rawFile, - chunkSize: this.chunkSize, - inWorker: this.opts.computedHashWorker - }, - ({ progress }) => { - this.readProgress = progress - } - ).then(({ hash }) => { - this.hash = hash - resolve() - }) - }) - } - - createChunks() { - const totalChunks = (this.totalChunks = Math.ceil(this.size / this.chunkSize)) - for (let i = 0; i < totalChunks; i++) { - this.chunks.push(new Chunk(this, i)) + const rejectCheck = (reject) => { + this.changeStatus(FileStatus.CheckFail) + reject(new Error('checkRequest error')) } - this.changeStatus(Status.Ready) - } - - setProgress() { - const progress = this.chunks.reduce((total, chunk) => { - const p = this.opts.fakeProgress ? chunk.fakeProgress : chunk.progress - return (total += p * (chunk.size / this.size)) - }, 0) - // this.progress = Math.max(Math.min(progress, 1), this.progress) - this.progress = Math.min(1, progress) + return new Promise(async (resolve, reject) => { + if (!isFunction(check)) { + resolve() + } - if (this.isUploadSuccess()) { - this.progress = 1 - } + const result = check(this) - this.uploader.emit(Events.FileProgress, this.progress, this, this.uploader.fileList) + if (isPromise(result)) { + const data = await result + data ? checkStatusFn(data.status, data.data, resolve) : rejectCheck(reject) + } else { + result ? checkStatusFn(data.status, data.data, resolve) : rejectCheck(reject) + } + }) } - removeUploadingQueue(chunk) { - this.uploadingQueue.delete(chunk) + addUploadingChunk(chunk) { + this.uploadingChunks.add(chunk) } - addUploadingQueue(chunk) { - this.uploadingQueue.add(chunk) + removeUploadingChunk(chunk) { + this.uploadingChunks.delete(chunk) } - uploadFile() { - if (this.isSuccess()) { - this.success() - return + async upload() { + if (this.isInit()) { + await this.read() } - const readyChunks = this.chunks.filter((chunk) => chunk.status === Status.Ready) + if (this.isReady() && this.options.checkRequest) { + await this.checkRequest() + } + + const readyChunks = this.chunks.filter((chunk) => chunk.status === ChunkStatus.Ready) each(readyChunks, () => { - if (this.uploadingQueue.size >= this.opts.maxConcurrency) { - return false // false时break; + if (this.uploadingChunks.size >= this.options.maxConcurrency) { + return false } const chunk = readyChunks.shift() if (chunk) { - this.addUploadingQueue(chunk) + this.addUploadingChunk(chunk) } else { return false } }) - if (this.uploadingQueue.size) { + if (this.uploadingChunks.size > 0) { // 有的chunk处于pending状态,但是已经准备发起请求了,就不在后面调用其send方法 - const readyInUploadQueue = [...this.uploadingQueue].filter( - (chunk) => chunk.status === Status.Ready + const readyInUploadQueue = [...this.uploadingChunks].filter( + (chunk) => chunk.status === ChunkStatus.Ready ) Promise.race(readyInUploadQueue.map((chunk) => chunk.send())) return } - - const hasErrorChunk = this.chunks.some((chunk) => chunk.status === Status.Fail) + // console.log('hasErrorChunk', readyChunks, this.uploadingChunks) + const hasErrorChunk = this.chunks.some((chunk) => chunk.status === ChunkStatus.Fail) if (hasErrorChunk) { this.uploadFail() } else { @@ -154,40 +280,34 @@ export default class File { } } - changeStatus(status) { - this.status = status - // 兼容默认值时没有uploader实例 - if (this.uploader && this.uploader.emit) { - this.uploader.emit(Events.FileChange, this, this.uploader.fileList) + setProgress() { + const progress = this.chunks.reduce((total, chunk) => { + const p = this.options.fakeProgress ? chunk.fakeProgress : chunk.progress + return (total += p * (chunk.size / this.size)) + }, 0) + + this.progress = Math.min(1, progress) + + if (this.isUploadSuccess() || this.isSuccess()) { + this.progress = 1 } - } - uploadSuccess() { - this.changeStatus(Status.UploadSuccess) - this.uploader.emit(Events.FileUploadSuccess, this, this.uploader.fileList) + this.uploader.emitCallback(Callbacks.FileProgress, this) } uploadFail() { - this.changeStatus(Status.UploadFail) - this.uploader.emit(Events.FileUploadFail, this, this.uploader.fileList) - this.uploader.upload() - } - - success() { - this.changeStatus(Status.Success) - this.progress = 1 - this.uploader.emit(Events.FileSuccess, this, this.uploader.fileList) - this.uploader.upload() + this.changeStatus(FileStatus.UploadFail) + this.uploader.emitCallback(Callbacks.FileUploadFail, this) + this.continueUpload() } - mergeFail() { - this.changeStatus(Status.Fail) - this.uploader.emit(Events.FileFail, this, this.uploader.fileList) - this.uploader.upload() + uploadSuccess() { + this.changeStatus(FileStatus.UploadSuccess) + this.uploader.emitCallback(Callbacks.FileUploadSuccess, this) } merge() { - const merge = this.opts.mergeRequest + const merge = this.options.mergeRequest if (!isFunction(merge)) { this.success() return @@ -197,7 +317,9 @@ export default class File { if (isPromise(result)) { result.then( - () => this.success(), + (data) => { + data === true ? this.success() : this.mergeFail() + }, () => this.mergeFail() ) } else { @@ -205,80 +327,102 @@ export default class File { } } - retry() { - if (this.isUploadSuccess() || this.isFail()) { - this.merge() - return - } - if (this.isUploadFail()) { - each(this.chunks, (chunk) => { - console.log(chunk) - if (chunk.status === Status.Fail) { - chunk.status = Status.Ready - chunk.maxRetries = this.opts.maxRetries - } - }) + mergeFail() { + this.changeStatus(FileStatus.Fail) + this.uploader.emitCallback(Callbacks.FileFail, this) + this.continueUpload() + } - this.uploadFile() - } + success() { + this.changeStatus(FileStatus.Success) + this.progress = 1 + this.uploader.emitCallback(Callbacks.FileSuccess, this) + this.continueUpload() } - remove() { - this.chunks = [] - this.uploadingQueue.forEach((chunk) => { - chunk.abort() - }) + continueUpload() { + let firstPauseFile + for (let i = 0; i < this.uploader.fileList.length; i++) { + const file = this.uploader.fileList[i] + if (file.isPause()) { + firstPauseFile = file + break + } + } + if (firstPauseFile) { + firstPauseFile.resume() + } + + this.uploader.upload() } - pause() { - this.uploadingQueue.forEach((chunk) => { + cancel() { + this.uploadingChunks.forEach((chunk) => { chunk.abort() }) - this.changeStatus(Status.Pause) - this.uploader.upload() + this.uploadingChunks.clear() } - resume() { - if (this.isPause()) { - this.changeStatus(Status.Resume) - this.uploader.pauseUploadingFiles() - this.uploader.upload() + async remove() { + if (this.abortRead) { + this.abortRead() } - } - - isInited() { - return this.status === Status.Init - } - isReady() { - return this.status === Status.Ready - } + setTimeout(() => { + this.cancel() + this.chunks = [] + this.changeStatus('removed') - isUploading() { - return this.status === Status.Uploading + const index = this.uploader.fileList.indexOf(this) + if (index > -1) { + this.uploader.fileList.splice(index, 1) + } + this.uploader.emitCallback(Callbacks.FileRemove, this) + this.uploader.upload() + }, 0) } - isPause() { - return this.status === Status.Pause + pause() { + if (this.abortRead) { + this.abortRead() + } + setTimeout(() => { + this.cancel() + this.changeStatus(FileStatus.Pause) + this.uploader.emitCallback(Callbacks.FilePause, this) + this.uploader.upload() + }, 0) } - isResume() { - return this.status === Status.Resume + resume() { + if (this.isPause()) { + this.changeStatus(FileStatus.Resume) + this.uploader.emitCallback(Callbacks.FileResume, this) + this.uploader.upload() + } } - isUploadSuccess() { - return this.status === Status.UploadSuccess - } + retry() { + if (this.isCheckFail()) { + this.changeStatus(FileStatus.Ready) + this.upload() + return + } - isUploadFail() { - return this.status === Status.UploadFail - } + if (this.isUploadSuccess() || this.isFail()) { + this.merge() + return + } - isFail() { - return this.status === Status.Fail - } + if (this.isUploadFail()) { + each(this.chunks, (chunk) => { + if (chunk.status === ChunkStatus.Fail) { + chunk.status = ChunkStatus.Ready + chunk.maxRetries = chunk.options.maxRetries + } + }) - isSuccess() { - return this.status === Status.Success + this.upload() + } } } diff --git a/packages/sdk/src/core/Uploader.js b/packages/sdk/src/core/Uploader.js index d006cdc..84b98dd 100644 --- a/packages/sdk/src/core/Uploader.js +++ b/packages/sdk/src/core/Uploader.js @@ -1,170 +1,208 @@ -import File from '@/core/File' -import Container from '@/core/Container' -import { Event, extend, isFunction, isPromise } from '@/shared' -import { defaults } from '@/core/defaults' -import { Status, Events, CheckStatus } from '@/core/constans' +import Container from './Container' +import Event from './Event' +import File from './File' +import { defaultOptions } from './defaults' +import { Callbacks, FileStatus } from './constants' +import { extend, isString, isArray, isFunction, isPromise } from '../shared' -class Uploader { +export default class Uploader { constructor(options) { this.container = new Container(this) this.event = new Event() - this.opts = extend({}, defaults, options) - let fileList = [] - if (this.opts.fileList && this.opts.fileList.length) { - fileList = this.opts.fileList.map( - (item) => - new File({ - name: item.name, - path: item.path, - status: Status.Success, - progress: 1 - }) + + this.options = extend(defaultOptions, options) + this.fileList = this.options.fileList || [] + this.listenerFiles() + } + + assignBrowse(domNode, attributes) { + if (attributes) { + const { accept } = attributes + if (accept) { + if (isString(accept)) { + attributes.accept = accept + } else if (isArray(accept)) { + attributes.accept = accept.join(',') + } + } + extend( + { + multiple: this.options.multiple, + accept: this.options.accept + }, + attributes ) + } else { + attributes = { + multiple: this.options.multiple, + accept: this.options.accept + } } - this.fileList = fileList - this.status = Status.Init - // 只注册一次 - this.listenerFiles() + + this.container.assignBrowse(domNode, attributes) } - on(name, func) { - this.event.on(name, func) + assignDrop(domNode) { + this.container.assignDrop(domNode) + } + + on(name, fn) { + this.event.on(name, fn) } emit(name, ...args) { this.event.emit(name, ...args) } - async addFiles(files) { - const { limit, beforeAdd, attributes } = this.opts + emitCallback(name, ...args) { + this.emit(name, ...args, this.fileList) + } - if (limit > 0) { - if (files.length + this.fileList.length > limit) { - this.emit(Events.Exceed, files, this.fileList) + listenerFiles() { + const emitAllSuccess = (file, fileList) => { + if (!fileList.length) { return } + const allSuccess = fileList.every((file) => file.isSuccess()) + if (allSuccess) { + this.emit(Callbacks.AllFileSuccess, this.fileList) + } } - let originFileList = [...files] + this.on(Callbacks.FileSuccess, emitAllSuccess) + this.on(Callbacks.FileRemove, emitAllSuccess) + } - if (!this.opts.multiple) { - originFileList = originFileList.slice(0, 1) + setDefaultFileList(fileList) { + fileList.forEach((file) => { + this.fileList.push( + new File( + { + ...file, + name: file.name, + readProgress: 1, + progress: 1, + status: FileStatus.Success + }, + this + ) + ) + }) + } + + async addFiles(arrayLike, e) { + const { limit, beforeAdd } = this.options + const originFiles = [...arrayLike] + + if (limit > 0) { + if (originFiles.length + this.fileList.length > limit) { + this.emitCallback(Callbacks.Exceed, originFiles) + return + } } - if (originFileList.length === 0) { + if (originFiles.length === 0) { return } - const newFileList = originFileList.map((file) => new File(file, this)) - this.fileList = [...this.fileList, ...newFileList] - - if (beforeAdd) { - if (isFunction(beforeAdd)) { - for (let i = 0; i < newFileList.length; i++) { - const file = newFileList[i] - const before = beforeAdd(file) - if (isPromise(before)) { - before.then( - () => {}, - () => { - this.doRemove(file) - } - ) - } else if (before !== false) { - // + if (!this.options.multiple) { + originFiles = originFiles.slice(0, 1) + } + + let newFileList = originFiles.map((file) => new File(file, this)) + + const fileAdd = (file) => { + this.fileList.push(file) + this.emitCallback(Callbacks.FileAdded, file) + } + + const fileRemove = (file) => { + newFileList = newFileList.filter((item) => item.uid !== file.uid) + this.emitCallback(Callbacks.FileRemove, file) + } + + const fileAddFail = (file) => { + if (this.options.addFailToRemove) { + this.emitCallback(Callbacks.FileAddFail, file) + fileRemove(file) + } else { + this.fileList.push(file) + file.changeStatus(FileStatus.AddFail) + this.emitCallback(Callbacks.FileAddFail, file) + } + } + + const handleBefore = async (file) => { + if (beforeAdd && isFunction(beforeAdd)) { + const before = beforeAdd(file) + if (isPromise(before)) { + try { + await before + fileAdd(file) + } catch (e) { + fileAddFail(file) + } + } else { + if (before !== false) { + fileAdd(file) } else { - this.doRemove(file) + fileAddFail(file) } } + } else { + fileAdd(file) } } - this.emit(Events.FilesAdded, this.fileList) - this.status = Status.Ready + await Promise.all(newFileList.map((file) => handleBefore(file))) - if (this.opts.autoUpload) { - this.submit() + if (newFileList.length > 0) { + this.emitCallback(Callbacks.FilesAdded, this.fileList) } - } - pauseUploadingFiles() { - const uploadingFiles = this.fileList.filter((file) => file.isUploading()) - uploadingFiles.forEach((file) => { - file.pause() - }) + if (this.options.autoUpload) { + this.submit() + } } async upload() { + if (this.fileList.length === 0) return + for (let i = 0; i < this.fileList.length; i++) { const file = this.fileList[i] - if (file.isUploading()) { - return - } - if (file.isResume()) { - file.status = Status.Uploading - file.uploadFile() - return + if (file.isAddFail()) { + continue } - if (file.isReady()) { - file.uploadFile() + + if (file.isUploading() || file.isReading()) { return } - } - } - async submit() { - const initedFileList = this.fileList.filter((file) => file.isInited()) - // 第一个file ready之后就开始上传,避免多个ready状态的file同时上传 - try { - await Promise.race(initedFileList.map((file) => file.start())) - } catch (e) { - console.log(e) - } - this.upload() - } + if (file.isResume()) { + // console.log(file.prevStatusLastRecord) + // [uploading, pause, resume] 回到pause之前的状态 + const prevStatus = file.prevStatusLastRecord[file.prevStatusLastRecord.length - 2] - listenerFiles() { - const emitAllSuccess = (fiel, fileList) => { - if (!fileList.length) { + if (prevStatus) { + file.changeStatus(prevStatus) + } + file.upload() return } - const allSuccess = fileList.every((file) => file.isSuccess()) - if (allSuccess) { - this.emit(Events.AllFileSuccess, this.fileList) - this.status = Status.Success - } - } - - this.on(Events.FileSuccess, emitAllSuccess) - this.on(Events.FileRemove, emitAllSuccess) - } - doRemove(file) { - if (!file) { - this.clear() - return - } - const index = this.fileList.indexOf(file) - if (index > -1) { - file.remove() - this.fileList.splice(index, 1) - this.emit(Events.FileRemove, file, this.fileList) - this.upload() + if (file.isReady() || file.isInit()) { + file.upload() + return + } } } - clear() { - for (let i = 0; i < this.fileList.length; i++) { - const file = this.fileList[i] - file.remove() - this.emit(Events.FileRemove, file, this.fileList) - } - this.fileList = [] - this.emit(Events.FileRemove, null, []) + submit() { + this.upload() } remove(file) { - const { beforeRemove } = this.opts + const { beforeRemove } = this.options if (!beforeRemove) { this.doRemove(file) } else if (isFunction(beforeRemove)) { @@ -179,15 +217,26 @@ class Uploader { } } - retry(file) { - const index = this.fileList.indexOf(file) - if (index > -1) { - file.retry() - this.upload() + clear() { + // 倒序删除 + for (let i = this.fileList.length - 1; i >= 0; i--) { + const file = this.fileList[i] + file.remove() } + this.fileList = [] + } + + doRemove(file) { + if (!file) { + this.clear() + return + } + + file.remove() } pause(file) { + if (!file) return const index = this.fileList.indexOf(file) if (index > -1) { file.pause() @@ -195,35 +244,29 @@ class Uploader { } resume(file) { - const index = this.fileList.indexOf(file) - if (index > -1) { - file.resume() - } - } + if (!file) return + const uploadingFiles = this.fileList.filter((file) => { + return file.isUploading() || file.isReading() + }) - assignBrowse(domNode, attributes = {}) { - const attrs = extend( - {}, - { - accept: this.opts.accept, - multiple: this.opts.multiple - }, - attributes - ) - this.container.assignBrowse(domNode, attrs) + uploadingFiles.forEach((item) => { + item.pause() + }) + file.resume() } - assignDrop(domNode) { - this.container.assignDrop(domNode) - } -} + retry(file) { + if (!file) return + const uploadingFiles = this.fileList.filter((file) => { + return file.isUploading() || file.isReading() + }) -Uploader.Status = Status -Uploader.Events = Events -Uploader.File = File -Uploader.CheckStatus = CheckStatus -Uploader.create = (options) => { - return new Uploader(options) + uploadingFiles.forEach((item) => { + item.pause() + }) + const index = this.fileList.indexOf(file) + if (index > -1) { + file.retry() + } + } } - -export default Uploader diff --git a/packages/sdk/src/core/constans.js b/packages/sdk/src/core/constans.js deleted file mode 100644 index c00be11..0000000 --- a/packages/sdk/src/core/constans.js +++ /dev/null @@ -1,35 +0,0 @@ -// File和Chunk的状态是共用,有重合 -export const Status = { - Init: 'init', // 文件初始化状态 - Reading: 'reading', // 计算hash,读取文件hash中 - Ready: 'ready', // 1. 文件hash计算完成;2. chunk初始化状态是Ready - Pending: 'pending', // 1. chunk的已经发起请求,Promise处于Pending状态 - Uploading: 'uploading', // 1. 文件在上传中 2. chunk上传中 - UploadSuccess: 'uploadSuccess', // 文件的所有chunk上传完成, 准备合并文件 - UploadFail: 'uploadFail', // 文件所有chunk已经请求上传接口,但是自动重试之后仍有失败时,文件状态为UploadFail - Success: 'success', // 1. 文件合并成功 2. chunk上传成功 - Fail: 'fail', // 1. 文件合并失败 2. chunk上传(所有重试都不成功)失败 - Pause: 'pause', // 暂停状态 - Resume: 'resume' // 恢复状态 -} - -export const Events = { - Exceed: 'exceed', - FilesAdded: 'filesAdded', - FileChange: 'fileChange', - FileRemove: 'fileRemove', - FileProgress: 'fileProgress', - FileFail: 'fileFail', - FileUploadFail: 'fileUploadFail', - FileUploadSuccess: 'fileUploadSuccess', - FileSuccess: 'fileSuccess', - // FileMergeFail: 'fileMergeFail', - AllFileSuccess: 'allFilesSuccess', - Change: 'change' -} - -export const CheckStatus = { - Part: 'part', - Success: 'success', - None: 'none' -} diff --git a/packages/sdk/src/core/constants.js b/packages/sdk/src/core/constants.js new file mode 100644 index 0000000..fce47d4 --- /dev/null +++ b/packages/sdk/src/core/constants.js @@ -0,0 +1,146 @@ +// File和Chunk的状态是共用,有重合 +export const Status = { + Init: 'init', // 文件初始化状态 + Reading: 'reading', // 计算hash,读取文件hash中 + Ready: 'ready', // 1. 文件hash计算完成;2. chunk初始化状态是Ready + Pending: 'pending', // 1. chunk的已经发起请求,Promise处于Pending状态 + Uploading: 'uploading', // 1. 文件在上传中 2. chunk上传中 + UploadSuccess: 'uploadSuccess', // 文件的所有chunk上传完成, 准备合并文件 + UploadFail: 'uploadFail', // 文件所有chunk已经请求上传接口,但是自动重试之后仍有失败时,文件状态为UploadFail + Success: 'success', // 1. 文件合并成功 2. chunk上传成功 + Fail: 'fail', // 1. 文件合并失败 2. chunk上传(所有重试都不成功)失败 + Pause: 'pause', // 暂停状态 + Resume: 'resume' // 恢复状态 +} + +// 文件状态 +export const FileStatus = { + // 文件初始化状态 + Init: 'init', + + // 文件添加失败, 添加文件时允许beforeAdd中失败的文件添加到列表,但是状态为AddFail + AddFail: 'addFail', + + // 文件读取中(计算hash中) + Reading: 'reading', + + // 文件hash计算完成;准备上传 + Ready: 'ready', + + // checkRequest 存在时,切checkRequest失败 + CheckFail: 'checkFail', + + // 文件上传中 + Uploading: 'uploading', + + // 文件上传完成;所有chunk上传完成,准备合并文件 + UploadSuccess: 'uploadSuccess', + + // 文件上传失败;有chunk上传失败 + UploadFail: 'uploadFail', + + // 文件上传成功 且 合并成功 + Success: 'success', + + // 文件合并失败 + Fail: 'fail', + + // 文件暂停上传 + Pause: 'pause', + + // 文件恢复上传 + Resume: 'resume' +} + +// chunk状态 +export const ChunkStatus = { + // chunk初始化状态是Ready + Ready: 'ready', + + // chunk创建请求成功,Promise处于Pending状态 + Pending: 'pending', + + // chunk上传中 + Uploading: 'uploading', + + // chunk上传成功 + Success: 'success', + + // chunk上传失败(所有重试次数完成后 都不成功) + Fail: 'fail' +} + +export const Events = { + Exceed: 'exceed', + FilesAdded: 'filesAdded', + FileChange: 'fileChange', + FileRemove: 'fileRemove', + FileProgress: 'fileProgress', + FileFail: 'fileFail', + FileUploadFail: 'fileUploadFail', + FileUploadSuccess: 'fileUploadSuccess', + FileSuccess: 'fileSuccess', + // FileMergeFail: 'fileMergeFail', + AllFileSuccess: 'allFilesSuccess', + Change: 'change' +} + +// 回调函数名称 +export const Callbacks = { + // 文件超出limit限制 + Exceed: 'exceed', + + // 单个文件添加成功 + FileAdded: 'fileAdded', + + // 文件添加失败 + FileAddFail: 'fileAddFail', + + // 所有文件添加成功 + FilesAdded: 'filesAdded', + + // 文件状态改变 + FileChange: 'fileChange', + + // 文件删除 + FileRemove: 'fileRemove', + + // 文件开始计算hash + FileReadStart: 'fileReadStart', + + // 文件计算进度 + FileReadProgress: 'fileReadProgress', + + // 文件hash计算完成 + FileReadEnd: 'fileReadEnd', + + // 文件上传进度 + FileProgress: 'fileProgress', + + // 文件上传成功 + FileUploadSuccess: 'fileUploadSuccess', + + // 文件上传失败 + FileUploadFail: 'fileUploadFail', + + // 文件合并成功 + FileSuccess: 'fileSuccess', + + // 文件上传失败合并失败 + FileFail: 'fileFail', + + // 所有文件上传成功 + AllFileSuccess: 'allFileSuccess' +} + +// check 文件上传状态 +export const CheckStatus = { + // 部分上传成功 + Part: 'part', + + // 上传成功 + Success: 'success', + + // 文件还没上传 + None: 'none' +} diff --git a/packages/sdk/src/core/defaults.js b/packages/sdk/src/core/defaults.js index c280f2b..079daeb 100644 --- a/packages/sdk/src/core/defaults.js +++ b/packages/sdk/src/core/defaults.js @@ -1,68 +1,32 @@ -import { sleep } from '@/shared' -import { CheckStatus } from './constans' +export const defaultOptions = { + // input 属性相关 + accept: '*', + multiple: true, -export const defaults = { - multipart: true, // TODO: 是否分片上传,false时单文件上传 + // 文件相关 + fileList: [], + limit: 10, + autoUpload: true, + customGenerateUid: null, + beforeAdd: (file) => true, + beforeRemove: (file) => true, + addFailToRemove: true, + chunkSize: 2 * 1024 * 1024, // 2M + fakeProgress: true, + withHash: true, + useWebWoker: false, - /** - * request - */ - action: 'https://jsonplaceholder.typicode.com/posts', + // 上传逻辑相关 + name: 'file', + action: '', + customRequest: null, withCredentials: true, headers: {}, data: {}, + requestSucceed: (xhr) => [200, 201, 202, 206].includes(xhr.status), maxConcurrency: 6, - requestSucceed(xhr) { - return [200, 201, 202].includes(xhr.status) - }, maxRetries: 3, retryInterval: 1000, - async checkFileRequest(file) { - // await sleep(1000) - // 成功 - // return { - // status: 'success', - // data: 'http://google.com' - // } - - // 没上传 - return { - status: CheckStatus.None - } - - // 部分成功 - // return { - // status: CheckStatus.Part, - // data: [0, 1, 2, 3, 4, 5, 6] - // } - }, - mergeRequest: async (file) => { - // await sleep(5000) - // file.path = 'http://baidu.com' - return true - }, - - /** - * file option - */ - chunkSize: 1024 * 4, - autoUpload: true, - name: 'file', - limit: 10, - withHash: true, - computedHashWorker: true, - fakeProgress: true, - customGenerateUid: null, - beforeAdd(file) { - return true - }, - beforeRemove(file) { - return true - }, - - /** - * input - */ - multiple: true, - accept: '*' + checkRequest: null, + mergeRequest: (file) => true } diff --git a/packages/sdk/src/core/request.js b/packages/sdk/src/core/request.js new file mode 100644 index 0000000..56fba9d --- /dev/null +++ b/packages/sdk/src/core/request.js @@ -0,0 +1,32 @@ +export function request(options) { + const { action, formData, data, headers, withCredentials, onSuccess, onFail, onProgress } = + options + + Object.keys(data).forEach((key) => { + formData.append(key, data[key]) + }) + + let xhr = new XMLHttpRequest() + xhr.responseType = 'json' + xhr.withCredentials = withCredentials + xhr.open('POST', action, true) + + // 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED + if ('setRequestHeader' in xhr) { + Object.keys(headers).forEach((key) => { + xhr.setRequestHeader(key, headers[key]) + }) + } + + xhr.upload.addEventListener('progress', onProgress) + xhr.addEventListener('load', (e) => onSuccess(e, xhr), false) + xhr.addEventListener('error', onFail, false) + xhr.send(formData) + + return { + abort() { + xhr.abort() + xhr = null + } + } +} diff --git a/packages/sdk/src/index.js b/packages/sdk/src/index.js index f7a657f..b926de9 100644 --- a/packages/sdk/src/index.js +++ b/packages/sdk/src/index.js @@ -1,10 +1,12 @@ import Uploader from '@/core/Uploader.js' import File from '@/core/File' import Chunk from '@/core/Chunk' -import { Status, Events, CheckStatus } from '@/core/constans' +import { Callbacks, FileStatus, ChunkStatus, CheckStatus } from '@/core/constants' const create = (options) => { return new Uploader(options) } + export default Uploader -export { File, Status, Events, CheckStatus, Chunk, create } + +export { File, Chunk, create, FileStatus, ChunkStatus, Callbacks, CheckStatus } diff --git a/packages/sdk/src/shared/hash-worker.js b/packages/sdk/src/shared/hash-worker.js index a7a8a58..5e3f418 100644 --- a/packages/sdk/src/shared/hash-worker.js +++ b/packages/sdk/src/shared/hash-worker.js @@ -731,7 +731,7 @@ const md5 = ` export const workerCode = `self.onmessage = (e) => { const { file, chunkSize, type } = e.data if(type === 'DONE') { - // console.log('worker closed !') + console.log('Hash calculation closed !') self.close() return } @@ -743,7 +743,18 @@ export const workerCode = `self.onmessage = (e) => { const startTime = Date.now() let currentChunk = 0 + // 创建 AbortController 用于中断 + const controller = new AbortController() + const signal = controller.signal + + signal.addEventListener('abort', () => { + fileReader.abort() // 中断 FileReader + self.postMessage({ error: new Error('Hash calculation cancelled'), progress: 0 }) + }) + fileReader.onload = (e) => { + if (signal.aborted) return + spark.append(e.target.result) currentChunk++ @@ -762,11 +773,13 @@ export const workerCode = `self.onmessage = (e) => { } fileReader.onerror = (error) => { + if (signal.aborted) return console.warn('oops, something went wrong.') - self.postMessage({ error }) + self.postMessage({ error, progress: 0 }) } function loadNext() { + if (signal.aborted) return const start = currentChunk * chunkSize const end = start + chunkSize >= file.size ? file.size : start + chunkSize @@ -774,33 +787,53 @@ export const workerCode = `self.onmessage = (e) => { } loadNext() + + return { + abort: () => controller.abort() + } } ` export const isSupportWorker = !!window.Worker export const computedHashWorker = ({ file, chunkSize }, callback) => { console.log('In Web Worker') - const worker = new Worker(URL.createObjectURL(new Blob([workerCode]))) + let abortComputedHash + const worker = new Worker(URL.createObjectURL(new Blob([workerCode]))) const close = () => { worker.postMessage({ type: 'DONE' }) worker.terminate() } - worker.postMessage({ file, chunkSize }) + const promise = new Promise((resolve, reject) => { + worker.postMessage({ file, chunkSize }) - worker.onmessage = (e) => { - const { error, progress, hash, time } = e.data - if (error) { - callback(error) - close() - return + worker.onmessage = (e) => { + const { error, progress, hash, time } = e.data + if (error) { + callback(error) + close() + reject(error) + return + } + if (progress === 100) { + close() + resolve({ progress, hash, time }) + callback(null, { progress, hash, time }) + return + } + callback(null, { progress }) } - if (progress === 100) { - close() - callback(null, { progress, hash, time }) - return + + abortComputedHash = { reject } + }) + + return { + promise, + abort: () => { + if (!abortComputedHash) return + worker.terminate() + abortComputedHash.reject() } - callback(null, { progress }) } } diff --git a/packages/sdk/src/shared/hash.js b/packages/sdk/src/shared/hash.js index 36f35ca..5ac4bed 100644 --- a/packages/sdk/src/shared/hash.js +++ b/packages/sdk/src/shared/hash.js @@ -1,65 +1,6 @@ import * as SparkMD5 from 'spark-md5' import { isSupportWorker, computedHashWorker } from './hash-worker' -export const getHash = (data) => { - return new Promise((resolve, reject) => { - const fileReader = new FileReader() - fileReader.onload = (e) => { - const fileHash = SparkMD5.ArrayBuffer.hash(e.target.result) - resolve(fileHash) - } - fileReader.onerror = () => { - reject('文件读取失败') - } - fileReader.readAsArrayBuffer(data) - }) -} - -export const computedHash = ({ file, chunkSize, totalChunks }, callback) => { - return new Promise((resolve, reject) => { - const spark = new SparkMD5.ArrayBuffer() - const fileReader = new FileReader() - let currentChunk = 0 - const startTime = Date.now() - - fileReader.onload = function (e) { - spark.append(e.target.result) - currentChunk++ - - if (currentChunk < totalChunks) { - loadNext() - callback && - callback({ - progress: (currentChunk / totalChunks) * 100 - }) - } else { - console.info('computed hash') - const result = { - hash: spark.end(), - time: Date.now() - startTime, - progress: 100 - } - callback && callback(result) - resolve(result) - } - } - - fileReader.onerror = function () { - console.warn('oops, something went wrong.') - reject() - } - - function loadNext() { - const start = currentChunk * chunkSize - const end = start + chunkSize >= file.size ? file.size : start + chunkSize - - fileReader.readAsArrayBuffer(slice.call(file, start, end)) - } - - loadNext() - }) -} - const computedHashNormal = ({ file, chunkSize }, callback) => { const slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice const spark = new SparkMD5.ArrayBuffer() @@ -68,10 +9,20 @@ const computedHashNormal = ({ file, chunkSize }, callback) => { const startTime = Date.now() let currentChunk = 0 + // 创建 AbortController 用于中断 + const controller = new AbortController() + const signal = controller.signal + + signal.addEventListener('abort', () => { + fileReader.abort() // 中断 FileReader + callback(new Error('Hash calculation cancelled'), { progress: 0 }) + }) + fileReader.onload = function (e) { + if (signal.aborted) return + spark.append(e.target.result) currentChunk++ - if (currentChunk < totalChunks) { loadNext() callback(null, { @@ -84,15 +35,18 @@ const computedHashNormal = ({ file, chunkSize }, callback) => { progress: 100 } callback(null, result) + return } } fileReader.onerror = function (error) { - console.warn('oops, something went wrong.') - callback(error) + if (signal.aborted) return + console.warn('Hash calculation error') + callback(error, { progress: 0 }) } function loadNext() { + if (signal.aborted) return const start = currentChunk * chunkSize const end = start + chunkSize >= file.size ? file.size : start + chunkSize @@ -100,12 +54,22 @@ const computedHashNormal = ({ file, chunkSize }, callback) => { } loadNext() + + return { + abort: () => controller.abort() + } } -export const asyncComputedHash = ({ file, chunkSize, inWorker }, callback) => { - return new Promise((resolve, reject) => { - const computedHash = isSupportWorker && inWorker ? computedHashWorker : computedHashNormal - computedHash( +export const asyncCancellableComputedHash = ( + { file, chunkSize, useWebWoker = false }, + callback +) => { + let abortComputedHash + + const promise = new Promise((resolve, reject) => { + const computedHash = isSupportWorker && useWebWoker ? computedHashWorker : computedHashNormal + + const { abort } = computedHash( { file, chunkSize @@ -120,5 +84,15 @@ export const asyncComputedHash = ({ file, chunkSize, inWorker }, callback) => { callback && callback({ progress }) } ) + abortComputedHash = { abort, reject } }) + + return { + promise, + abort: () => { + if (!abortComputedHash) return + abortComputedHash.abort() + abortComputedHash.reject() + } + } } diff --git a/packages/sdk/src/shared/index.js b/packages/sdk/src/shared/index.js index 87987f0..24e1609 100644 --- a/packages/sdk/src/shared/index.js +++ b/packages/sdk/src/shared/index.js @@ -7,10 +7,25 @@ export const sleep = (time = 600, mockError = false) => { }) } +export const throttle = (fn, wait = 300) => { + // 上一次执行 fn 的时间 + let previous = 0 + // 将 throttle 处理结果当作函数返回 + return function (...args) { + // 获取当前时间,转换成时间戳,单位毫秒 + let now = +new Date() + // 将当前时间和上一次执行函数的时间进行对比 + // 大于等待时间就把 previous 设置为当前时间并执行函数 fn + if (now - previous > wait) { + previous = now + fn.apply(this, args) + } + } +} + export * from './uid' export * from './types' export * from './array' export * from './object' -export * from './event' export * from './hash' export * from './blob' diff --git a/packages/sdk/src/shared/types.js b/packages/sdk/src/shared/types.js index 3d18be1..1d258e3 100644 --- a/packages/sdk/src/shared/types.js +++ b/packages/sdk/src/shared/types.js @@ -22,3 +22,7 @@ export const isArray = export const isPromise = (promise) => { return promise && isFunction(promise.then) } + +export const isString = function (a) { + return typeof a === 'string' +} diff --git a/packages/sdk/vite.config.js b/packages/sdk/vite.config.js index b33917a..010468f 100644 --- a/packages/sdk/vite.config.js +++ b/packages/sdk/vite.config.js @@ -8,12 +8,11 @@ export default defineConfig({ } }, server: { - open: '/example/index.html' + open: '/examples/quick-start/index.html' }, build: { lib: { entry: './src/index.js', - // formats: ['es', 'umd'], name: 'UploaderSdk', fileName: 'sdk' }, From fa0e5c8ffdf724aff9961c9d46f85772a12780b1 Mon Sep 17 00:00:00 2001 From: moyuderen Date: Tue, 8 Apr 2025 18:53:07 +0800 Subject: [PATCH 02/39] =?UTF-8?q?refactor(server):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/app.controller.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts index fb373ec..eb28303 100644 --- a/server/src/app.controller.ts +++ b/server/src/app.controller.ts @@ -3,6 +3,7 @@ import { Controller, Get, Post, + Query, UploadedFile, UseInterceptors, } from '@nestjs/common'; @@ -40,12 +41,27 @@ export class AppController { return { data: true }; } - @Post('merge') - async merge(@Body() body) { - const { hash, name: filename } = body; + @Get('merge') + async merge( + @Query('hash') hash: string, + @Query('filename') filename: string, + ) { const chunkDir = `uploads/${hash}_${filename}`; const files = fs.readdirSync(chunkDir); + // 按顺序合并 + files.sort((aVal, bVal) => { + const a = parseInt(aVal); + const b = parseInt(bVal); + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + return 0; + }); + let startPos = 0; files.map((file) => { const filePath = chunkDir + '/' + file; @@ -65,9 +81,12 @@ export class AppController { }; } - @Post('checkFile') - async checkFile(@Body() body) { - const { status } = body; + @Get('check') + async checkFile( + @Query('hash') hash: string, + @Query('filename') filename: string, + @Query('status') status: string, + ) { await sleep(500); if (status === 'success') { return { From 0e7e3eaa257fce1d4c47f14a3243378e4b5be354 Mon Sep 17 00:00:00 2001 From: moyuderen Date: Thu, 10 Apr 2025 14:45:33 +0800 Subject: [PATCH 03/39] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sdk/src/core/File.js | 39 +++++++++++++++++++++++------- packages/sdk/src/core/Uploader.js | 40 +++++++++++-------------------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/packages/sdk/src/core/File.js b/packages/sdk/src/core/File.js index 4807066..46faf91 100644 --- a/packages/sdk/src/core/File.js +++ b/packages/sdk/src/core/File.js @@ -10,8 +10,8 @@ import { } from '../shared' export default class File { - constructor(file, uploader) { - this.uploader = uploader || { options: {} } + constructor(file, uploader, defaults) { + this.uploader = uploader this.options = this.uploader.options this.uid = this.generateId() @@ -23,9 +23,9 @@ export default class File { this.size = file.size this.type = file.type this.hash = '' - this.url = file.url || '' + this.url = '' - this.progress = file.progress || 0 + this.progress = 0 this.chunkSize = this.options.chunkSize this.chunks = [] this.totalChunks = 0 @@ -33,7 +33,19 @@ export default class File { this.readProgress = 0 this.errorMessage = '' - this.changeStatus(file.status || FileStatus.Init) + if (defaults) { + Object.keys(defaults).forEach((key) => { + this[key] = defaults[key] + }) + + this.name = defaults.name + this.url = defaults.url + this.readProgress = 1 + this.progress = 1 + this.changeStatus(FileStatus.Success) + } else { + this.changeStatus(FileStatus.Init) + } } generateId() { @@ -59,7 +71,7 @@ export default class File { const value = this.size const ONE_KB = 1024 if (null == value || value == '') { - return '0 Bytes' + return '0 B' } var unitArr = new Array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB') var index = 0 @@ -67,7 +79,7 @@ export default class File { index = Math.floor(Math.log(srcsize) / Math.log(ONE_KB)) var size = srcsize / Math.pow(ONE_KB, index) size = size.toFixed(2) //保留的小数位数 - return size + unitArr[index] + return size + ' ' + unitArr[index] } changeStatus(newStatus) { @@ -209,6 +221,7 @@ export default class File { const rejectCheck = (reject) => { this.changeStatus(FileStatus.CheckFail) + this.uploader.upload() reject(new Error('checkRequest error')) } @@ -220,8 +233,12 @@ export default class File { const result = check(this) if (isPromise(result)) { - const data = await result - data ? checkStatusFn(data.status, data.data, resolve) : rejectCheck(reject) + try { + const data = await result + data ? checkStatusFn(data.status, data.data, resolve) : rejectCheck(reject) + } catch { + rejectCheck(reject) + } } else { result ? checkStatusFn(data.status, data.data, resolve) : rejectCheck(reject) } @@ -403,6 +420,10 @@ export default class File { } retry() { + if (this.isAddFail()) { + return + } + if (this.isCheckFail()) { this.changeStatus(FileStatus.Ready) this.upload() diff --git a/packages/sdk/src/core/Uploader.js b/packages/sdk/src/core/Uploader.js index 84b98dd..df853d1 100644 --- a/packages/sdk/src/core/Uploader.js +++ b/packages/sdk/src/core/Uploader.js @@ -75,18 +75,7 @@ export default class Uploader { setDefaultFileList(fileList) { fileList.forEach((file) => { - this.fileList.push( - new File( - { - ...file, - name: file.name, - readProgress: 1, - progress: 1, - status: FileStatus.Success - }, - this - ) - ) + this.fileList.push(new File(file, this, file)) }) } @@ -116,20 +105,10 @@ export default class Uploader { this.emitCallback(Callbacks.FileAdded, file) } - const fileRemove = (file) => { - newFileList = newFileList.filter((item) => item.uid !== file.uid) - this.emitCallback(Callbacks.FileRemove, file) - } - const fileAddFail = (file) => { - if (this.options.addFailToRemove) { - this.emitCallback(Callbacks.FileAddFail, file) - fileRemove(file) - } else { - this.fileList.push(file) - file.changeStatus(FileStatus.AddFail) - this.emitCallback(Callbacks.FileAddFail, file) - } + this.fileList.push(file) + file.changeStatus(FileStatus.AddFail) + this.emitCallback(Callbacks.FileAddFail, file) } const handleBefore = async (file) => { @@ -156,6 +135,15 @@ export default class Uploader { await Promise.all(newFileList.map((file) => handleBefore(file))) + this.fileList = this.fileList.filter((file) => { + if (file.isAddFail() && this.options.addFailToRemove === true) { + this.doRemove(file) + return false + } else { + } + return true + }) + if (newFileList.length > 0) { this.emitCallback(Callbacks.FilesAdded, this.fileList) } @@ -170,7 +158,7 @@ export default class Uploader { for (let i = 0; i < this.fileList.length; i++) { const file = this.fileList[i] - if (file.isAddFail()) { + if (file.isAddFail() || file.isCheckFail()) { continue } From ba2bf4b90a8a1a199c7a65da709e57b1ffc5f0fb Mon Sep 17 00:00:00 2001 From: moyuderen Date: Thu, 10 Apr 2025 14:46:05 +0800 Subject: [PATCH 04/39] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8=E6=9C=80?= =?UTF-8?q?=E6=96=B0sdk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vue/example/App.vue | 108 ++++---- packages/vue/index.html | 2 +- packages/vue/src/components/file-icon.vue | 2 +- packages/vue/src/components/loading-icon.vue | 45 ++++ packages/vue/src/components/pause-icon.vue | 2 +- packages/vue/src/components/play-icon.vue | 2 +- packages/vue/src/components/remove-icon.vue | 2 +- packages/vue/src/components/retry-icon.vue | 2 +- packages/vue/src/components/upload-icon.vue | 2 +- packages/vue/src/components/uploader-btn.vue | 10 +- packages/vue/src/components/uploader-drop.vue | 2 + packages/vue/src/components/uploader-file.vue | 176 +++++++++---- packages/vue/src/components/uploader-list.vue | 23 +- packages/vue/src/components/uploader.vue | 233 ++++++++++-------- 14 files changed, 393 insertions(+), 218 deletions(-) create mode 100644 packages/vue/src/components/loading-icon.vue diff --git a/packages/vue/example/App.vue b/packages/vue/example/App.vue index 7cadcb3..aed0682 100644 --- a/packages/vue/example/App.vue +++ b/packages/vue/example/App.vue @@ -1,10 +1,7 @@