diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 64627cb..b92bd3a 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -60,7 +60,7 @@ jobs:
run: pnpm i
- name: Build with VitePress
- run: pnpm build:docs
+ run: pnpm docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
diff --git a/.gitignore b/.gitignore
index 6874957..3710000 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,6 @@ refs
objects
info
hooks
+
+# server
+server/uploads
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2d442a3..c6f8a28 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,5 +15,11 @@
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[javascript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
diff --git a/docs/.vitepress/cache/deps/_metadata.json b/docs/.vitepress/cache/deps/_metadata.json
index bf88f90..775d65d 100644
--- a/docs/.vitepress/cache/deps/_metadata.json
+++ b/docs/.vitepress/cache/deps/_metadata.json
@@ -1,43 +1,43 @@
{
- "hash": "b2a223ce",
- "configHash": "3c801238",
- "lockfileHash": "2eb6c3e6",
- "browserHash": "5197b7f6",
+ "hash": "c068ed14",
+ "configHash": "ca39393b",
+ "lockfileHash": "64117e8b",
+ "browserHash": "8a8cc074",
"optimized": {
"vue": {
"src": "../../../../node_modules/.pnpm/vue@3.4.35_typescript@5.5.4/node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
- "fileHash": "b116bb2d",
+ "fileHash": "c1ba99dc",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../node_modules/.pnpm/@vue+devtools-api@7.3.7/node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
- "fileHash": "d624aba5",
+ "fileHash": "105dd1ac",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../node_modules/.pnpm/@vueuse+core@10.11.0_vue@3.4.35_typescript@5.5.4_/node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
- "fileHash": "7caa917e",
+ "fileHash": "ff660189",
"needsInterop": false
},
"vitepress > @vueuse/integrations/useFocusTrap": {
"src": "../../../../node_modules/.pnpm/@vueuse+integrations@10.11.0_focus-trap@7.5.4_vue@3.4.35_typescript@5.5.4_/node_modules/@vueuse/integrations/useFocusTrap.mjs",
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
- "fileHash": "d94660bf",
+ "fileHash": "60206fc0",
"needsInterop": false
},
"vitepress > mark.js/src/vanilla.js": {
"src": "../../../../node_modules/.pnpm/mark.js@8.11.1/node_modules/mark.js/src/vanilla.js",
"file": "vitepress___mark__js_src_vanilla__js.js",
- "fileHash": "d534ef1e",
+ "fileHash": "7f67ece3",
"needsInterop": false
},
"vitepress > minisearch": {
"src": "../../../../node_modules/.pnpm/minisearch@7.1.0/node_modules/minisearch/dist/es/index.js",
"file": "vitepress___minisearch.js",
- "fileHash": "f3681b8a",
+ "fileHash": "5b665f23",
"needsInterop": false
}
},
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index c7254e5..d7cc444 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -12,7 +12,6 @@ export default {
})
},
enhanceApp({ app, router, siteData }) {
- // ...
- console.log(app)
+ // console.log(app)
}
} satisfies Theme
diff --git a/docs/sdk/questions.md b/docs/sdk/questions.md
index d072a1a..61c4b74 100644
--- a/docs/sdk/questions.md
+++ b/docs/sdk/questions.md
@@ -7,88 +7,48 @@ outline: deep
## 模拟接口请求
-在仓库 server 目录下有基于`nest.js`模拟的接口
+在仓库 server 目录下有基于`NestJS`模拟的接口
-1. `upload`接口
-
-```js
- @Post('upload')
- // file和前端上传的名称保持一致
- @UseInterceptors(
- FileInterceptor('file', {
- dest: 'uploads',
- }),
- )
- uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body) {
- const { filename, hash, index } = body;
- const chunkDir = `uploads/${hash}_${filename}`;
+```bash
+# 启动sever
+pnpm run server:dev
+```
- if (!fs.existsSync(chunkDir)) {
- fs.mkdirSync(chunkDir);
- }
- fs.cpSync(file.path, chunkDir + '/' + index);
- fs.rmSync(file.path);
+1. `check` 接口
- return { data: true };
- }
+```js
+axios.get('http://localhost:3000/check', {
+ params: {
+ hash: 'xxxx-xxxx-xxxx',
+ filename: 'xxx.png',
+ status: 'none' // none, part, waitMerge, success
+ },
+})
```
-2. `merge`接口
+2. `upload`接口
```js
- @Post('merge')
- merge(@Body() body) {
- const { hash, name: filename } = body;
- const chunkDir = `uploads/${hash}_${filename}`;
- const files = fs.readdirSync(chunkDir);
+const data = {
+ hash: 'xxxx-xxxx-xxxx',
+ filename: 'xxx.png',
+ index: 2,
+ file: (blob)
+}
+const formData = new FormData()
+Object.entries(data).forEach(([key, value]) => formData.append(key, value))
- let startPos = 0;
- files.map((file) => {
- const filePath = chunkDir + '/' + file;
- const stream = fs.createReadStream(filePath);
- stream.pipe(
- fs.createWriteStream('uploads/' + filename, {
- start: startPos,
- }),
- );
+axios.post('http://localhost:3000/upload', { data: formData })
- startPos += fs.statSync(filePath).size;
- });
-
- return {
- data: `http://localhost:3000/static/${filename}`,
- };
- }
```
-3. `checkFile`接口
+2. `merge`接口
```js
-@Post('checkFile')
- checkFile(@Body() body) {
- const { status } = body;
- if (status === 'success') {
- return {
- status: 'success',
- data: 'https://baidu.com',
- };
- }
-
- if (status === 'part') {
- return {
- status: 'part',
- // 成功的索引
- data: [0, 2, 4, 6, 8, 10],
- };
- }
-
- if (status === 'none') {
- return {
- status: 'none',
- data: false,
- };
- }
- }
+axios.get('http://localhost:3000/merge', {
+ params: {
+ hash: 'xxxx-xxxx-xxxx',
+ filename: 'xxx.png',
+ },
+})
```
-
-clone 代码到本地,启动 server 在`3000`端口
diff --git a/package.json b/package.json
index 8123761..01bd283 100644
--- a/package.json
+++ b/package.json
@@ -4,31 +4,38 @@
"workspaces": [
"packages/*"
],
- "description": "",
+ "description": "大文件分片上传解决方案",
"private": true,
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
+ "sdk:dev": "pnpm -F @tinyuploader/sdk dev",
+ "sdk:build": "pnpm -F @tinyuploader/sdk run build",
+ "sdk:preview": "pnpm -F @tinyuploader/sdk run preview",
"build": "pnpm run --parallel --filter @tinyuploader/* build",
- "dev:sdk": "pnpm -F @tinyuploader/sdk run dev:play",
- "build:sdk": "pnpm -F @tinyuploader/sdk run build",
"dev:package-vue": "pnpm -F @tinyuploader/vue dev:play",
"build:package-vue": "pnpm -F @tinyuploader/vue run build",
"dev:package-vuenext": "pnpm -F @tinyuploader/vuenext run dev:play",
"build:package-vuenext": "pnpm -F @tinyuploader/vuenext run build",
- "build:docs": "pnpm run -C docs docs:build",
+ "docs:dev": "pnpm run -C docs docs:dev",
+ "docs:build": "pnpm run -C docs docs:build",
+ "docs:preview": "pnpm run -C docs docs:preview",
"dev:vue": "pnpm -F @demos/vue run dev",
"dev:vuenext": "pnpm -F @demos/vuenext run dev",
"play:vue": "npm-run-all -p dev:sdk dev:package-vue dev:vue",
"play:vuenext": "npm-run-all -p dev:sdk dev:package-vuenext dev:vuenext",
- "dev:server": "pnpm run -C server start:debug",
+ "server:dev": "pnpm run -C server start:debug",
"change": "pnpm changeset",
"change-version": "pnpm changeset version",
"publish": "pnpm install && pnpm build && pnpm publish -F @tinyuploader/* ",
- "clear": "rimraf node_modules & rimraf packages/**/node_modules & rimraf demos/**/node_modules & rimraf docs/node_modules & rimraf server/node_modules"
+ "clean": "pnpm -r exec rimraf node_modules --verbose && npx rimraf node_modules --verbose"
},
"keywords": [
"uploader",
"upload",
+ "file",
+ "big file",
+ "retry",
+ "pause",
+ "resume",
"上传",
"分片上传",
"大文件",
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/index.html b/packages/sdk/examples/quick-start/index.html
new file mode 100644
index 0000000..f2af59d
--- /dev/null
+++ b/packages/sdk/examples/quick-start/index.html
@@ -0,0 +1,154 @@
+
+
+
+
+
+ Quick Start
+
+
+
+
+
+
+
+
+
+
Set Option
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1024 x 1024 x
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ms
+
+
+
+ Server
+
+
+ {{key}}
+
+
+
+
+
+
+
+
点击上传
+
+
+
+
+
+ {{ file.name }}
+
+
+
{{ file.status }}
+
Read {{ file.readProgress.toFixed(2) }}%
+
Upload {{ (file.progress * 100).toFixed(2) }}%
+
{{ file.renderSize }}
+
+
+ Pause
+
+
+ Resume
+
+
+ Retry
+
+ X
+
+
+
+
+
上传到服务器
+
Uploader clear
+
+
+
+
+
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..058bac0
--- /dev/null
+++ b/packages/sdk/examples/quick-start/quick-start.js
@@ -0,0 +1,190 @@
+const { createApp, ref, reactive, onMounted, watch } = Vue
+// const { VideoPause, VideoPlay, RefreshRight, CircleClose } = ElementPlusIconsVue
+
+// dist
+// import { create, FileStatus, ChunkStatus, CheckStatus, Callbacks } from '../dist/sdk.mjs'
+
+// dev
+import { create, FileStatus, Callbacks, CheckStatus } from '../../src/index.js'
+import { requestSucceed, customRequest, checkRequest, mergeRequest } from './request.js'
+
+const app = createApp({
+ setup() {
+ const drawer = ref(false)
+ const actionList = [
+ 'http://localhost:3000/upload',
+ 'https://jsonplaceholder.typicode.com/posts'
+ ]
+ const options = reactive({
+ drag: true,
+ // input相关
+ accept: '*',
+ multiple: true,
+ // 文件相关
+ limit: 10,
+ autoUpload: true,
+ addFailToRemove: true,
+ chunkSize: 2,
+ fakeProgress: true,
+ withHash: true,
+ useWebWoker: true,
+ // 上传相关
+ name: 'file',
+ action: 'http://localhost:3000/upload',
+ withCredentials: true,
+ data: {
+ bucket: 'test-public',
+ filePath: 'files/test01/',
+ status: CheckStatus.None
+ },
+ headers: {
+ userauth: 'xxxxx-xxxx-xxxxx'
+ },
+ maxConcurrency: 6,
+ maxRetries: 3,
+ retryInterval: 1000
+ })
+ 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({
+ ...options,
+ chunkSize: options.chunkSize * 1024 * 1024,
+ 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: 'default.png',
+ url: 'http://baidu.com'
+ }
+ ])
+
+ watch(
+ () => options,
+ () => {
+ uploader.value.setOption({
+ ...options,
+ chunkSize: options.chunkSize * 1024 * 1024
+ })
+ },
+ { deep: true }
+ )
+
+ 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,
+ CheckStatus,
+ actionList,
+ drawer,
+ options,
+ files,
+ submit,
+ clear,
+ remove,
+ pause,
+ resume,
+ retry
+ }
+ }
+})
+app.use(ElementPlus)
+app.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..922044a
--- /dev/null
+++ b/packages/sdk/examples/quick-start/request.js
@@ -0,0 +1,84 @@
+const queryString = (object) => {
+ let str = ''
+ for (const [key, value] of Object.entries(object)) {
+ str += `&${key}=${value}`
+ }
+ return str
+}
+
+export const requestSucceed = (response) => {
+ const { status } = response
+ if (status >= 200 && status < 300) {
+ return true
+ }
+ return false
+}
+
+export const customRequest = (options) => {
+ const { action, data, query, headers, name, withCredentials, onSuccess, onFail, onProgress } =
+ options
+ const realData = {
+ fileHashCode: data.hash,
+ uploadId: data.fileId,
+ chunkNumber: data.index + 1,
+ chunkSize: data.size,
+ totalChunks: data.totalChunks,
+ [name]: data[name],
+ hash: data.hash,
+ filename: data.filename,
+ index: data.index,
+ ...query
+ }
+ const formData = new FormData()
+
+ Object.keys(realData).forEach((key) => {
+ formData.append(key, realData[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, query) => {
+ const params = {
+ hash: file.hash,
+ filename: file.name,
+ status: 'none',
+ ...query
+ }
+ const data = await fetch(`http://localhost:3000/check?${queryString(params)}`)
+
+ return await data.json()
+}
+
+export const mergeRequest = async (file, query) => {
+ const params = {
+ hash: file.hash,
+ filename: file.name,
+ ...query
+ }
+ const data = await fetch(`http://localhost:3000/merge?${queryString(params)}`)
+ const json = await data.json()
+ return json.data
+}
diff --git a/packages/sdk/example/style.css b/packages/sdk/examples/style.css
similarity index 67%
rename from packages/sdk/example/style.css
rename to packages/sdk/examples/style.css
index 592dca3..c736c4b 100644
--- a/packages/sdk/example/style.css
+++ b/packages/sdk/examples/style.css
@@ -7,9 +7,20 @@
margin-right: var(--default-margin);
}
+.uploader-drag {
+ width: 300px;
+ height: 160px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ border: 1px dashed #000;
+ margin-top: 10px;
+ border-radius: 3px;
+}
+
.file-list {
margin-top: 10px;
- max-width: 660px;
}
.file {
@@ -21,10 +32,11 @@
border: 1px darkgray solid;
border-radius: 4px;
cursor: pointer;
+ font-size: 14px;
}
.file-name {
- width: 200px;
+ width: 300px;
overflow: hidden;
text-overflow: ellipsis;
margin-right: var(--default-margin);
@@ -37,14 +49,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/package.json b/packages/sdk/package.json
index e735635..073c362 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,7 +1,7 @@
{
"name": "@tinyuploader/sdk",
"version": "2.1.1",
- "description": "",
+ "description": "大文件分片上传解决方案sdk, 可用于各种UI框架",
"private": false,
"publishConfig": {
"access": "public"
@@ -9,12 +9,17 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "vite",
- "dev:play": "vite build --watch",
- "build": "vite build"
+ "build": "vite build",
+ "preview": "vite build --watch"
},
"keywords": [
"uploader",
"upload",
+ "file",
+ "big file",
+ "retry",
+ "pause",
+ "resume",
"上传",
"分片上传",
"大文件",
diff --git a/packages/sdk/src/core/Chunk.js b/packages/sdk/src/core/Chunk.js
index 47c5195..588cf12 100644
--- a/packages/sdk/src/core/Chunk.js
+++ b/packages/sdk/src/core/Chunk.js
@@ -1,135 +1,135 @@
-import { generateUid, each } from '@/shared'
-import { Status } from './constans.js'
+import { generateUid, isFunction, parseData } from '../shared'
+import { ChunkStatus, FileStatus, ProcessType } 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.totalChunks = file.totalChunks
- this.fileHash = this.file.hash
- this.uid = generateUid('chunk_id')
- this.stardByte = this.chunkSize * index
- this.endByte = Math.min(this.stardByte + this.chunkSize, this.totalSize)
- this.size = this.endByte - this.stardByte
+ this.uid = generateUid()
this.chunkIndex = index
+ this.status = ChunkStatus.Ready
+ this.startByte = this.chunkSize * index
+ this.endByte = Math.min(this.startByte + this.chunkSize, this.totalSize)
+ this.size = this.endByte - this.startByte
+
+ this.maxRetries = this.options.maxRetries
- this.maxRetries = this.opts.maxRetries
- this.xhr = null
- this.promise = null
- this.status = Status.Ready
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 = ChunkStatus.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(e, this)
+ } else {
+ this.timer = setTimeout(() => {
+ this.send()
+ this.maxRetries--
+ clearTimeout(this.timer)
+ }, this.options.retryInterval)
}
-
- return data
}
- 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()
- }
+ onProgress(e) {
+ this.progress = Math.min(1, e.loaded / e.total)
+ this.fakeProgress = Math.max(this.progress, this.fakeProgress)
- 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)
- }
- }
+ this.status = ChunkStatus.Uploading
+ this.file.changeStatus(FileStatus.Uploading)
+ this.file.setProgress(this)
+ }
- const progressHandler = (e) => {
- this.progress = Math.min(1, e.loaded / e.total)
- this.fakeProgress = Math.max(this.progress, this.fakeProgress)
+ prepare() {
+ const { name, data, processData } = this.options
+ const { data: fileData } = this.file
+ const defaults = {
+ [name]: this.file.rawFile.slice(this.startByte, this.endByte),
+ hash: this.fileHash,
+ id: this.uid,
+ fileId: this.fileId,
+ index: this.chunkIndex,
+ filename: this.filename,
+ size: this.size,
+ totalSize: this.totalSize,
+ totalChunks: this.totalChunks,
+ ...parseData(data),
+ ...fileData
+ }
+ if (!isFunction(processData)) {
+ return defaults
+ }
+ return processData(defaults, ProcessType.Upload) || defaults
+ }
- this.status = Status.Uploading
- this.file.changeStatus(Status.Uploading)
- this.file.setProgress(this)
- }
+ send() {
+ this.status = ChunkStatus.Pending
+ const { action, headers, withCredentials, name } = this.options
- 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)
+ return new Promise((resolve, reject) => {
+ this.request = this.customRequest({
+ action,
+ name,
+ withCredentials,
+ headers: parseData(headers),
+ data: this.prepare(),
+ query: {
+ ...parseData(this.options.data),
+ ...this.file.data
+ },
+ 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()
}
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..aab66f1 100644
--- a/packages/sdk/src/core/Container.js
+++ b/packages/sdk/src/core/Container.js
@@ -1,73 +1,103 @@
-import { each, extend } from '@/shared'
+import { each, extend } from '../shared/index.js'
class Container {
constructor(uploader) {
this.uploader = uploader
+ this.listeners = [] // 存储事件监听器
+ this.inputs = [] // 存储动态创建的 input 元素
}
assignBrowse(domNode, attributes) {
- let input
- if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
- input = domNode
- } else {
- input = document.createElement('input')
- input.setAttribute('type', 'file')
-
- extend(input.style, {
- visibility: 'hidden',
- position: 'absolute',
- width: '1px',
- height: '1px'
- })
-
- each(attributes, (val, key) => {
- input.setAttribute(key, val)
- })
-
- if (attributes.multiple) {
- input.setAttribute('multiple', 'multiple')
- } else {
- input.removeAttribute('multiple')
- }
-
- domNode.appendChild(input)
- domNode.addEventListener(
- 'click',
- () => {
- input.click()
- },
- false
- )
-
- input.addEventListener(
- 'change',
- (e) => {
- this.uploader.addFiles(e.target.files, e)
- e.target.value = ''
- },
- false
- )
+ if (!domNode) {
+ console.warn('DOM 节点不存在')
+ return
}
+
+ const input = this.createOrGetInput(domNode)
+ this.setInputAttributes(input, attributes)
+ this.attachBrowseEvents(domNode, input)
}
assignDrop(domNode) {
- const preventEvent = (e) => {
- e.preventDefault()
+ if (!domNode) {
+ console.warn('DOM 节点不存在')
+ return
}
- const handles = {
+
+ const preventEvent = (e) => e.preventDefault()
+ const eventHandlers = {
dragover: preventEvent,
dragenter: preventEvent,
dragleave: preventEvent,
- drop: (e) => {
- e.stopPropagation()
- e.preventDefault()
- this.uploader.addFiles(e.dataTransfer.files, e)
- }
+ drop: this.handleDrop.bind(this)
}
- each(handles, (handler, name) => {
- domNode.addEventListener(name, handler, false)
+
+ each(eventHandlers, (handler, event) => {
+ domNode.addEventListener(event, handler, { passive: false })
+ this.listeners.push({ node: domNode, event, handler })
})
}
+
+ createOrGetInput(domNode) {
+ if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
+ return domNode
+ }
+
+ const input = document.createElement('input')
+ input.type = 'file'
+ extend(input.style, {
+ visibility: 'hidden',
+ position: 'absolute',
+ width: '1px',
+ height: '1px'
+ })
+ domNode.appendChild(input)
+ this.inputs.push(input)
+ return input
+ }
+
+ setInputAttributes(input, attributes) {
+ each(attributes, (value, key) => input.setAttribute(key, value))
+ input.toggleAttribute('multiple', !!attributes.multiple)
+ }
+
+ attachBrowseEvents(domNode, input) {
+ const clickHandler = () => input.click()
+ const changeHandler = (e) => {
+ this.uploader.addFiles(e.target.files, e)
+ e.target.value = ''
+ }
+
+ domNode.addEventListener('click', clickHandler, { passive: true })
+ input.addEventListener('change', changeHandler, { passive: true })
+
+ this.listeners.push(
+ { node: domNode, event: 'click', handler: clickHandler },
+ { node: input, event: 'change', handler: changeHandler }
+ )
+ }
+
+ handleDrop(e) {
+ e.preventDefault()
+ e.stopPropagation()
+ this.uploader.addFiles(e.dataTransfer.files, e)
+ }
+
+ destroy() {
+ this.listeners.forEach(({ node, event, handler }) => {
+ node.removeEventListener(event, handler)
+ })
+ this.listeners = []
+
+ this.inputs.forEach((input) => {
+ if (input.parentNode) {
+ input.parentNode.removeChild(input)
+ }
+ })
+ this.inputs = []
+
+ return this
+ }
}
export default Container
diff --git a/packages/sdk/src/core/Event.js b/packages/sdk/src/core/Event.js
new file mode 100644
index 0000000..3716958
--- /dev/null
+++ b/packages/sdk/src/core/Event.js
@@ -0,0 +1,54 @@
+import { isFunction, isString } from '../shared'
+export default class Event {
+ constructor() {
+ this.events = new Map()
+ }
+
+ on(name, callback) {
+ if (!isFunction(callback)) return
+
+ const callbacks = this.events.get(name) || []
+ if (!callbacks.includes(callback)) {
+ callbacks.push(callback)
+ this.events.set(name, callbacks)
+ }
+ }
+
+ off(name, callback) {
+ if (!this.events.has(name)) return
+
+ if (!callback) {
+ this.events.set(name, [])
+ return
+ }
+
+ const callbacks = this.events.get(name).filter((cb) => cb !== callback)
+ this.events.set(name, callbacks)
+ }
+
+ emit(name, ...args) {
+ const callbacks = this.events.get(name)
+ if (callbacks && callbacks.length) {
+ callbacks.forEach((cb) => cb(...args))
+ }
+ }
+
+ once(name, callback) {
+ if (!isFunction(callback)) return
+
+ const onceCallback = (...args) => {
+ callback(...args)
+ this.off(name, onceCallback)
+ }
+ this.on(name, onceCallback)
+ }
+
+ clear(name) {
+ if (name === undefined) {
+ this.events.clear()
+ } else if (isString(name) && this.events.has(name)) {
+ this.events.set(name, [])
+ }
+ return this
+ }
+}
diff --git a/packages/sdk/src/core/File.js b/packages/sdk/src/core/File.js
index 6f9086f..e3216d5 100644
--- a/packages/sdk/src/core/File.js
+++ b/packages/sdk/src/core/File.js
@@ -1,150 +1,322 @@
-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, ProcessType } from './constants'
+import {
+ generateUid,
+ isFunction,
+ isBoolean,
+ asyncCancellableComputedHash,
+ each,
+ throttle,
+ renderSize,
+ parseData
+} from '../shared'
export default class File {
- constructor(file, uploader) {
- this.uploader = uploader || { opts: {} }
- this.opts = this.uploader.opts
+ constructor(file, uploader, defaults) {
+ this.uploader = uploader
+ this.options = this.uploader.options
+ this.uid = this.generateId()
+
+ this.prevStatusLastRecord = []
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 = ''
- this.progress = file.progress || 0
+ this.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.data = {}
+
+ 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() {
+ const { customGenerateUid } = this.options
+ if (!isFunction(customGenerateUid)) return generateUid()
+ return customGenerateUid(this) || generateUid()
+ }
+
+ setErrorMessage(message) {
+ this.errorMessage = String(message)
+ return this
+ }
+
+ setData(data) {
+ this.data = { ...this.data, ...data }
+ return this
+ }
+
+ get renderSize() {
+ return renderSize(this.size)
+ }
+
+ 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
}
- async start() {
- await this.computedHash()
+ isPause() {
+ return this.status === FileStatus.Pause
+ }
+
+ isResume() {
+ return this.status === FileStatus.Resume
+ }
+
+ createChunks() {
+ this.totalChunks = Math.ceil(this.size / this.chunkSize) || 1
+ this.chunks = Array.from({ length: this.totalChunks }, (_, i) => new Chunk(this, i))
+ }
+
+ async read() {
+ if (!this.options.withHash) {
+ this.createChunks()
+ this.changeStatus(FileStatus.Ready)
+ return
+ }
+
+ this.uploader.emitCallback(Callbacks.FileReadStart, this)
+ this.changeStatus(FileStatus.Reading)
+
+ try {
+ const startTime = Date.now()
+ const { hash, progress } = await this._computeHash()
+ this.hash = hash
+ this.readProgress = progress
+ this.uploader.emitCallback(Callbacks.FileReadEnd, this)
+ console.log(
+ `${this.options.useWebWoker ? 'Web Worker' : 'Main Thread'} read file took`,
+ (Date.now() - startTime) / 1000,
+ 's'
+ )
+ } catch (error) {
+ this.setErrorMessage('File read failed')
+ this.changeStatus(FileStatus.Init)
+ this.uploader.emitCallback(Callbacks.FileReadFail, this)
+ throw error
+ } finally {
+ this.abortRead = null
+ }
+
this.createChunks()
- if (this.opts.checkFileRequest) {
- await this.checkRequest()
+ this.changeStatus(FileStatus.Ready)
+ }
+
+ async _computeHash() {
+ const updateReadProgress = throttle((progress) => {
+ this.readProgress = progress
+ this.uploader.emitCallback(Callbacks.FileReadProgress, this)
+ }, 200)
+
+ const { promise, abort } = asyncCancellableComputedHash(
+ {
+ file: this.rawFile,
+ chunkSize: this.chunkSize,
+ useWebWoker: this.options.useWebWoker
+ },
+ ({ progress: readProgress }) => updateReadProgress(readProgress)
+ )
+
+ this.abortRead = abort
+ const hashResult = await promise
+ updateReadProgress(hashResult.progress)
+ return hashResult
+ }
+
+ _processData(processType) {
+ const { data: optionData, processData } = this.options
+ const defaults = { ...parseData(optionData), ...this.data }
+ if (!isFunction(processData)) {
+ return defaults
}
+ return processData(defaults, processType) || defaults
}
async checkRequest() {
- try {
- const { status: checkStatus, data } = await this.opts.checkFileRequest(this)
- if (checkStatus === CheckStatus.Part) {
+ const { checkRequest: check } = this.options
+
+ // 提前处理非函数情况
+ if (!isFunction(check)) {
+ return Promise.resolve()
+ }
+
+ // 统一状态修改方法
+ const updateChunksStatus = (status) => {
+ this.chunks.forEach((chunk) => {
+ chunk.status = status
+ if (status === ChunkStatus.Success) {
+ chunk.progress = 1
+ chunk.fakeProgress = 1
+ }
+ })
+ }
+
+ // 状态处理策略模式
+ const statusHandlers = {
+ [CheckStatus.Part]: (data) => {
this.chunks.forEach((chunk) => {
if (data.includes(chunk.chunkIndex)) {
- chunk.status = Status.Success
+ chunk.status = ChunkStatus.Success
chunk.progress = 1
chunk.fakeProgress = 1
}
})
+ },
+ [CheckStatus.WaitMerge]: () => {
+ this.changeStatus(FileStatus.UploadSuccess)
+ updateChunksStatus(ChunkStatus.Success)
+ },
+ [CheckStatus.Success]: (data) => {
+ this.changeStatus(FileStatus.Success)
+ updateChunksStatus(ChunkStatus.Success)
+ this.url = data
+ },
+ [CheckStatus.None]: () => {}
+ }
+
+ try {
+ const result = await Promise.resolve(check(this, this._processData(ProcessType.Check))) // 统一异步处理
+
+ // 验证响应格式
+ if (!result || !result.status) {
+ throw new Error('Invalid check response format')
}
- if (checkStatus === CheckStatus.Success) {
- this.success()
- this.chunks.forEach((chunk) => {
- chunk.status = Status.success
- })
- this.path = data
+
+ const handler = statusHandlers[result.status]
+ if (!handler) {
+ throw new Error(`Unknown check status: ${result.status}`)
}
- } catch {
- //
+
+ handler(result.data)
+ return Promise.resolve()
+ } catch (error) {
+ // 统一错误处理
+ this.changeStatus(FileStatus.CheckFail)
+ this.uploader.upload()
+
+ // 增强错误信息
+ const enhancedError = new Error(`Check request failed: ${error.message}`)
+ enhancedError.originalError = error
+ throw enhancedError
}
}
- 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()
- })
- })
+ addUploadingChunk(chunk) {
+ this.uploadingChunks.add(chunk)
}
- 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))
- }
- this.changeStatus(Status.Ready)
+ removeUploadingChunk(chunk) {
+ this.uploadingChunks.delete(chunk)
}
- 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)
+ async upload() {
+ if (this.isInit()) {
+ await this.read()
+ }
- // this.progress = Math.max(Math.min(progress, 1), this.progress)
- this.progress = Math.min(1, progress)
+ if (this.isReady() && this.options.checkRequest) {
+ await this.checkRequest()
+ }
if (this.isUploadSuccess()) {
- this.progress = 1
+ return this.merge()
}
- this.uploader.emit(Events.FileProgress, this.progress, this, this.uploader.fileList)
- }
-
- removeUploadingQueue(chunk) {
- this.uploadingQueue.delete(chunk)
- }
-
- addUploadingQueue(chunk) {
- this.uploadingQueue.add(chunk)
- }
-
- uploadFile() {
if (this.isSuccess()) {
- this.success()
- return
+ return this.success()
}
- const readyChunks = this.chunks.filter((chunk) => chunk.status === Status.Ready)
+ 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)
+ const hasErrorChunk = this.chunks.some((chunk) => chunk.status === ChunkStatus.Fail)
if (hasErrorChunk) {
this.uploadFail()
} else {
@@ -154,131 +326,142 @@ 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()
+ this.changeStatus(FileStatus.UploadFail)
+ this.uploader.emitCallback(Callbacks.FileUploadFail, this)
+ this._continueUpload()
}
- success() {
- this.changeStatus(Status.Success)
- this.progress = 1
- this.uploader.emit(Events.FileSuccess, this, this.uploader.fileList)
- this.uploader.upload()
+ uploadSuccess() {
+ this.changeStatus(FileStatus.UploadSuccess)
+ this.uploader.emitCallback(Callbacks.FileUploadSuccess, this)
}
- mergeFail() {
- this.changeStatus(Status.Fail)
- this.uploader.emit(Events.FileFail, this, this.uploader.fileList)
- this.uploader.upload()
- }
+ async merge() {
+ const { mergeRequest: merge } = this.options
- merge() {
- const merge = this.opts.mergeRequest
+ // 非函数处理提前终止
if (!isFunction(merge)) {
- this.success()
- return
+ return this.success()
}
- const result = merge(this)
-
- if (isPromise(result)) {
- result.then(
- () => this.success(),
- () => this.mergeFail()
- )
- } else {
- result ? this.success() : this.mergeFail()
+ try {
+ const result = merge(this, this._processData(ProcessType.Merge))
+ const data = await Promise.resolve(result)
+ if (isBoolean(data)) {
+ data ? this.success() : this.mergeFail()
+ } else {
+ this.url = data
+ this.success()
+ }
+ } catch (error) {
+ console.log(error)
+ this.mergeFail()
}
}
- 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
- }
- })
-
- this.uploadFile()
- }
+ mergeFail() {
+ this.changeStatus(FileStatus.Fail)
+ this.uploader.emitCallback(Callbacks.FileFail, this)
+ this._continueUpload()
}
- remove() {
- this.chunks = []
- this.uploadingQueue.forEach((chunk) => {
- chunk.abort()
- })
+ success() {
+ this.changeStatus(FileStatus.Success)
+ this.progress = 1
+ this.uploader.emitCallback(Callbacks.FileSuccess, this)
+ this._continueUpload()
}
- pause() {
- this.uploadingQueue.forEach((chunk) => {
- chunk.abort()
- })
- this.changeStatus(Status.Pause)
+ _continueUpload() {
+ const firstPauseFile = this.uploader.fileList.find((file) => file.isPause())
+ if (firstPauseFile) {
+ firstPauseFile.resume()
+ }
this.uploader.upload()
}
- resume() {
- if (this.isPause()) {
- this.changeStatus(Status.Resume)
- this.uploader.pauseUploadingFiles()
- this.uploader.upload()
- }
+ cancel() {
+ this.uploadingChunks.forEach((chunk) => chunk.abort())
+ this.uploadingChunks.clear()
}
- isInited() {
- return this.status === Status.Init
- }
+ async remove() {
+ if (this.abortRead) {
+ this.abortRead()
+ }
- 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.isAddFail()) {
+ return
+ }
- isUploadFail() {
- return this.status === Status.UploadFail
- }
+ if (this.isCheckFail()) {
+ this.changeStatus(FileStatus.Ready)
+ this.upload()
+ return
+ }
- isFail() {
- return this.status === Status.Fail
- }
+ if (this.isUploadSuccess() || this.isFail()) {
+ this.merge()
+ return
+ }
- isSuccess() {
- return this.status === Status.Success
+ if (this.isUploadFail()) {
+ each(this.chunks, (chunk) => {
+ if (chunk.status === ChunkStatus.Fail) {
+ chunk.status = ChunkStatus.Ready
+ chunk.maxRetries = chunk.options.maxRetries
+ }
+ })
+
+ this.upload()
+ }
}
}
diff --git a/packages/sdk/src/core/Uploader.js b/packages/sdk/src/core/Uploader.js
index d006cdc..2c0f2d6 100644
--- a/packages/sdk/src/core/Uploader.js
+++ b/packages/sdk/src/core/Uploader.js
@@ -1,170 +1,175 @@
-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'
-
-class Uploader {
+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'
+
+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.fileList = fileList
- this.status = Status.Init
- // 只注册一次
- this.listenerFiles()
+
+ this.options = extend(defaultOptions, options)
+ this.fileList = this.options.fileList || []
+ this._setupFileListeners()
}
- on(name, func) {
- this.event.on(name, func)
+ 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)
+ }
+
+ updateData(data) {
+ this.options.data = data
+ }
+
+ updateHeaders(headers) {
+ this.options.headers = headers
+ }
+
+ setOption(options) {
+ this.options = extend(this.options, options)
+ }
+
+ formatAccept(accept) {
+ if (isString(accept)) return accept
+ if (isArray(accept)) return accept.join(',')
+ return ''
+ }
+
+ assignBrowse(domNode, userAttributes = {}) {
+ const { accept, ...attributes } = userAttributes
+ const defaults = {
+ multiple: this.options.multiple,
+ accept: this.formatAccept(accept || this.options.accept)
+ }
+ this.container.assignBrowse(domNode, extend({}, defaults, attributes))
+ }
- if (limit > 0) {
- if (files.length + this.fileList.length > limit) {
- this.emit(Events.Exceed, files, this.fileList)
+ assignDrop(domNode) {
+ this.container.assignDrop(domNode)
+ }
+
+ _setupFileListeners() {
+ const checkAllSuccess = (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, checkAllSuccess)
+ this.on(Callbacks.FileRemove, checkAllSuccess)
+ }
- if (!this.opts.multiple) {
- originFileList = originFileList.slice(0, 1)
- }
+ setDefaultFileList(fileList) {
+ fileList.forEach((file) => {
+ this.fileList.push(new File(file, this, file))
+ })
+ }
+
+ async addFiles(arrayLike) {
+ const { limit, multiple, addFailToRemove, beforeAdd, autoUpload } = this.options
+ let originFiles = [...arrayLike]
+
+ if (originFiles.length === 0) return
- if (originFileList.length === 0) {
+ if (limit > 0 && originFiles.length + this.fileList.length > limit) {
+ this.emitCallback(Callbacks.Exceed, originFiles)
return
}
- const newFileList = originFileList.map((file) => new File(file, this))
- this.fileList = [...this.fileList, ...newFileList]
+ if (!multiple) {
+ originFiles = originFiles.slice(0, 1)
+ }
- 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) {
- //
- } else {
- this.doRemove(file)
- }
- }
+ const newFileList = originFiles.map((file) => new File(file, this))
+ await Promise.all(newFileList.map((file) => this._handleFileAdd(file), beforeAdd))
+
+ this.fileList = this.fileList.filter((file) => {
+ if (file.isAddFail() && addFailToRemove) {
+ this.doRemove(file)
+ return false
}
- }
+ return true
+ })
- this.emit(Events.FilesAdded, this.fileList)
- this.status = Status.Ready
+ if (newFileList.length > 0) {
+ this.emitCallback(Callbacks.FilesAdded, this.fileList)
+ }
- if (this.opts.autoUpload) {
+ if (autoUpload) {
this.submit()
}
}
- pauseUploadingFiles() {
- const uploadingFiles = this.fileList.filter((file) => file.isUploading())
- uploadingFiles.forEach((file) => {
- file.pause()
- })
+ async _handleFileAdd(file, beforeAdd) {
+ try {
+ // 如果是函数则进行结果判断,否则认为校验通过
+ if (isFunction(beforeAdd)) {
+ const result = await beforeAdd(file)
+ // 如果返回的是false则失败,如果不返回或者返回为其他值如true则认为成功
+ if (result === false) {
+ throw new Error('Before add rejected')
+ }
+ }
+ this.emitCallback(Callbacks.FileAdded, file)
+ } catch {
+ file.changeStatus(FileStatus.AddFail)
+ this.emitCallback(Callbacks.FileAddFail, file)
+ }
+ this.fileList.push(file)
}
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.isAddFail() || file.isCheckFail()) {
+ continue
}
- if (file.isResume()) {
- file.status = Status.Uploading
- file.uploadFile()
- return
- }
- 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 +184,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 +211,35 @@ class Uploader {
}
resume(file) {
+ if (!file) return
+ const uploadingFiles = this.fileList.filter((file) => {
+ return file.isUploading() || file.isReading()
+ })
+
+ uploadingFiles.forEach((item) => {
+ item.pause()
+ })
+ file.resume()
+ }
+
+ retry(file) {
+ if (!file) return
+ const uploadingFiles = this.fileList.filter((file) => {
+ return file.isUploading() || file.isReading()
+ })
+
+ uploadingFiles.forEach((item) => {
+ item.pause()
+ })
const index = this.fileList.indexOf(file)
if (index > -1) {
- file.resume()
+ file.retry()
}
}
- assignBrowse(domNode, attributes = {}) {
- const attrs = extend(
- {},
- {
- accept: this.opts.accept,
- multiple: this.opts.multiple
- },
- attributes
- )
- this.container.assignBrowse(domNode, attrs)
- }
-
- assignDrop(domNode) {
- this.container.assignDrop(domNode)
+ destroy() {
+ this.clear()
+ this.event.clear()
+ this.container.destroy()
}
}
-
-Uploader.Status = Status
-Uploader.Events = Events
-Uploader.File = File
-Uploader.CheckStatus = CheckStatus
-Uploader.create = (options) => {
- return new Uploader(options)
-}
-
-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..605bed6
--- /dev/null
+++ b/packages/sdk/src/core/constants.js
@@ -0,0 +1,161 @@
+// 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',
+
+ FileReadFail: 'fileReadFail',
+
+ // 文件上传进度
+ FileProgress: 'fileProgress',
+
+ // 文件上传成功
+ FileUploadSuccess: 'fileUploadSuccess',
+
+ // 文件上传失败
+ FileUploadFail: 'fileUploadFail',
+
+ // 文件合并成功
+ FileSuccess: 'fileSuccess',
+
+ // 文件上传失败合并失败
+ FileFail: 'fileFail',
+
+ // 所有文件上传成功
+ AllFileSuccess: 'allFileSuccess'
+}
+
+// check 文件上传状态
+export const CheckStatus = {
+ // 文件还没上传
+ None: 'none',
+
+ // 部分上传成功
+ Part: 'part',
+
+ // 准备合并
+ WaitMerge: 'waitMerge',
+
+ // 上传成功
+ Success: 'success'
+}
+
+// 文件上传进程
+export const ProcessType = {
+ // check接口
+ Check: 'check',
+ // upload chunk接口
+ Upload: 'upload',
+ // merge接口
+ Merge: 'merge'
+}
diff --git a/packages/sdk/src/core/defaults.js b/packages/sdk/src/core/defaults.js
index c280f2b..628e919 100644
--- a/packages/sdk/src/core/defaults.js
+++ b/packages/sdk/src/core/defaults.js
@@ -1,68 +1,35 @@
-import { sleep } from '@/shared'
-import { CheckStatus } from './constans'
+import { CheckStatus } from './constants.js'
-export const defaults = {
- multipart: true, // TODO: 是否分片上传,false时单文件上传
+export const defaultOptions = {
+ // input 属性相关
+ accept: '*',
+ multiple: true,
+
+ // 文件相关
+ 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: (_file) => ({ status: CheckStatus.None }),
+ mergeRequest: (_file) => true,
+ processData: (data, _processType) => data
}
diff --git a/packages/sdk/src/core/request.js b/packages/sdk/src/core/request.js
new file mode 100644
index 0000000..402dd79
--- /dev/null
+++ b/packages/sdk/src/core/request.js
@@ -0,0 +1,48 @@
+export function request(options) {
+ const {
+ method = 'POST',
+ withCredentials = true,
+ responseType = 'json',
+ action,
+ data,
+ // query,
+ headers,
+ // name,
+ onSuccess,
+ onFail,
+ onProgress
+ } = options
+
+ let xhr = new XMLHttpRequest()
+ xhr.responseType = responseType
+ xhr.withCredentials = withCredentials
+ xhr.open(method, action, true)
+
+ const formData = new FormData()
+ Object.entries(data).forEach(([key, value]) => formData.append(key, value))
+
+ // 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED
+ if ('setRequestHeader' in xhr) {
+ Object.entries(headers).forEach(([key, value]) => xhr.setRequestHeader(key, value))
+ }
+
+ xhr.addEventListener('timeout', () => onFail(new Error('Request timed out'), xhr))
+ xhr.upload.addEventListener('progress', onProgress)
+ xhr.addEventListener('error', onFail, false)
+ xhr.addEventListener('readystatechange', (e) => {
+ if (xhr.readyState !== 4) return
+ if (xhr.status < 200 || xhr.status >= 300) {
+ onFail(new Error(`xhr: status === ${xhr.status}`), xhr)
+ return
+ }
+ onSuccess(e, xhr)
+ })
+ 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..48cc107 100644
--- a/packages/sdk/src/index.js
+++ b/packages/sdk/src/index.js
@@ -1,10 +1,11 @@
-import Uploader from '@/core/Uploader.js'
-import File from '@/core/File'
-import Chunk from '@/core/Chunk'
-import { Status, Events, CheckStatus } from '@/core/constans'
+import Uploader from './core/Uploader.js'
+import File from './core/File.js'
+import Chunk from './core/Chunk.js'
+import * as Utils from './shared/index.js'
+import { Callbacks, FileStatus, ChunkStatus, CheckStatus } from './core/constants.js'
+
+const create = (options) => new Uploader(options)
-const create = (options) => {
- return new Uploader(options)
-}
export default Uploader
-export { File, Status, Events, CheckStatus, Chunk, create }
+
+export { create, File, Chunk, Utils, FileStatus, ChunkStatus, Callbacks, CheckStatus }
diff --git a/packages/sdk/src/shared/blob.js b/packages/sdk/src/shared/blob.js
index 850b5d2..54cd554 100644
--- a/packages/sdk/src/shared/blob.js
+++ b/packages/sdk/src/shared/blob.js
@@ -1 +1,12 @@
export const slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
+
+export const renderSize = (value) => {
+ if (!value || isNaN(value) || value <= 0) return '0 B'
+
+ const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+ const base = 1024
+ const index = Math.floor(Math.log(value) / Math.log(base))
+ const size = (value / Math.pow(base, index)).toFixed(2)
+
+ return `${size} ${units[Math.min(index, units.length - 1)]}`
+}
diff --git a/packages/sdk/src/shared/event.js b/packages/sdk/src/shared/event.js
deleted file mode 100644
index a55751b..0000000
--- a/packages/sdk/src/shared/event.js
+++ /dev/null
@@ -1,58 +0,0 @@
-export class Event {
- constructor() {
- this.event = {}
- }
-
- on(name, func) {
- if (!this.event) {
- this.event = {}
- }
-
- if (!this.event[name]) {
- this.event[name] = []
- }
-
- // 避免通过函数多次被调用
- if (this.event[name].indexOf(func) > -1) {
- return
- }
-
- this.event[name].push(func)
- }
-
- off(name, func) {
- if (!func) {
- this.event[name] = []
- return
- }
- if (this.event && this.event[name]) {
- for (let i = 0; i < this.event[name].length; i++) {
- if (func === this.event[name]) {
- this.event.splice(i, 1)
- return
- }
- }
- }
- }
-
- emit(name, ...args) {
- if (!this.event) {
- this.event = {}
- }
- if (!this.event[name]) {
- return
- }
- this.event[name].forEach((func) => {
- func(...args)
- })
- }
-
- once(name, func) {
- function on(...args) {
- func.apply(this, ...args)
- this.off(name, on)
- }
- on.func = func
- this.on(name, on)
- }
-}
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/object.js b/packages/sdk/src/shared/object.js
index 686d2fc..7dee7cb 100644
--- a/packages/sdk/src/shared/object.js
+++ b/packages/sdk/src/shared/object.js
@@ -57,3 +57,15 @@ export function extend() {
}
return target
}
+
+export const parseData = (data) => {
+ if (isFunction(data)) {
+ return data() || {}
+ }
+
+ if (isPlainObject(data)) {
+ return data
+ }
+
+ return {}
+}
diff --git a/packages/sdk/src/shared/types.js b/packages/sdk/src/shared/types.js
index 3d18be1..3594ffc 100644
--- a/packages/sdk/src/shared/types.js
+++ b/packages/sdk/src/shared/types.js
@@ -22,3 +22,13 @@ export const isArray =
export const isPromise = (promise) => {
return promise && isFunction(promise.then)
}
+
+export const isString = function (a) {
+ return typeof a === 'string'
+}
+
+export const isBoolean = function (a) {
+ return typeof a === 'boolean'
+}
+
+
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'
},
diff --git a/packages/vue/example/App.vue b/packages/vue/example/App.vue
index 7cadcb3..1732587 100644
--- a/packages/vue/example/App.vue
+++ b/packages/vue/example/App.vue
@@ -1,31 +1,36 @@
-
Dev pkg vue2
-
+
-
@@ -35,67 +40,81 @@
export default {
data() {
return {
- fileList: [
+ name: 'moyuderen',
+ fileList: []
+ }
+ },
+ created() {
+ setTimeout(() => {
+ this.fileList = [
{
+ id: '222',
name: '哈哈',
- path: 'http://baidu.com'
+ url: 'http://baidu.com'
}
]
- }
+ }, 500)
},
methods: {
- onExceed() {
- console.log('超出最大上传次数了')
+ processData(data, type) {
+ return {...data, name: this.name}
},
- onFilesAdded(fileList) {
- console.log('添加文件成功', fileList)
- },
- onRemove(file, fileList) {
- console.log('删除文件成功', file, fileList)
+ async checkRequest(file, query) {
+ console.log(query)
+ const { hash, name } = file
+ const { data } = await axios.get(`http://localhost:3000/check?hash=${hash}&filename=${name}&status=none`)
+ file.setData({uploadId: file.uid})
+ return data
},
- onProgress(p, file, fileList) {
- // console.log('上传中', p, file, fileList)
+ async merge(file) {
+ const { hash, name } = file
+ const { data } = await axios.get(`http://localhost:3000/merge?hash=${hash}&filename=${name}`)
+ file.url = data.data
+ return true
},
- onFail(file, fileList) {
- console.log('上传失败', file, fileList)
+ beforeAdd(file) {
+ if(file.name.endsWith('.js')) {
+ file.setErrorMessage('不允许上传js文件')
+ return false
+ }
},
- onSuccess(file, fileList) {
- console.log('上传成功', file, fileList)
+ clear() {
+ this.$refs.uploaderRef.clear()
},
- onAllFileSuccess(fileList) {
- console.log('全部上传成功', fileList)
+ submit() {
+ this.$refs.uploaderRef.submit()
},
onChange(fileList) {
console.log('change', fileList)
-
this.fileList = fileList
},
- onClick(file) {
- console.log(file)
+ onExceed(files, fileList) {
+ console.log('onExceed', files, fileList)
},
-
- async checkFileRequest(file) {
- const { hash, name } = file
- const { data } = await axios.post('http://localhost:3000/checkFile', {
- hash,
- name,
- status: 'none'
- })
- return data
+ onFileAdded(file, fileList) {
+ console.log('onFileAdded', file, fileList)
},
- async merge(file) {
- const { hash, name } = file
- const { data } = await axios.post('http://localhost:3000/merge', { hash, name })
- file.path = data.data
+ onFilesAdded(fileList) {
+ console.log('onFilesAdded', fileList)
},
- clear() {
- this.$refs.uploaderRef.clear()
+ onFileRemove(file, fileList) {
+ console.log('onFileRemove', file, fileList)
},
- submit() {
- this.$refs.uploaderRef.submit()
+ onFileProgress(file, fileList) {
+ console.log('onFileProgress', file.name, file.progress, fileList)
+ },
+ onFileUploadSuccess() {},
+ onSuccess(file, fileList) {
+ console.log('onSuccess', file, fileList)
+ },
+ onFail(file, fileList) {
+ console.log('onFail', file, fileList)
+ },
+ onAllSuccess(fileList) {
+ console.log('onAllSuccess', fileList)
},
- viewFileList() {
- console.log(this.fileList)
+ onPreview(file) {
+ console.log('onPreview', file.name, file.url)
}
}
}
diff --git a/packages/vue/index.html b/packages/vue/index.html
index 2458703..d9889ae 100644
--- a/packages/vue/index.html
+++ b/packages/vue/index.html
@@ -4,7 +4,7 @@
- dev vue2
+ Example vue2
diff --git a/packages/vue/src/components/file-icon.vue b/packages/vue/src/components/file-icon.vue
index 33d942e..f899890 100644
--- a/packages/vue/src/components/file-icon.vue
+++ b/packages/vue/src/components/file-icon.vue
@@ -1,7 +1,7 @@
+
+
+
+
diff --git a/packages/vue/src/components/pause-icon.vue b/packages/vue/src/components/pause-icon.vue
index 94576a1..c12694d 100644
--- a/packages/vue/src/components/pause-icon.vue
+++ b/packages/vue/src/components/pause-icon.vue
@@ -1,7 +1,7 @@
-
+
diff --git a/packages/vue/src/components/uploader-drop.vue b/packages/vue/src/components/uploader-drop.vue
index e55a65c..f275714 100644
--- a/packages/vue/src/components/uploader-drop.vue
+++ b/packages/vue/src/components/uploader-drop.vue
@@ -29,8 +29,10 @@ export default {
font-size: 14px;
border-radius: 4px;
cursor: pointer;
+ background-color: rgb(247, 248, 250);
}
+
.tiny-uploader-drop:hover {
border: 1px dashed #409eff;
}
diff --git a/packages/vue/src/components/uploader-file.vue b/packages/vue/src/components/uploader-file.vue
index 7b8d19a..d6c6cfe 100644
--- a/packages/vue/src/components/uploader-file.vue
+++ b/packages/vue/src/components/uploader-file.vue
@@ -1,46 +1,58 @@
-
-
-
-
- {{ file.name }}
+
+
+
+
+
+ {{ file.name }}
+ {{ `(${file.renderSize})` }}
+
-
{{ parseProgress(file.progress) }}%
-
-
-
-
-
+
+
+
{{ file.errorMessage || statusMap[file.status] }}
+
+ {{ parseProgress(file.progress) }}%
+
-
@@ -48,11 +60,23 @@
diff --git a/packages/vue/src/components/uploader.vue b/packages/vue/src/components/uploader.vue
index 37bf8db..920a7d5 100644
--- a/packages/vue/src/components/uploader.vue
+++ b/packages/vue/src/components/uploader.vue
@@ -1,6 +1,6 @@
-
-
+
+
@@ -10,15 +10,15 @@
-
+
-
+
-
+
@@ -26,15 +26,13 @@