目录
- 1. 什么是分片上传
- 2. 上传组件模板
- 3. 上传组件逻辑
- 3.1 基本思路
- 3.2 选择上传文件
- 3.3 校验文件是否合法
- 3.4 文件加密
- 3.5 合并文件
- 3.6 文件切片上传
- 4. 参考文章
- 4.1 文章链接
- 4.2 参考文章提到的注意事项
- 4.2.1 nginx 上传大小限制
- 4.2.2 大文件下载
- 总结
1. 什么是分片上传
将 一个文件 切割为 一系列特定大小的 数据片段,将这些 数据片段 分别上传到服务端;
全部上传完成后,再由服务端将这些 数据片段 合并成为一个完整的资源;
上传过程中,由于外部因素(比如网络波动)导致上传中断,下次上传时会保留该文件的上传进度(断点续传);
2. 上传组件模板
包含三部分:
- 上传组件,使用 el-upload
- 进度条组件,使用 el-progress
- 上传完成状态组件,使用 el-input 自定义
<el-form-item label="上传附件" prop="uploadFile"> | |
<el-upload | |
v-if="!editForm.inlineAppVersionModel.fileName" | |
class="upload-demo" | |
drag | |
:show-file-list="false" | |
:action="APP_MANAGEMENT.uploadFile" | |
// 根据项目的接口传递参数 | |
:data="{ | |
applicationId: applicationId, | |
applicationVersion: applicationVersion, | |
bucketName: 'app' | |
}" | |
// 覆盖默认的http请求 | |
:http-request="handleFileUpload" | |
> | |
<el-icon class="el-icon--upload"> | |
<upload-filled /> | |
</el-icon> | |
<div v-if="!progress" class="el-upload__text"> | |
Drop file here or <em>click to upload</em> | |
</div> | |
// 进度条 | |
<el-progress | |
v-else | |
:text-inside="true" | |
:stroke-width="" | |
:percentage="progress" | |
status="success" | |
/> | |
</el-upload> | |
// 上传成功之后隐藏上传文件组件 | |
<div v-else style="display: flex;"> | |
<el-input v-model="editForm.inlineAppVersionModel.fileName" readonly> | |
</el-input> | |
<div style="display: flex;"> | |
<el-button | |
type="primary" | |
:icon="Download" | |
size="small" | |
@click="handleFileDownload" | |
/> | |
<el-button type="primary" :icon="Delete" size="small" @click="handleFileDel" /> | |
</div> | |
</div> | |
</el-form-item> |
3. 上传组件逻辑
3.1 基本思路
使用 el-upload 选择文件
选择成功的 回调函数 可以读取文件信息,用于前端校验文件的合法性
前端校验文件合法后,将文件进行切片
通过 请求轮询 把切片传递给后端
3.2 选择上传文件
在这一步,可以获得文件信息
根据文件信息,对文件进行合法性校验
校验成功后,调用文件切片方法
/** | |
* @description: 选择上传文件 | |
* @param file el-upload 返回的参数 | |
*/ | |
const handleFileUpload = async (file: any) => { | |
console.log('el-upload 返回的参数 === ', file.file); | |
// 如果文件合法,则进行分片上传 | |
if (await checkMirrorFile(file)) { | |
// 文件信息 | |
const files = file.file; | |
// 从 开始的切片 | |
const shardIndex =; | |
// 调用 文件切片 方法 | |
uploadFileSilce(files, shardIndex); | |
// 文件非法,则进行提示 | |
} else { | |
ElMessage.error('请检查文件是否合法!'); | |
} | |
}; |
3.3 校验文件是否合法
校验文件格式
校验文件大小
调用接口,校验磁盘剩余空间大小
/** | |
* @description: 校验文件合法性 | |
*/ | |
const checkMirrorFile = async (file) => { | |
// 校验文件格式,支持.zip/.tar | |
const fileType = file.file.name.split('.') | |
if (fileType[fileType.length -] !== 'zip' && fileType[fileType.length - 1] !== 'tar') { | |
ElMessage.warning('文件格式错误,仅支持 .zip/.tar') | |
return false | |
} | |
// 校验文件大小 | |
const fileSize = file.file.size; | |
// 文件大小是否超出G | |
if (fileSize > * 1024 * 1024 * 1024) { | |
ElMessage.warning('上传文件大小不能超过G') | |
return false | |
} | |
// 调用接口校验文件合法性,比如判断磁盘空间大小是否足够 | |
const res = await checkMirrorFileApi() | |
if (res.code !==) { | |
ElMessage.warning('暂时无法查看磁盘可用空间,请重试') | |
return false | |
} | |
// 查看磁盘容量大小 | |
if (res.data.diskDevInfos && res.data.diskDevInfos.length >) { | |
let saveSize = | |
res.data.diskDevInfos.forEach(i => { | |
// 磁盘空间赋值 | |
if (i.devName === '/dev/mapper/centos-root') { | |
// 返回值为GB,转为字节B | |
saveSize = i.free * * 1024 * 1024 | |
} | |
}) | |
// 上传的文件大小没有超出磁盘可用空间 | |
if (fileSize < saveSize) { | |
return true | |
} else { | |
ElMessage.warning('文件大小超出磁盘可用空间容量') | |
return false | |
} | |
} else { | |
ElMessage.warning('文件大小超出磁盘可用空间容量') | |
return false | |
} | |
} |
3.4 文件加密
此处文件上传用 MD5 进行加密,需要安装依赖 spark-md5
npm i spark-md5
/** | |
* @description: 文件加密处理 | |
*/ | |
const getMD = (file: any): Promise<string> => new Promise((resolve, reject) => { | |
const spark = new SparkMD.ArrayBuffer(); | |
// 获取文件二进制数据 | |
const fileReader = new FileReader(); | |
fileReader.readAsArrayBuffer(file); // file 就是获取到的文件 | |
// 异步执行函数 | |
fileReader.addEventListener('load', (e: any) => { | |
spark.append(e.target.result); | |
const md: string = spark.end(); | |
resolve(md); | |
}); | |
fileReader.addEventListener('error', (e) => { | |
reject(e); | |
}); | |
}); |
3.5 合并文件
通过接口合并上传文件,接口需要的参数:
- 文件名
- 文件唯一 hash 值
接口合并完成后,前端展示已上传的文件名称
/** | |
* @description: 合并文件 | |
* @param name 文件名 | |
* @param hash 文件唯一 hash 值 | |
* @return 命名名称 | |
*/ | |
const composeFile = async (name: string, hash: string) => { | |
console.log('开始文件合并'); | |
const res = await uploadFileMerge({ | |
applicationId: props.applicationId, | |
applicationVersion: props.applicationVersion, | |
bucketName: 'app', | |
fileName: name, | |
hash, | |
}); | |
console.log('后端接口合并文件 ===', res); | |
if (res.status === && res.data.code) { | |
// 合并成功后,调整已上传的文件名称 | |
state.editForm.inlineAppVersionModel.fileName = name; | |
} | |
}; |
3.6 文件切片上传
接口轮询 —— 每次携带一个文件切片给后端;后端接受到切片 并 返回成功状态码后,再进行下一次切片上传
/** | |
* @description: 分片函数 | |
* @param file 文件 | |
* @param shardIndex 分片数量 | |
*/ | |
const uploadFileSilce = async (file: File, shardIndex: number) => { | |
// 文件名 | |
const { name } = file; | |
// 文件大小 | |
const { size } = file; | |
// 分片大小 | |
const shardSize = * 1024 * 5; | |
// 文件加密 | |
const hash: string = await getMD(file); | |
// 分片总数 | |
const shardTotal = Math.ceil(size / shardSize); | |
// 如果 当前分片索引 大于 总分片数 | |
if (shardIndex >= shardTotal) { | |
isAlive.value = false; | |
progress.value =; | |
// 合并文件 | |
composeFile(name, hash); | |
return; | |
} | |
// 文件开始结束的位置 | |
const start = shardIndex * shardSize; | |
const end = Math.min(start + shardSize, size); | |
// 开始切割 | |
const packet = file.slice(start, end); | |
// 拼接请求参数 | |
const formData = new FormData(); | |
formData.append('file', packet); | |
formData.append('applicationId', props.applicationId); | |
formData.append('applicationVersion', props.applicationVersion); | |
formData.append('bucketName', 'app'); | |
formData.append('hash', hash); | |
formData.append('shardSize', shardSize as unknown as string); | |
formData.append('seq', shardIndex as unknown as string); | |
// 如果 当前分片索引 小于 总分片数 | |
if (shardIndex < shardTotal) { | |
// 进度条保留两位小数展示 | |
progress.value = Number(((shardIndex / shardTotal) *).toFixed(2)) * 1; | |
// 调用文件上传接口 | |
const res = await uploadFile(formData); | |
if (res.status !==) { | |
ElMessage.error('上传失败'); | |
progress.value =; | |
return; | |
} | |
if (res.status === && res.data.code === 200) { | |
// 这里为所有切片上传成功后进行的操作 | |
console.log('上传成功'); | |
} | |
// eslint-disable-next-line no-param-reassign | |
shardIndex++; | |
// 递归调用 分片函数 | |
uploadFileSilce(file, shardIndex); | |
} | |
}; |
4. 参考文章
4.1 文章链接
前端大文件上传和下载(分片上传)
4.2 参考文章提到的注意事项
4.2.1 nginx 上传大小限制
nginx 默认上传大小为 1MB,若超过 1MB,则需要修改 nginx 配置 解除上传限制
4.2.2 大文件下载
/** | |
* @description: 动态创建 a 标签,实现大文件下载 | |
*/ | |
const downloadMirror = async (item) => { | |
let t = { | |
id: item.id, | |
} | |
const res = await downloadMirrorApi(t) | |
if (res.headers["content-disposition"]) { | |
let temp = res.headers["content-disposition"].split(";")[].split("filename=")[1] | |
let fileName = decodeURIComponent(temp) | |
// 通过创建a标签实现文件下载 | |
let link = document.createElement('a') | |
link.download = fileName | |
link.style.display = 'none' | |
link.href = res.data.msg | |
document.body.appendChild(link) | |
link.click() | |
document.body.removeChild(link) | |
} else { | |
ElMessage({ | |
message: '该文件不存在', | |
type: 'warning', | |
}) | |
} | |
} |