
本文详解在 WebSocket 多文件并发上传场景下,如何避免 socket.on('upload-progress') 监听器因闭包捕获失效导致的进度/ID 错位问题,通过事件命名隔离、唯一标识传递和监听器生命周期管理三大策略实现精准索引绑定。
本文详解在 websocket 多文件并发上传场景下,如何避免 `socket.on('upload-progress')` 监听器因闭包捕获失效导致的进度/id 错位问题,通过事件命名隔离、唯一标识传递和监听器生命周期管理三大策略实现精准索引绑定。
在基于 Socket.IO 的多视频并发上传中,一个常见却极易被忽视的问题是:当多个 handleUploadLectureVideo 调用并行执行时,其内部注册的 socket.on('upload-progress') 和 socket.on('upload-success') 监听器会共享同一作用域,而 JavaScript 闭包中的 index 变量在异步回调触发时往往已变为循环末尾值(如 files.length - 1),导致所有进度更新都错误地写入最后一个视频项——这正是你观察到“进度乱跳、ID错配”的根本原因。
✅ 正确方案:为每个上传会话创建独立事件通道
最直接、可靠且符合 Socket.IO 设计哲学的解法是避免复用全局事件名,转而为每次上传动态生成唯一事件标识。修改前端逻辑如下:
async function handleUploadLectureVideo({ files }: any, index: number) {
const file = files[0];
const formData = new FormData();
formData.append("file", file);
formData.append("name", file.name);
// ✅ 为本次上传生成唯一事件前缀(推荐使用 UUID 或时间戳+索引)
const uploadId = `upload_${Date.now()}_${index}`;
socket.emit("upload-file", {
file,
fileMeta: { originalName: file.name },
name: file.name,
uploadId, // ? 关键:将唯一 ID 透传至后端
});
// ✅ 使用带 uploadId 的专属事件监听进度
const progressHandler = (data: number) => {
setValue(`courseLectures.${index}.uploadingProgress`, data);
};
socket.on(`upload-progress-${uploadId}`, progressHandler);
// ✅ 同理监听专属 success 事件
const successHandler = (data: any) => {
setValue(`courseLectures.${index}.lectureVideo`, data.id);
// ? 重要:上传完成后立即移除监听器,防止内存泄漏
socket.off(`upload-progress-${uploadId}`, progressHandler);
socket.off(`upload-success-${uploadId}`, successHandler);
};
socket.on(`upload-success-${uploadId}`, successHandler);
}? 后端同步改造(NestJS)
需确保后端在 emit 进度/成功事件时,使用与前端一致的 uploadId 构建事件名:
async handleFileUpload(
@MessageBody() data: any,
@ConnectedSocket() client: Socket
) {
const { file, fileMeta, name, uploadId } = data; // ? 解构 uploadId
const filename = this.getCompleteFileName(fileMeta);
const completePath = join('video-lectures', filename);
await promises.writeFile(completePath, file);
this.vimeoClient.upload(
completePath,
{ name },
async (uri: string) => {
const video = new Video();
video.url = uri;
const result = await this.videoRepository.save(video);
// ✅ 发送带 uploadId 的专属成功事件
client.emit(`upload-success-${uploadId}`, result);
},
(bytesUploaded: number, bytesTotal: number) => {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
// ✅ 发送带 uploadId 的专属进度事件
client.emit(`upload-progress-${uploadId}`, percentage);
},
(error) => {
console.error('Upload failed:', error);
client.emit(`upload-failed-${uploadId}`, error);
}
);
}⚠️ 关键注意事项
- 禁止在循环/多次调用中重复注册同名全局事件:socket.on('upload-progress') 是单例监听器,多次调用会叠加 handler,造成 N 次重复更新。
- 务必手动清理监听器:使用 socket.off(eventName, handler) 显式注销,否则上传完成后的 handler 仍驻留内存,后续任意上传的同名事件都会触发它(严重 Bug)。
-
uploadId 必须全局唯一:若使用 index 作为 ID,需确保同一页面生命周期内 index 不重复(如列表重排、动态增删)。更健壮的做法是生成 UUID:
import { v4 as uuidv4 } from 'uuid'; const uploadId = `upload_${uuidv4()}`; - 服务端事件名拼接需严格一致:前后端 uploadId 生成逻辑、拼接格式(如是否含 -、大小写)必须完全一致,建议提取为常量或工具函数。
✅ 替代方案(进阶):统一监听 + payload 匹配
若因架构限制无法修改事件名,可改用单次注册 + payload 标识匹配模式(需后端在事件 payload 中携带 uploadId):
// ✅ 全局仅注册一次(如组件挂载时)
useEffect(() => {
const onProgress = (payload: { uploadId: string; progress: number }) => {
const index = findIndexByUploadId(payload.uploadId); // 业务逻辑:根据 uploadId 查找对应 index
setValue(`courseLectures.${index}.uploadingProgress`, payload.progress);
};
socket.on('upload-progress', onProgress);
return () => socket.off('upload-progress', onProgress); // 清理
}, []);
// 前端 emit 时传 uploadId
socket.emit("upload-file", { ..., uploadId });但此方案要求你维护 uploadId ↔ index 映射表,复杂度高于事件隔离法,仅作备选。
通过上述改造,每个视频上传拥有了独立的通信信道,彻底规避了闭包陷阱与监听器污染,确保进度条实时、准确地反映对应视频的上传状态——这才是高可靠性实时文件上传系统的基石。










