需求提要
通过两种方式进行文件上传:【直接上传】和【分步上传】
直接上传
字面意思,即点击了上传,一直到最后上传结束,整个流程没有阻碍
分步上传
点击上传后,流程会在中途暂停,等其他条件触发时才会继续后续的上传流程
【注意】:不管是直接上传还是分步上传,文件的上传都需要排队(即等前一个任务完成之后,才能继续下一个任务)
编码分析
采用队列数据结构
- 使用
fileList作为队列存储容器,用来存放待上传的文件 - 文件的处理遵循先进先出(
FIFO)的原则(队列的基本特征)
队列状态管理
-
通过几种关键状态来管理队列
isUploading标志:表示队列是否正在处理中currentFile:记录当前正在处理的文件- 文件状态追踪(
WAITING、WAITING_CALLBACK、SUCCESS)
队列操作
- 入队:通过
uploadClick函数添加新文件到队列 - 处理:通过
processUpload函数逐个处理文件 - 出队:文件状态变为
SUCCESS时相当于完成出队
顺序处理
-
创建一个可控的
Promise,能够在Promise外部控制其状态(resolve和reject)// 工具函数 - 创建全局可访问的promise const createDeferred = () => { let resolveCallback, rejectCallback const promise = new Promise((resolve, reject) => { resolveCallback = resolve, rejectCallback = reject }) return { promise, resolve: resolveCallback, reject: rejectCallback } } // # ======= 使用 ======= # const deferred = createDeferred() // 在某个时间点触发完成 promise setTimeout(() => { deferred.resolve('上传完成'); }, 1000); // 等待promise完成 await deferred.promise; -
通过
await currentFile.value.deferred.promise实现异步控制和同步等待 -
它会暂停当前的上传流程,等待当前文件完成上传完成后(调用
currentFile.value.deferred.resolve())才继续执行后续逻辑
编码实现
<template>
<div class="queue-demo-module">
<div class="btn-group">
<div
class="btn"
v-for="item in uploadButtons"
:id="item.id"
@click="uploadClick(item.type)"
>{{ item.text }}</div>
</div>
<div>
<ul>
<li v-for="item of fileList" :id="item.id">
<div>{{ item.name }}</div>
<div class="percent">{{ item.uploadProgress }}</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import { defineComponent, ref } from '@vue/composition-api'
import { nanoid } from 'nanoid'
const UPLOAD_STATUS = {
WAITING: 1, // 等待上传
WAITING_CALLBACK: 2, // 等待回调后上传
SUCCESS: 3, // 上传完成
}
export default defineComponent({
name: 'QueueDemo',
setup () {
const fileList = ref([]) // 文件队列
const isUploading = ref(false) // 是否有在上传文件的队列在进行
const currentFile = ref({}) // 当前上传的文件对象
const uploadButtons = [
{ id: 1, type: 'direct', text: '直接上传' },
{ id: 2, type: 'callback', text: '分步上传' },
]
// 上传过程 -【核心函数】
const processUpload = async () => {
if (isUploading.value) return
// 过滤待上传文件列表
const pendingFile = fileList.value.find(item => [UPLOAD_STATUS.WAITING, UPLOAD_STATUS.WAITING_CALLBACK].includes(item.uploadStatus))
// 如果没有待上传文件,队列结束
if (!pendingFile) {
isUploading.value = false
return
}
// 有,则标识队列进行
isUploading.value = true
const deferred = createDeferred() // 获取 deferred 对象
// 处理当前要上传的文件对象
currentFile.value = { ...pendingFile, deferred }
try {
if (currentFile.value.uploadStatus === UPLOAD_STATUS.WAITING) {
await handleProgress(currentFile.value, 100)
currentFile.value.deferred.resolve() // 同步上传完成
} else {
await handleProgress(currentFile.value, 50)
setTimeout(() => {
handleCallback() //【模拟分步上传的回调】
}, 1000);
}
// 触发一个全局promise等待,确保在文件真正上传完成(包括分步上传的回调)后才继续处理下一个文件
await currentFile.value.deferred.promise
console.warn(' == upload finish == ')
// 更新状态
fileList.value = fileList.value.map(file => {
return file.id === currentFile.value.id
? { ...file, uploadStatus: UPLOAD_STATUS.SUCCESS }
: file
})
// 寻找下一个待上传的文件对象
const hasMorePendding = fileList.value.some(file => [UPLOAD_STATUS.WAITING, UPLOAD_STATUS.WAITING_CALLBACK].includes(file.uploadStatus))
if (hasMorePendding) {
isUploading.value = false // 重置状态后再递归
await processUpload()
}
} catch (error) {
console.error(' == upload failed:', error)
} finally {
if (fileList.value.some(file => [UPLOAD_STATUS.WAITING, UPLOAD_STATUS.WAITING_CALLBACK].includes(file.uploadStatus))) {
isUploading.value = false
}
}
}
// 分步上传的回调
const handleCallback = async () => {
await updateProgress(currentFile.value, 100)
currentFile.value.deferred.resolve()
}
// 工具函数 - 创建全局可访问的promise
const createDeferred = () => {
let resolveCallback, rejectCallback
const promise = new Promise((resolve, reject) => {
resolveCallback = resolve,
rejectCallback = reject
})
return { promise, resolve: resolveCallback, reject: rejectCallback }
}
// 工具函数 - 睡眠函数
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const handleProgress = async (item, targetProgress) => {
await sleep(500) // 等待500ms
await updateProgress(item, targetProgress)
}
// 初始化上传文件对象
const createFileObject = (type) => ({
id: nanoid(),
name: `File-${nanoid()}-${type}`,
uploadProgress: 0,
uploadStatus: type === 'direct' ? UPLOAD_STATUS.WAITING : UPLOAD_STATUS.WAITING_CALLBACK,
})
// 更新上传进度
const updateProgress = (item, targetProgress) => {
const step = 1
const interval = 30 // 每50ms更新一次
return new Promise((resolve) => {
const timer = setInterval(() => {
if (item.uploadProgress < targetProgress) {
item.uploadProgress = Math.min(item.uploadProgress + step, targetProgress)
fileList.value = fileList.value.map(config => config.id === item.id ? item : config)
} else {
clearInterval(timer)
resolve()
}
}, interval)
})
}
// 事件处理
const uploadClick = async (type) => {
const obj = createFileObject(type)
fileList.value.push(obj)
await processUpload()
}
return {
fileList,
uploadButtons,
uploadClick,
}
}
})
</script>
<style lang="scss" scoped>
.btn-group {
.btn {
width: 160px;
height: 40px;
border-radius: 46px;
text-align: center;
line-height: 40px;
background: #FFFFFF;
border: 1px solid #CCCCCC;
font-family: Microsoft YaHei;
font-size: 14px;
font-weight: 400;
color: #333333;
cursor: pointer;
}
}
ul {
li {
display: flex;
.percent {
margin-left: 12px;
}
}
}
</style>