使用tus-js-client实现Tus多文件顺序上传及进度条波动修复
问题修复方案
核心问题定位
- 进度条波动原因:所有上传任务共用同一条进度更新逻辑,每个文件的
onProgress回调都会直接覆盖全局进度值,没有统计所有文件的累计上传量,导致数值在不同文件的进度之间跳变 - 无法串行上传原因:遍历文件列表时给开始按钮重复绑定了多次点击事件,点击后所有文件的上传任务会同时启动,没有做队列顺序控制
- 额外逻辑bug:单个文件上传完成就调用
reset()清空进度,且每次选择文件都会重复绑定按钮事件,多次选文件后会出现重复上传问题
实现思路
- 串行上传控制:选中文件后先把所有上传任务存入队列,点击开始时仅启动队列第一个任务,当前文件上传成功后自动触发下一个任务,直到队列全部执行完成
- 全局进度计算:提前统计所有待上传文件的总字节数,维护全局累计已上传字节数映射表,每个文件触发进度更新时,先更新该文件自身的已上传值,再汇总所有文件的已上传量计算全局进度,避免进度跳变
- 事件绑定优化:将开始按钮的点击事件移到文件选择逻辑外部,避免重复绑定
修正后完整代码
JavaScript 部分
const fileInput = document.querySelector('#js-file-input') const dataDiv = document.querySelector('#data-pre'); const toggleBtn = document.querySelector('#toggle-btn') const progressBar = document.querySelector('.progress-bar') const textProgress = document.querySelector('#js-upload-text-progress') const uploadList = document.querySelector('#upload-list') // 全局状态管理 let uploadQueue = [] // 上传任务队列 let totalBytes = 0 // 所有文件总大小 let fileUploadedBytes = [] // 每个文件已上传的字节数,索引和队列对应 let currentUploadIndex = 0 // 当前正在上传的文件索引 function reset() { progressBar.style.width = `${0}%` textProgress.textContent = "" // 重置所有全局状态 uploadQueue = [] totalBytes = 0 fileUploadedBytes = [] currentUploadIndex = 0 toggleBtn.textContent = 'start' toggleBtn.disabled = false } /** * 字节数格式化 */ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes' const k = 1024 const dm = decimals < 0 ? 0 : decimals const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } /** * 更新全局进度 */ function updateGlobalProgress() { const totalUploaded = fileUploadedBytes.reduce((sum, val) => sum + val, 0) const percentage = totalBytes === 0 ? 0 : ((totalUploaded / totalBytes) * 100).toFixed(2) progressBar.style.width = `${percentage}%` textProgress.textContent = `已上传 ${formatBytes(totalUploaded)} / ${formatBytes(totalBytes)} (${percentage}%)` } /** * 启动下一个上传任务 */ function startNextUpload() { if (currentUploadIndex >= uploadQueue.length) { // 所有文件上传完成 textProgress.textContent = "全部文件上传完成" toggleBtn.disabled = true return } const currentUpload = uploadQueue[currentUploadIndex] currentUpload.start() } // 按钮点击事件仅绑定一次 toggleBtn.addEventListener('click', () => { if (uploadQueue.length === 0) return toggleBtn.disabled = true toggleBtn.textContent = '上传中' startNextUpload() }) fileInput.addEventListener('change', (e) => { // 每次选新文件先重置状态 reset() const files = e.target.files; dataDiv.innerHTML = '' Object.keys(files).forEach((k, index) => { const file = files[k] const fileId = URL.createObjectURL(file) totalBytes += file.size fileUploadedBytes[index] = 0 // 初始化每个文件的已上传字节为0 const chunkSize = parseInt(5242880, 10) const upload = new tus.Upload(file, { endpoint: "https://tusd.tusdemo.net/files/", chunkSize, retryDelays: [0, 1000, 3000, 5000], metadata: { filename: file.name, filetype: file.type }, onError: function (error) { console.log("上传失败: " + error); toggleBtn.disabled = false toggleBtn.textContent = '重试' }, onProgress: function (bytesUploaded, bytesTotal) { // 更新当前文件的已上传字节数 fileUploadedBytes[index] = bytesUploaded updateGlobalProgress() }, onSuccess: function () { const anchor = document.createElement('a') anchor.textContent = `下载 ${upload.file.name} (${formatBytes(upload.file.size)})` anchor.href = upload.url anchor.className = 'btn btn-success' uploadList.appendChild(anchor) // 启动下一个文件上传 currentUploadIndex++ startNextUpload() } }); // 把上传任务加入队列 uploadQueue.push(upload) // 渲染文件信息 dataDiv.innerHTML += ` <div class="file-data" key=${fileId}> <img alt="文件预览" src=${fileId} width="150" /> <div>文件名: ${file.name}</div> <div>文件类型: ${file.type}</div> <div>文件大小: ${formatBytes(file.size)}</div> </div> ` }); });
HTML 部分
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tus-js-client@3.0.0-0/dist/tus.min.js"></script> <div class="upload-card"> <div class="row"> <div class="col-md-12"> <div id="input-section"> <div class="heading">选择需要上传的文件:</div> <input type="file" id="js-file-input" accept="image/*, video/*" multiple> </div> <div id="progress-section"> <div class="heading" id="heading"></div> <div class="d-flex"> <div class="flex-grow-1"> <div class="progress"> <div class="progress-bar progress-bar-striped bg-success" role="progressbar"></div> </div> <div class="upload-text-progress" id="js-upload-text-progress"></div> </div> <div class="js-action-btn"> <button class="btn stop" id="toggle-btn">start</button> </div> </div> </div> </div> </div> </div> <hr> <h3>已完成上传</h3> <p>上传成功的文件会在此处展示下载入口</p> <p id="upload-list"></p> <br> <hr> <div id="data-pre"></div>
关键改动说明
- 移除了遍历文件时重复绑定按钮事件的逻辑,按钮点击事件仅在页面初始化时绑定一次,避免事件叠加
- 新增上传队列机制,每个文件上传成功后自动触发下一个任务,严格保证串行执行
- 全局进度基于所有文件的累计上传量计算,从根本上解决单个文件进度覆盖全局值导致的跳变问题
- 调整重置逻辑,仅在选择新文件、所有任务完成时才重置状态,单个文件传完不会清空进度
内容的提问来源于stack exchange,提问作者afi
相关产品推荐
相关产品推荐

