一个基于 simple-uploader.js 的 react 的上传组件,是vue-simple-uploader的 react 实现,并对秒传和断点续传做了一些优化方案。
在线体验(账号:test;密码:test)
-
支持文件、多文件、文件夹上传
-
支持拖拽文件、文件夹上传
-
统一对待文件和文件夹,方便操作管理
-
可暂停、继续上传
-
错误处理
-
支持“快传”,通过文件判断服务端是否已存在从而实现“快传”
-
支持断点续传,通过文件判断服务端存在已上传的文件切片,只上传未上传的切片
-
上传队列管理,支持最大并发上传
-
分块上传,并发控制
-
支持进度、预估剩余时间、出错自动重试、重传等操作
npm i react-simple-upload
样式为默认样式,需要定制样式请看例子
import Uploader from 'react-simple-upload';
import { UploadFile } from 'react-simple-upload/dist/types';
export default function Upload() {
const [options] = useState({
target: '//localhost:3000/upload',
testChunks: false
});
const complete = () => {
console.log('complete');
};
return <Uploader options={options}></Uploader>;
}
上传根组件,可以直接使用选择其默认样式,也可以使用 UploaderBtn 等组件自定义样式及结构
参数 | 说明 | 类型 | 默认值 |
options | simple-uploader.js uploader实例配置 | UploadeOptions | 见simple-uploader.js文档 |
fileStatusText | 用于转换文件上传状态文本映射对象。 |
object | function(status:"success"|"error"|"uploading"|"paused"|"waiting",response):string |
{
"success": "success",
"error": "error",
"uploading": "uploading",
"paused": "paused",
"waiting": "waiting"
} |
autoStart | 是否选择文件后立即上传 | Boolean |
true |
onFileAdded | 这个事件一般用作文件校验,如果说返回了 false,那么这个文件就会被忽略,不会添加到文件上传列表中 |
(file: UploadFile) => boolean; |
(file) => true; |
onFilesAdded | fileAdded 一样,但是一般用作多个文件的校验 |
(files: UploadFile[], fileList: UploadFile[]) => boolean; |
() => true; |
onFilesSubmitted | 和 filesAdded 类似,但是是文件已经加入到上传列表中,一般用来开始整个的上传 |
(files: UploadFile[],fileList: UploadFile[],e: Event) => void; |
() => {}; |
onFileProgress | 一个文件正在上传 |
(rootfile: UploadFile,file: UploadFile,chunk: UploadChunk) => void; |
()=>void |
onFileSuccess | 文件上传成功事件,rootFile 就是成功上传的文件所属的根 Uploader.File 对象,它应该包含或者等于成功上传文件; file 就是当前成功的 Uploader.File 对象本身;message 就是服务端响应内容,永远都是字符串;chunk 就是 Uploader.Chunk 实例,它就是该文件的最后一个块实例,如果你想得到请求响应码的话,chunk.xhr.status 就是。 |
(rootFile: UploadFile,file: UploadFile,message: string,chunk: UploadChunk) => void; |
() => {}; |
onFileComplete | 一个根文件(文件夹)成功上传完成。 |
(rootFile: UploadFile) => void |
() => {}; |
onComplete | 上传完毕 |
() => void |
() => {}; |
onFileRemove | 一个文件(文件夹)被移除 |
(file: UploadFile) => void; |
() => {}; |
onFileRetry | 文件重试上传事件 |
(rootFile: UploadFile,file: UploadFile,chunk: UploadChunk) => void |
() => {}; |
onFileError | 上传过程中出错了 |
(rootFile: UploadFile,file: UploadFile,message: string,chunk: UploadChunk)=>void; |
() => {}; |
children | 自定义样式时使用,类似Vue的作用域插槽 |
({ fileList }: { fileList: UploadFile[] }) => JSX.Element; |
- |
当前浏览器是否支持 File API 上传,不支持时作提示使用
参数 | 说明 | 类型 | 默认值 |
children | 不支持File API上传时的提示信息 |
JSX.Element; |
<p>你的浏览器等级过低暂不支持上传组件</p> |
拖拽上传组件
参数 | 说明 | 类型 | 默认值 |
children | 拖拽上传组件区域内显示的内容,一般直接存放上传按钮 |
JSX.Element; |
<p className="m-3">把文件拖拽到此处进行上传</p>
<div className="flex">
<UploaderBtn>选择文件</UploaderBtn>
<UploaderBtn directory={true}>选择文件夹</UploaderBtn>
</div> |
点击上传文件按钮
参数 | 说明 | 类型 | 默认值 |
children | 上传按钮中的内容 |
JSX.Element | string; |
- |
directory | 是否上传文件夹 | Boolean | false |
single | 是否一次只能选择一个文件夹 | Boolean | false |
文件文件夹列表
参数 | 说明 | 类型 | 默认值 |
children | 类Vue作用域插槽 应该返回UploadFile组件或自定义文件组件使用 |
({ fileList }: { fileList: UploadFile[] }) => JSX.Element; |
<ul>
{fileList.map((file) => (
<UploaderFile key={file.id} file={file} list={true}>
</UploaderFile>
))}
</ul> |
fileList | 文件列表 | UploadFile[] | - |
文件 文件夹单个组件
参数 | 说明 | 类型 | 默认值 |
file | 文件对象 | UploadFile | - |
children |
文件组件的类Vue作用域插槽 返回新的自定义的上传组件
|
(fileInfo: {
name: string;
size: string;
averageSpeed: string;
error: boolean;
paused: boolean;
isComplete: boolean;
isUploading: boolean;
progress: number;
formatedTimeRemaining: string;
status: 'success' | 'error' | 'uploading' | 'paused' | 'waiting';
statusText: string;
fileCategory: string;
pause: () => void;
retry: () => void;
resume: () => void;
remove: () => void;
}) => JSX.Element; |
- |
例子主要包含自定义样式 以及 断点续传和秒传的实现
-
定制样式仅做参考 案例演示的 css 类使用的是 tailwindcss 可以自行调整样式
-
断点续传和秒传 需要配合服务端共同实现,可以参考react-admin-background的实现
- 首先配置 testChunks,checkChunkUploadedByResponse 函数如下
- testChunks 为 true 时每次上传文件前先发送一个 url 为"/uploadChunk"的 get 请求,判断文件及切片的上传情况,返回值主要是 skip 字段和 uploaded 字段
- skip:boolean 字段判断是否需要秒传,skip:true,表明文件上传过,直接秒传即可
- uploaded:number[] 字段表明已经上传过的切片
- checkChunkUploadedByResponse:这个函数在每个切片正式上传前触发,chunk 参数是当前的切片,message 是 get 请求返回的字符串形式的结果,如果返回 true 表明该切片成功上传,不需要重复上传
-
关于 merge 这个请求:这个请求可以根据后端业务需求考虑是否需要单独添加,实际后端服务器是可以在切片上传完成后自动进行合并操作。
import { Divider, Space } from 'antd';
import {
CaretRightOutlined,
ReloadOutlined,
PauseOutlined,
CloseOutlined
} from '@ant-design/icons';
import { useState } from 'react';
import Uploader, {
UnSupport,
UploaderBtn,
UploaderDrop,
UploaderFile,
UploaderList
} from 'react-simple-upload';
import { UploadFile, UploadChunk } from 'react-simple-upload/dist/types';
export default function Upload() {
const [options] = useState({
target: '//localhost:3000/uploadChunk',
// 秒传测试,为true进行一次get请求判断上传的内容
testChunks: true,
// 每个chunk进行上传前的校验工作,返回true表明该chunk进行上传
checkChunkUploadedByResponse: (chunk: UploadChunk, message: string) => {
const { data } = JSON.parse(message);
if (data.skip) {
return true;
}
return (data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
}
});
// 所有切片上传完成时进行文件合并操作
const fileComplete = (rootFile: UploadFile) => {
// 文件夹上传时的合并方式
if (rootFile.isFolder) {
for (const file of rootFile.files) {
merge({
identifier: file.uniqueIdentifier,
filename: file.name,
uploadBy: userId,
size: file.size
});
}
} else {
// 文件上传时的合并方式
merge({
identifier: uploadTool.cleanIdentifier(rootFile.uniqueIdentifier),
filename: rootFile.name,
uploadBy: userId,
size: uploadTool.transferSize(rootFile.size)
});
}
console.log(rootFile, 'fileComplete');
};
const complete = () => {
console.log('complete');
};
return (
<Uploader
options={options}
onFileComplete={fileComplete}
onComplete={complete}
>
{({ fileList }) => (
<>
<UnSupport></UnSupport>
<UploaderDrop>
<>
<p>拖拽上传</p>
<div className="flex mt-2">
<UploaderBtn>选择文件</UploaderBtn>
<UploaderBtn directory={true}>选择文件夹</UploaderBtn>
</div>
</>
</UploaderDrop>
<UploaderList fileList={fileList}>
{() => (
<>
{fileList.map((file) => (
<UploaderFile file={file} list={true} key={file.id}>
{({
name,
size,
averageSpeed,
pause,
retry,
remove,
resume,
formatedTimeRemaining,
error,
isComplete,
isUploading,
paused,
status,
statusText,
progress,
fileCategory
}) => (
<div className="uploader-file flex ">
<span className="flex-1">{fileCategory}</span>
<span className="flex-1">{name}</span>
<span className="flex-1">{size}</span>
<span className="flex-1">
{isUploading ? (
<>
<span>{Math.floor(progress * 100) + '%'}</span>
<span className="mx-3">{averageSpeed}</span>
<span>{formatedTimeRemaining}</span>
</>
) : (
statusText
)}
</span>
{isUploading && (
<div className="flex-1 flex items-center justify-center ">
<Space>
<PauseOutlined onClick={pause} />
<CloseOutlined onClick={remove} />
</Space>
</div>
)}
{error && (
<span
className="flex-1 flex items-center justify-center "
onClick={retry}
>
<ReloadOutlined />
</span>
)}
{paused && (
<span
className="flex-1 flex items-center justify-center "
onClick={resume}
>
<CaretRightOutlined />
</span>
)}
</div>
)}
</UploaderFile>
))}
</>
)}
</UploaderList>
</>
)}
</Uploader>
);
}