@mxyu/uploader
v0.1.0
Published
生产级 Vue 3 文件上传组件库:点击/拖拽/粘贴、分片上传、断点续传、并发调度
Maintainers
Readme
@mxyu/uploader
生产级 Vue 3 文件上传组件库:点击 / 拖拽 / 粘贴、单/多文件、文件夹、进度、取消、重试、分片上传、断点续传。 提供开箱即用的
<FileUploader>组件,也提供 headless 的useUploader组合式函数。
安装
pnpm add @mxyu/uploader
# 或 npm install @mxyu/uploader依赖 vue >= 3(peerDependency)。内置 spark-md5 用于分片哈希。
快速开始(组件用法)
<script setup lang="ts">
import { FileUploader } from '@mxyu/uploader';
</script>
<template>
<FileUploader
action="/api/upload"
multiple
accept="image/*,.pdf"
:max-size="50 * 1024 * 1024"
@success="(res, file) => console.log('上传成功', file.name, file.url)"
@error="(err, file) => console.error('上传失败', file.name, err)"
/>
</template>Headless 用法(useUploader)
不需要内置 UI 时,直接用组合式函数自行渲染:
import { useUploader } from '@mxyu/uploader';
const uploader = useUploader({
action: '/api/upload',
maxConcurrent: 3,
onSuccess: (res, file) => console.log(file.url),
});
// uploader.fileList / isUploading / totalProgress 均为响应式
// uploader.addFiles(fileList) / submit() / cancel() / retry(uid) ...Props
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| action | string | — | 上传接口 URL,与 customRequest 互斥(后者优先) |
| customRequest | (opts) => AbortController \| void | — | 完全自定义上传(OSS/S3 直传、签名) |
| responseHandler | (response) => string \| undefined | 取 response.url ?? response.data.url | 从响应提取文件地址写入 file.url |
| disabled | boolean | false | 禁用 |
| multiple | boolean | false | 多文件 |
| directory | boolean | false | 文件夹上传(webkitdirectory,自动视为多文件) |
| accept | string | — | 类型限制,如 "image/*,.pdf" |
| pasteEnabled | boolean | true | 启用粘贴上传 |
| includeHiddenFiles | boolean | false | 文件夹上传是否含隐藏文件 |
| duplicateStrategy | 'replace' \| 'ignore' \| 'prompt' | 'replace' | 重复文件策略(name+size+lastModified 判重) |
| beforeUpload | (file, list) => boolean \| File \| undefined \| Promise<...> | — | 上传前钩子(压缩/水印/格式转换) |
| maxSize | number | — | 单文件最大字节数(超出 → invalid) |
| maxCount | number | — | 列表最大数量(超出 → exceed 事件) |
| validator | (file) => boolean \| string \| Promise<...> | — | 自定义校验 |
| autoUpload | boolean | true | 入队后自动上传;false 需调用 submit() |
| withCredentials | boolean | false | 跨域携带凭证 |
| headers | Record<string,string> | — | 自定义请求头 |
| data | object \| (file) => object | — | 附加表单字段 |
| name | string | 'file' | FormData 文件字段名 |
| requestTimeout | number | 0 | 请求超时(毫秒),0 不超时 |
| maxRetry | number | 3 | 单文件最大重试次数 |
| maxConcurrent | number | 3 | 文件级并发数 |
| chunkConcurrent | number | 5 | 单文件分片并发数 |
| chunkSize | number | 2MB | 每片大小 |
| chunkThreshold | number | — | 超过该字节数启用分片;不设则不分片 |
| checkChunks | (fileHash) => Promise<number[]> | — | 断点续传:返回已完成分片索引 |
| mergeChunks | (uploadId, totalChunks, fileHash) => Promise<unknown> | — | 通知服务端合并分片 |
| dragEnabled | boolean | true | 启用拖拽 |
| showList | boolean | true | 显示文件列表 |
| showGlobalProgress | boolean | true | 显示聚合进度 |
| limit | number | — | 列表最大渲染条目数 |
Events
| 事件 | 参数 | 说明 |
|---|---|---|
| change | (file, fileList) | 任意状态变更 |
| progress | (percent, file, fileList) | 进度更新 |
| success | (response, file, fileList) | 单文件成功 |
| error | (error, file, fileList) | 网络/服务端失败(不含校验失败) |
| invalid | (file, reason) | 校验失败 / beforeUpload 拒绝 |
| cancel | (file \| null, fileList) | 取消(null 表示全部) |
| duplicate | (file, fileList) | duplicateStrategy='prompt' 时触发 |
| exceed | (files, fileList) | maxCount 溢出 |
| drop | (files) | 拖拽投放(纯通知) |
| paste | (files) | 粘贴(纯通知) |
| all-done | (fileList) | 全部文件达终态 |
Slots
| 插槽 | 作用域 | 说明 |
|---|---|---|
| default | — | 替换整个上传区域 |
| trigger | { isDragging, disabled } | 替换触发器内容 |
| file-item | { file } | 完全自定义单行 |
| file-icon | { file } | 自定义图标 |
| file-extra | { file } | 文件行末尾扩展区 |
| actions | { fileList, submit, cancel, clearFiles } | 全局操作区 |
| empty | — | 空列表占位 |
Expose(通过模板 ref 调用)
const uploaderRef = ref();
uploaderRef.value.submit(); // 提交全部 queued(autoUpload:false 时)
uploaderRef.value.cancel(uid?); // 取消单个/全部
uploaderRef.value.retry(uid); // 重试
uploaderRef.value.clearFiles(); // 清除终态文件
uploaderRef.value.clearAll(); // 清空全部
uploaderRef.value.getFileList(); // 当前文件列表只读副本customRequest(OSS / S3 直传)
function customRequest(opts: CustomRequestOptions) {
const controller = new AbortController();
fetch(presignedUrl, { method: 'PUT', body: opts.file.raw, signal: controller.signal })
.then(() => opts.onSuccess({ url: publicUrl }))
.catch(opts.onError);
return { abort: () => controller.abort() };
}分片上传与断点续传
设置 chunkThreshold 后,超过阈值的文件自动走分片上传:
<FileUploader
action="/api/upload-chunk"
:chunk-threshold="100 * 1024 * 1024"
:chunk-size="2 * 1024 * 1024"
:check-chunks="hash => fetch(`/api/uploaded?hash=${hash}`).then(r => r.json())"
:merge-chunks="(uploadId, total, hash) => fetch('/api/merge', { method: 'POST', body: JSON.stringify({ uploadId, total, hash }) }).then(r => r.json())"
/>后端契约(默认分片实现):每个分片以 POST {action} 发送 FormData,字段包含:
uploadId、fileHash、chunkIndex、totalChunks,以及文件字段(name 指定,默认 file)的分片 Blob。
其中 uploadId === fileHash(内容寻址,本库未约定独立的 init 接口)。服务端按 fileHash 归并;
checkChunks(fileHash) 返回已完成分片索引用于续传;mergeChunks 合并并返回最终响应(经 responseHandler 提取 url)。
主题定制(CSS 变量)
组件样式通过 CSS 变量覆盖,例如:
.mxu-uploader {
--mxu-color-primary: #1677ff;
--mxu-color-uploading: #1677ff;
--mxu-color-done: #52c41a;
--mxu-color-error: #ff4d4f;
--mxu-color-paused: #fa8c16;
}状态机
pending → validating →(invalid | processing | queued) → uploading →(paused | done | error | cancelled)。
invalid 为校验失败终态;error 为网络失败,retryCount < maxRetry 时可重试,否则等同终态。
FAQ
- 进度一直停在 99%? 分片上传在全部分片完成后、合并成功前锁定 99%,合并成功后置 100%。
- 哈希在哪里计算? 当前版本在主线程增量计算(MD5);
useUploader支持注入自定义computeHash(如 Worker 实现)。 - 取消和失败如何区分? 用户主动取消 →
cancelled(不计入重试);网络/服务端失败 →error(按指数退避自动重试)。 - React 支持? 当前为 Vue 3 组件库;框架无关的底层能力(
computeFileHash、uploadFileInChunks、createDefaultExecutor)可直接在任意框架使用,React demo 在后续迭代提供。
License
MIT
