Vue3 项目, 使用 vue-simple-uploader 实现分块/片上传分块上传: 顾名思义, 就是把一个需要上
前言
分块上传: 顾名思义, 就是把一个需要上传的大文件, 分成若干份小块, 一块一块的传输给服务器, 然后再合并成一个大文件的过程。
分块上传的优点: 巴拉巴拉巴拉小魔仙。。。
需求背景:
最近做了一个项目需要对接甲方的上传, 我们上传文件的最大才限制了 50M
, 却发现超过 30M
的文件上传会超时、失败。
得到对方技术的回复是: 超过30M
的文件上传需要对接他们的分片上传功能, 没听错没写错也没看错,不是30G
也不是 300M
, 仅仅是 30M
。由于【专网】的限制,沟通成本和对接成本非常高,也不和他们扯那么多,怎么要求就怎么来吧。
我没做过分块上传的功能,网上大概搜了下方案, 后面看到 vue-simple-uploader, 一个基于 simple-uploader.js 的 Vue 上传组件, 感觉能实现我需要的功能, 文档有中文的, 用起来比较方便。
先看完成的效果
官方提供的效果
我完成的效果
流程梳理
第一步: 通过上传组件的 @file-added
方法拿到文件信息, 调用接口【文件分块上传_初始化】拿到这次上传任务的{上传ID}
第二步: 将{分块标识, 上传ID}通过调用接口【分块上传】, 开始分块上传
第三步: 分块上传完成后, 上传组件的 @file-success
方法中调用接口【分块上传_合并】,拿到上传完成的{文件ID}, 上传至此完成
动手使用
安装 vue-simple-uploader
需要加上 @next
才是 Vue3
的版本, 否则下载的是 Vue2
的版本
npm install vue-simple-uploader@next --save
main.js
import { createApp } from 'vue';
import App from './App.vue';
import uploader from 'vue-simple-uploader';
import 'vue-simple-uploader/dist/style.css';
// ...
createApp(App).use(uploader).mount('#app');
页面使用, 详细说明查看代码中的注释
<template>
<uploader
:options="options"
:auto-start="false"
class="uploader-container"
@file-added="onFileAdded"
@file-success="onFileSuccess"
>
<uploader-unsupport>您的浏览器不支持上传组件</uploader-unsupport>
<uploader-drop>
<uploader-btn :attrs="attrs">选择文件上传</uploader-btn>
</uploader-drop>
<uploader-list>
<!-- 我自定义了文件列表, 如果没有这个需求, <uploader-list>内部留空即可 -->
<template #default="props">
<!-- 原已上传的文件列表: 将原来已经上传的文件展示出来 -->
<div v-for="file in uploadFileList" :key="file.fileId" :file="file" class="file-item">
<div class="file-progress success" style="width: 100%"></div>
<SimpleUploaderIcon :file="file" />
<div class="file-title van-ellipsis">{{ file.name }}</div>
<div class="file-status">已上传</div>
<van-button size="mini" type="danger" @click="onDelete(uploadFileList, file)">删除文件</van-button>
</div>
<!-- 新上传的文件列表: 本次通过组件上传的操作区域 -->
<uploader-file v-for="file in props.fileList" :key="file.fileId" :file="file" :list="true">
<template #default="fileProps">
<div class="file-item">
<div class="file-progress" :class="{ success: fileProps.status === 'success', error: fileProps.status === 'error' }" :style="{ width: fileProps.progressStyle.progress }"></div>
<SimpleUploaderIcon :file="fileProps" />
<div class="file-title van-ellipsis">{{ file.name }}</div>
<div v-show="fileProps.status === 'success'" class="file-status">上传成功</div>
<div v-show="fileProps.status === 'error'" class="file-status">上传失败</div>
<div v-show="fileProps.status === 'uploading'" class="file-status">正在上传({{ fileProps.progressStyle.progress }})</div>
<van-button v-show="fileProps.status === 'success'" type="danger" @click="onDelete(props.fileList, file)">删除文件</van-button>
<van-button v-show="fileProps.status === 'error'" type="warning" @click="onCancel(file)">移除文件</van-button>
<van-button v-show="fileProps.status === 'uploading'" type="danger" @click="onCancel">取消上传</van-button>
</div>
</template>
</uploader-file>
</template>
</uploader-list>
</uploader>
</template>
<script setup>
import SimpleUploaderIcon from './SimpleUploaderIcon.vue'; // 一个小组件: 根据文件类型展示不同的图标, 不重要
import { uploadInit, uploadComplete } from '@/api/attachment';
import { showFailToast, showSuccessToast, showToast } from 'vant';
// 原来已上传过的文件列表
const uploadFileList = ref([
{
fileId: '23fdklk23nm23lmnl32kkl',
extension: 'png',
name: '20240505.png',
},
]);
/**
* 上传组件配置
*/
const options = {
target: '//localhost:3000/upload', // 目标上传 URL
// target: '/api/upload', // 目标上传 URL
headers: { Authorization: '授权token' }, // 接口的定义, 根据实际情况而定
chunkSize: 5 * 1024 * 1024, // 分块大小
forceChunkSize: true, // 是否强制所有的块都是小于等于 chunkSize 的值。默认是 false。
fileParameterName: 'file', // 上传文件时文件的参数名,默认file
maxChunkRetries: 3, // 最大自动失败重试上传次数
testChunks: false, // 是否开启服务器分片校验
// 组件上传会默认携带很多参数, 可以通过query和processParams进行修改
// 额外的请求参数
query: (file) => {
return {
...file.params,
};
},
// 处理请求参数, 将参数名字修改成接口需要的
processParams: (params) => {
params.f_PartNumber = params.chunkNumber; // 分块标识(1,2,3,4...)
return params;
},
};
// 限制上传的文件类型
const attrs = {
accept: '.png,.jpg,.txt,.pdf,.ppt,.pptx,.doc,docx,.xls,xlsx,.ofd',
};
/**
* 这个事件一般用作文件校验,如果说返回了 false, 那么这个文件就会被忽略,不会添加到文件上传列表中
* 我模版中禁止了自动上传(:auto-start="false"), 在这里校验文件和拿到上传{上传ID}后需要调用继续上传
*/
const onFileAdded = async (file) => {
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
showFailToast({ message: '最大支持上传 50MB 的文件', icon: 'warning-o' });
file.cancel(); // 取消上传且从文件列表中移除。
return false;
}
// 先设置为暂停状态,才能继续上传
file.pause();
// 文件分块上传_初始化,拿到上传{上传ID}
const { data } = await uploadInit({ fileName: file.name });
if (data.code === 200 && data.data) {
file.params = { f_UploadId: data.data.f_UploadId };
// 从 uploader 实例调用上传方法.upload() 方法会报错, 我不知道为什么, 故而使用 resume()。
// 继续上传
file.resume();
} else {
file.cancel(); // 拦截器会报错误信息, 我这里直接取消上传且从文件列表中移除。
}
};
/**
* 一个文件上传成功事件,
* 第一个参数 rootFile 就是成功上传的文件所属的根 Uploader.File 对象,它应该包含或者等于成功上传文件;
* 第二个参数 file 就是当前成功的 Uploader.File 对象本身;
* 第三个参数就是 d 就是服务端响应内容,永远都是字符串!!!!!!!!
* 第四个参数 chunk 就是 Uploader.Chunk 实例,它就是该文件的最后一个块实例,如果你想得到请求响应码的话,chunk.xhr.status 就是。
*/
const onFileSuccess = async (rootFile, file, d) => {
const res = JSON.parse(d); // 转成json
if (res.code === 200) {
// 分块上传_合并(分块上传完毕后调用)
const { data } = await uploadComplete({ f_UploadId: file.params.f_UploadId });
if (data.code === 200 && data.data) {
file.fileId = data.data.f_FileId; // 提供给后续删除操作
showSuccessToast('上传成功, 附件已保存到报销明细中');
} else {
onCancel(file);
}
} else {
// 分块上传不走拦截器, 所以这里写报错提示
showToast({ message: '上传失败, 请重试', icon: 'cross' });
onCancel(file);
}
};
const onDelete = (uploadListData, file) => {
// 确认提示
// 调用删除接口
// 成功后删除列表数据
const findIndex = uploadListData.findIndex((v) => v.fileId === file.fileId);
if (findIndex !== -1) {
uploadListData.splice(findIndex, 1);
}
};
/**
* 取消本次上传任务
*/
const onCancel = async (file) => {
// 调用取消上传接口
// 取消上传且从文件列表中移除。
file.cancel();
};
</script>
// css 不重要
其他说明
- 官方提供有分块上传接口, 按照说明运行即可: github.com/simple-uplo…
- 上方模版中
props
fileList
file
fileProps
的数据属性比较多, 不再介绍, 可以直接在页面输出来看 - 没有做断点续传的功能, 上传失败的时候我就调用取消上传的接口将本次上传任务取消
- 上传异常/失败的时候也调用了任务取消让用户重新上传
- 分块上传的时候会提交默认参数, 可通过给
options
配置query
和processParams
修改或添加参数, 我添加的参数如划线所示
(完结)
需求简单, 代码就简单, 复杂的业务看下方的文档吧
参考
转载自:https://juejin.cn/post/7376465327359557670