前言
公司代码重构,一些技术需要调整,就比如上传下载功能。上个版本使用的是华为云的obs ,因为某些原因后端架构调整,导致前端也要进行改变,经讨论使用MiniIo和OnlyOffice。
基本思路
- 点击上传文件拿到文件大小
- 根据分片大小把文件进行分割拿到分片数量
- 请求创建文件分片接口,拿到后端给的分片信息和单个文件上传的地址
- 请求每个单个文件上传地址
- 上传完成每个单个文件地址后请求后端合并上传接口通知他已经上传完成。
代码实现-完整代码在最下方
html
<template>
<div>
<el-upload action="#" :before-upload="handleBeforeUpload" :http-request="handleHttpRequest" :show-file-list="false" :multiple="multiple" :drag="drag">
<el-button type="primary">点击上传</el-button>
</el-upload>
<!-- 上传文件的列表 -->
<div class="fileBox" v-if="showFile">
<div v-for="task in uploadTaskList" :key="task.uid" class="fileItem">
<div class="rowBox">
<div class="name" @click.stop="openPreview(task)">{{ task.fileName }}</div>
<div class="delBox">
<i class="el-icon-close" @click.stop="handleRemove(task.uid)"></i>
</div>
</div>
<el-progress :percentage="task.totalPercent" :stroke-width="2" style="width: 100%;" v-if="task.totalPercent < 100" />
</div>
</div>
</div>
</template>
分析
使用element ui上传的手动上传功能来实现,节省开发时间。上面主要代码为before-upload和http-request
上传前的操作:调用element ui的
before-upload
上传前的钩子 这里为了控制上传文件类型、上传大小、最大个数,实现方式如下
js
// 上传前回调
handleBeforeUpload(file) {
if (this.uploadTaskList.length >= this.limit) {
this.$message.error(`最多上传${this.limit}个文件`)
return false
}
let fileSize = file.size / 1024 / 1024
// 文件大小限制
if (fileSize > this.sizeLimit) {
this.$message.error(`文件大小不能超过${this.sizeLimit}M`)
return false
}
// 分片大小 默认 5M
this.chunkSize = this.sliceSize * 1024 * 1024
return true
}
其中uploadTaskList
上传任务列表即上传文件列表 limit
最大个数 sizeLimit
为文件大小限制
手动上传
调用elementui的
http-request
方法实现手动上传,这里就要做上面基本思路
的操作了
js
// 上传
async handleHttpRequest(options) {
// 获取文件
const file = options.file
// 获取分片数量
const chunksCount = Math.ceil(file.size / this.chunkSize)
// 创建上传任务
let task = {
index: this.uploadTaskList.length,
uid: file.uid,
// 文件信息
file: file,
fileName: file.name,
fileUrl: '',
size: file.size,
// 获取文件大小
totalSize: file.size,
// 获取分片数量
chunksCount: chunksCount,
// 已上传的总字节数
totalUploaded: 0,
// 进度相关:记录每个分片已上传的字节数
uploadedSizePerChunk: new Array(chunksCount).fill(0),
// 上传进度
totalPercent: 0,
// 创建控制器
controller: new AbortController(),
// 分片地址列表
chunkUrlList: [],
// 上传文件id
uploadId: '',
// 上传文件参数
filePrefix: '',
// 上传文件名称
finalName: '',
// 标记正在上传
isUploading: true
}
this.uploadTaskList.push(task)
// 更新进度的方法
const updateProgress = () => {
const percent = Math.ceil((task.totalUploaded / task.totalSize) * 100)
task.totalPercent = percent
// el-upload 会监听这个
options.onProgress({ percent: percent });
};
// 获取-创建分片上传的参数
let params = {
bucketName: 'files',
chunkSize: chunksCount,
fileName: file.name
}
// 创建分片上传
try {
const initRes = await createMultipartUpload(params)
if (initRes.code == 500) {
this.$message.error(initRes.msg)
return
}
// 获取创建分片的参数
task.uploadId = initRes.uploadId
task.filePrefix = initRes.filePrefix
task.finalName = initRes.finalName
// 获取分片地址列表并排序
task.chunkUrlList = []
for (let i = 0; i < task.chunksCount; i++) {
const key = `chunk_${i}`
if (!initRes[key]) {
throw new Error(`缺少第 ${i + 1} 个分片上传地址`)
}
task.chunkUrlList[i] = initRes[key]
}
// ---------------------------------------------------开始分片上传-------------------------------------------------
// 并发上传所有分片
const uploadPromises = []
for (let i = 0; i < task.chunksCount; i++) {
// 获取分片开始位置
const start = i * this.chunkSize
// 获取分片结束位置
const end = Math.min(start + this.chunkSize, task.totalSize)
// 分割文件
const chunkBlob = file.slice(start, end)
// 分片上传
const promise = this.uploadChunk(task.chunkUrlList[i], chunkBlob, task.controller.signal, (progressEvent) => {
// 计算这个分片上传了多少
const chunkUploaded = progressEvent.loaded;
// 更新"该分片"上传量(防止重复叠加)
task.totalUploaded = task.totalUploaded - task.uploadedSizePerChunk[i] + chunkUploaded;
task.uploadedSizePerChunk[i] = chunkUploaded;
// 更新进度条
updateProgress();
})
uploadPromises.push(promise)
}
// 等待所有分片完成
await Promise.all(uploadPromises)
const mergeData = {
bucketName: 'files',
objectName: task.filePrefix + task.finalName,
uploadId: task.uploadId,
}
// 合并分片完成上传
const res = await completeMultipartUpload(mergeData)
if (res.code == 200) {
task.fileUrl = res.data.uploadUrl
// this.$message.success(`${file.name} 上传成功!`)
// 通知 el-upload 上传成功
options.onSuccess(res);
} else {
console.log(res, '-------------res');
this.$message.error(`${file.name} 合并失败:${res.msg}`)
options.onError(new Error(res.msg));
}
} catch (error) {
console.log(error, '-------------error', task);
let index = this.uploadTaskList.findIndex(v => v.uid === task.uid)
if (index > -1) {
this.uploadTaskList.splice(index, 1)
}
if (error.name === 'AbortError' || error.message.toLowerCase().includes('canceled') || error.code === 'ERR_CANCELED') {
this.$message.info('用户取消上传')
} else {
this.$message.error('上传失败')
options.onError(error)
}
} finally {
task.isUploading = false
// 检查是否所有任务都已完成
this.$nextTick(() => {
this.checkAllUploaded()
})
}
}
js
// 上传单个分片
uploadChunk(url, fileBlob, signal, onUploadProgress) {
return axios.put(url, fileBlob, {
signal: signal,
// 设置请求进度
onUploadProgress
})
}
js
// 检查是否所有文件都上传完成
checkAllUploaded() {
const allFinished = this.uploadTaskList.every(task => !task.isUploading)
if (allFinished && this.uploadTaskList.length) {
this.handleAfterAllUploads()
}
}
js
// 所有文件上传完成后的回调
handleAfterAllUploads() {
// console.log('上传完成', this.uploadTaskList);
this.$emit('success', this.uploadTaskList)
}
js
// 删除文件
handleRemove(uid) {
const index = this.uploadTaskList.findIndex(v => v.uid === uid)
if (index !== -1) {
const task = this.uploadTaskList[index]
if (task.isUploading) {
task.controller.abort()
}
this.uploadTaskList.splice(index, 1)
}
},
// 预览
openPreview(val) {
if (val.fileUrl) {
previewShowView(val.fileUrl)
}
}
完整代码如下
js
<template>
<div>
<el-upload action="#" :before-upload="handleBeforeUpload" :http-request="handleHttpRequest" :show-file-list="false" :multiple="multiple" :drag="drag">
<el-button type="primary">点击上传</el-button>
</el-upload>
<!-- 列表 -->
<div class="fileBox" v-if="showFile">
<div v-for="task in uploadTaskList" :key="task.uid" class="fileItem">
<div class="rowBox">
<div class="name" @click.stop="openPreview(task)">{{ task.fileName }}</div>
<div class="delBox">
<i class="el-icon-close" @click.stop="handleRemove(task.uid)"></i>
</div>
</div>
<el-progress :percentage="task.totalPercent" :stroke-width="2" style="width: 100%;" v-if="task.totalPercent < 100" />
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import { createMultipartUpload, completeMultipartUpload } from '@/api/common'
import { previewShowView } from '@/utils/previewShowView.js'
export default {
props: {
// 是否支持多选文件
multiple: {
type: Boolean,
default: false
},
// 最大允许上传个数
limit: {
type: Number,
default: 5
},
// 限制大小默认10M
sizeLimit: {
type: Number,
default: 300
},
// 分片大小
sliceSize: {
type: Number,
default: 5
},
// 是否支持拖拽上传
drag: {
type: Boolean,
default: false
},
// 文件类型
fileList: {
type: Array,
default: () => []
},
// 是否显示上传后的数据
showFile: {
type: Boolean,
default: true,
},
},
data() {
return {
// 分片大小
chunkSize: 0,
// 上传任务列表
uploadTaskList: []
}
},
watch: {
fileList: {
handler(val) {
this.uploadTaskList = val.map(v => {
let uid = v.uid || Date.now() + Math.random()
return {
...v,
isUploading: false,
uid: uid
}
})
},
immediate: true
}
},
methods: {
// 上传前回调
handleBeforeUpload(file) {
if (this.uploadTaskList.length >= this.limit) {
this.$message.error(`最多上传${this.limit}个文件`)
return false
}
let fileSize = file.size / 1024 / 1024
// 文件大小限制
if (fileSize > this.sizeLimit) {
this.$message.error(`文件大小不能超过${this.sizeLimit}M`)
return false
}
// 分片大小 默认 5M
this.chunkSize = this.sliceSize * 1024 * 1024
return true
},
// 上传
async handleHttpRequest(options) {
// 获取文件
const file = options.file
// 获取分片数量
const chunksCount = Math.ceil(file.size / this.chunkSize)
// 创建上传任务
let task = {
index: this.uploadTaskList.length,
uid: file.uid,
// 文件信息
file: file,
fileName: file.name,
fileUrl: '',
size: file.size,
// 获取文件大小
totalSize: file.size,
// 获取分片数量
chunksCount: chunksCount,
// 已上传的总字节数
totalUploaded: 0,
// 进度相关:记录每个分片已上传的字节数
uploadedSizePerChunk: new Array(chunksCount).fill(0),
// 上传进度
totalPercent: 0,
// 创建控制器
controller: new AbortController(),
// 分片地址列表
chunkUrlList: [],
// 上传文件id
uploadId: '',
// 上传文件参数
filePrefix: '',
// 上传文件名称
finalName: '',
// 标记正在上传
isUploading: true
}
this.uploadTaskList.push(task)
// 更新进度的方法
const updateProgress = () => {
const percent = Math.ceil((task.totalUploaded / task.totalSize) * 100)
task.totalPercent = percent
// el-upload 会监听这个
options.onProgress({ percent: percent });
};
// 获取-创建分片上传的参数
let params = {
bucketName: 'files',
chunkSize: chunksCount,
fileName: file.name
}
// 创建分片上传
try {
const initRes = await createMultipartUpload(params)
if (initRes.code == 500) {
this.$message.error(initRes.msg)
return
}
// 获取创建分片的参数
task.uploadId = initRes.uploadId
task.filePrefix = initRes.filePrefix
task.finalName = initRes.finalName
// 获取分片地址列表并排序
task.chunkUrlList = []
for (let i = 0; i < task.chunksCount; i++) {
const key = `chunk_${i}`
if (!initRes[key]) {
throw new Error(`缺少第 ${i + 1} 个分片上传地址`)
}
task.chunkUrlList[i] = initRes[key]
}
// ---------------------------------------------------开始分片上传-------------------------------------------------
// 并发上传所有分片
const uploadPromises = []
for (let i = 0; i < task.chunksCount; i++) {
// 获取分片开始位置
const start = i * this.chunkSize
// 获取分片结束位置
const end = Math.min(start + this.chunkSize, task.totalSize)
// 分割文件
const chunkBlob = file.slice(start, end)
// 分片上传
const promise = this.uploadChunk(task.chunkUrlList[i], chunkBlob, task.controller.signal, (progressEvent) => {
// 计算这个分片上传了多少
const chunkUploaded = progressEvent.loaded;
// 更新"该分片"上传量(防止重复叠加)
task.totalUploaded = task.totalUploaded - task.uploadedSizePerChunk[i] + chunkUploaded;
task.uploadedSizePerChunk[i] = chunkUploaded;
// 更新进度条
updateProgress();
})
uploadPromises.push(promise)
}
// 等待所有分片完成
await Promise.all(uploadPromises)
const mergeData = {
bucketName: 'files',
objectName: task.filePrefix + task.finalName,
uploadId: task.uploadId,
}
// 合并分片完成上传
const res = await completeMultipartUpload(mergeData)
if (res.code == 200) {
task.fileUrl = res.data.uploadUrl
// this.$message.success(`${file.name} 上传成功!`)
// 通知 el-upload 上传成功
options.onSuccess(res);
} else {
console.log(res, '-------------res');
this.$message.error(`${file.name} 合并失败:${res.msg}`)
options.onError(new Error(res.msg));
}
} catch (error) {
console.log(error, '-------------error', task);
let index = this.uploadTaskList.findIndex(v => v.uid === task.uid)
if (index > -1) {
this.uploadTaskList.splice(index, 1)
}
if (error.name === 'AbortError' || error.message.toLowerCase().includes('canceled') || error.code === 'ERR_CANCELED') {
this.$message.info('用户取消上传')
} else {
this.$message.error('上传失败')
options.onError(error)
}
} finally {
task.isUploading = false
// 检查是否所有任务都已完成
this.$nextTick(() => {
this.checkAllUploaded()
})
}
},
// 上传单个分片
uploadChunk(url, fileBlob, signal, onUploadProgress) {
return axios.put(url, fileBlob, {
signal: signal,
// 设置请求进度
onUploadProgress
})
},
// 检查是否所有文件都上传完成
checkAllUploaded() {
const allFinished = this.uploadTaskList.every(task => !task.isUploading)
if (allFinished && this.uploadTaskList.length) {
this.handleAfterAllUploads()
}
},
// 所有文件上传完成后的回调
handleAfterAllUploads() {
// console.log('上传完成', this.uploadTaskList);
this.$emit('success', this.uploadTaskList)
},
// 删除文件
handleRemove(uid) {
const index = this.uploadTaskList.findIndex(v => v.uid === uid)
if (index !== -1) {
const task = this.uploadTaskList[index]
if (task.isUploading) {
task.controller.abort()
}
this.uploadTaskList.splice(index, 1)
}
},
// 预览
openPreview(val) {
if (val.fileUrl) {
previewShowView(val.fileUrl)
}
}
}
}
</script>
<style lang="scss" scoped>
.fileBox {
margin-top: 10px;
.fileItem {
margin-top: 5px;
.rowBox {
position: relative;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
background-color: #f5f7fa;
.name {
color: #409eff;
}
.delBox {
opacity: 1;
}
}
.name {
color: #606266;
display: block;
margin-right: 40px;
overflow: hidden;
padding-left: 4px;
text-overflow: ellipsis;
transition: color 0.3s;
white-space: nowrap;
text-align: left;
}
.delBox {
cursor: pointer;
opacity: 0;
color: #606266;
}
}
}
}
</style>