React文件上传组件:并行进度条改串行执行方案求助
文件上传进度串行执行实现方案
当前问题:文件上传组件中所有文件的进度计时器并行运行,需要改为串行执行——完成一个文件的上传进度后,再启动下一个文件的上传。
核心修改思路
现有实现中每个UploadFile子组件挂载时自动启动自身的上传计时器,导致并行执行。需要把上传的调度逻辑移到父组件,由父组件统一管理上传队列,控制串行执行的顺序。
具体修改步骤
改造
UploadFile组件- 移除组件挂载时自动启动上传的
useEffect - 暴露
startUpload方法,由父组件触发上传动作 - 新增上传完成/终止的回调,通知父组件切换到下一个文件
- 移除组件挂载时自动启动上传的
父组件添加串行调度逻辑
- 维护上传状态,跟踪当前是否有文件正在上传
- 监听文件列表变化,当无文件上传时,自动启动下一个待上传文件
修改后的代码
Upload.tsx(子组件)
import * as React from "react"; import { Stack, Button, ListItem } from "@material-ui/core"; import { useRef, forwardRef, useImperativeHandle } from "react"; import { Progress } from "./Progress"; export interface FileType { file: File; isDeleted: boolean; uploadStatus: number; uploadError: string; uploadAbort: boolean; } interface FileUploaderProps { selectedFiles: FileType[]; setSelectedFiles: React.Dispatch<React.SetStateAction<FileType[]>>; file: FileType; onUploadComplete?: () => void; } const UploadFile = forwardRef(({ file, setSelectedFiles, onUploadComplete }: FileUploaderProps, ref) => { const timerRef = useRef<ReturnType<typeof setInterval> | undefined>(); const pollRef = useRef<number>(file.uploadStatus); const updateProgress = () => { if (pollRef.current === 100) { clearInterval(timerRef.current); onUploadComplete?.(); return; } const diff = Math.random() * 15; setSelectedFiles((prevState) => { return prevState.map((current) => { if (current.file.name === file.file.name) { const newProgress = Math.min(current.uploadStatus + diff, 100); pollRef.current = newProgress; return { ...current, uploadStatus: newProgress }; } return current; }); }); }; const startUpload = () => { if (file.uploadStatus === 100 || file.uploadAbort || file.isDeleted || timerRef.current) return; timerRef.current = setInterval(updateProgress, 100 * Math.floor(Math.random() * 6) + 250); }; const cancelTimer = (e: React.MouseEvent<HTMLSpanElement>) => { e.stopPropagation(); clearInterval(timerRef.current); timerRef.current = undefined; setSelectedFiles((prevState) => { return prevState.map((current) => { if (current.file.name === file.file.name) { return { ...current, uploadAbort: true, uploadStatus: 0, uploadError: "File Upload cancelled" }; } return current; }); }); onUploadComplete?.(); }; useImperativeHandle(ref, () => ({ startUpload })); return ( <ListItem key={file.file.name} className="file-name"> <span style={{ padding: "10px" }}>{file.file.name}</span> {file.uploadStatus !== 100 ? ( file.uploadError || file.uploadAbort ? ( <div className="upload-error"> <span>{file.file.name}</span> <span style={{ padding: "10px" }}>{file.uploadError}</span> </div> ) : ( <> <span>Uploading...</span> <Stack sx={{ width: "80px", padding: "20px" }} spacing={2}> <Progress progress={file.uploadStatus} /> </Stack> <Button variant="contained" color="error" onClick={cancelTimer}> Cancel </Button> </> ) ) : ( <> <span className="primary">{file.file.name}</span> </> )} {file.uploadStatus === 100 && ( <Button variant="contained" color="error" className="mgmt-Delete-icon" style={{ padding: "10px" }} onClick={(e: React.MouseEvent<HTMLSpanElement>) => { e.stopPropagation(); setSelectedFiles((prevState) => { return prevState.map((current) => { if (current.file.name === file.file.name) { return { ...current, isDeleted: true }; } return current; }); }); }} > Delete </Button> )} </ListItem> ); }); export default UploadFile;
父组件示例(如App.tsx)
import { useState, useEffect, useRef } from "react"; import UploadFile, { FileType } from "./Upload"; export default function App() { const [selectedFiles, setSelectedFiles] = useState<FileType[]>([]); const fileRefs = useRef<{[key: string]: { startUpload: () => void }}>({}); // 监听文件列表变化,启动下一个上传 useEffect(() => { // 找到第一个待上传的文件:未完成、未取消、未删除 const nextFile = selectedFiles.find( f => f.uploadStatus < 100 && !f.uploadAbort && !f.isDeleted ); if (nextFile && fileRefs.current[nextFile.file.name]) { fileRefs.current[nextFile.file.name].startUpload(); } }, [selectedFiles]); // 处理文件选择 const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const files = e.target.files; if (!files) return; const newFiles: FileType[] = Array.from(files).map(file => ({ file, isDeleted: false, uploadStatus: 0, uploadError: "", uploadAbort: false })); setSelectedFiles(prev => [...prev, ...newFiles]); }; return ( <div className="App"> <input type="file" multiple onChange={handleFileChange} /> {selectedFiles.map(file => ( <UploadFile key={file.file.name} file={file} selectedFiles={selectedFiles} setSelectedFiles={setSelectedFiles} onUploadComplete={() => { // 上传完成后触发父组件重新检查队列 setSelectedFiles([...selectedFiles]); }} ref={el => { if (el) fileRefs.current[file.file.name] = el; }} /> ))} </div> ); }
关键逻辑说明
- 子组件通过
forwardRef和useImperativeHandle暴露startUpload方法,让父组件控制上传时机 - 父组件通过
useEffect监听文件列表状态,每次状态变化时检查是否有可上传的文件,启动第一个符合条件的文件 - 当文件上传完成或被取消时,通过
onUploadComplete回调通知父组件,触发下一轮的队列检查
内容的提问来源于stack exchange,提问作者user16860065
相关产品推荐
相关产品推荐

