Angular中HttpEventType.UploadProgress仅触发一次,上传进度直接显示100%的问题求助
解决视频上传进度直接跳100%的问题
嘿,我之前也碰到过一模一样的问题!核心原因是你当前的架构里,前端只能跟踪自己到后端的上传进度,而文件从后端传到谷歌云存储(GCS)的过程,前端完全感知不到。如果文件不大,前端到后端的上传会瞬间完成,所以event.loaded和event.total一开始就相等,进度直接显示100%,但此时后端可能还在往GCS传文件呢。
下面给你两种可行的解决方案,你可以根据自己的需求选择:
方案一:让后端把GCS上传进度实时推送给前端(后端中转模式)
这种方案保留你当前的后端中转架构,修改后端代码,把GCS的上传进度通过Server-Sent Events(SSE)推送给前端,前端再更新进度条。
第一步:修改Express后端代码
假设你用multer处理文件上传,修改你的/post/upload路由:
const { Storage } = require('@google-cloud/storage'); const multer = require('multer'); const storage = new Storage(); const bucket = storage.bucket('你的GCS桶名'); // 用内存存储文件流,方便跟踪进度 const multerStorage = multer.memoryStorage(); const upload = multer({ storage: multerStorage }); app.post('/post/upload', upload.single('videoUrl'), (req, res) => { if (!req.file) { return res.status(400).json({ error: '未上传视频文件' }); } // 设置SSE响应头,允许实时推送 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const totalBytes = req.file.size; let uploadedBytes = 0; const gcsFile = bucket.file(req.file.originalname); const writeStream = gcsFile.createWriteStream({ metadata: { contentType: req.file.mimetype } }); // 监听文件流的data事件,计算并推送进度 req.file.stream.on('data', (chunk) => { uploadedBytes += chunk.length; const progress = Math.round((uploadedBytes / totalBytes) * 100); // 向前端发送进度数据 res.write(`data: ${JSON.stringify({ progress, total: totalBytes, loaded: uploadedBytes })}\n\n`); }); // 把文件流pipe到GCS req.file.stream.pipe(writeStream); // 上传完成时推送最终状态 writeStream.on('finish', () => { const videoUrl = `https://storage.googleapis.com/${bucket.name}/${gcsFile.name}`; res.write(`data: ${JSON.stringify({ progress: 100, success: true, body: { videoUrl, ...req.body } })}\n\n`); res.end(); }); // 处理上传错误 writeStream.on('error', (err) => { res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); res.status(500).end(); }); });
第二步:修改前端Angular代码
更新PostsService的方法,改用SSE监听进度:
createPostWithVideoUpload(data: FormData): Observable<any> { return new Observable((observer) => { // 创建SSE连接 const eventSource = new EventSource(`${this.apiUrl}/post/upload`, { withCredentials: true // 如果你的后端需要认证,请开启 }); // 接收后端推送的进度数据 eventSource.onmessage = (event) => { const payload = JSON.parse(event.data); if (payload.progress !== undefined) { // 模拟HttpClient的UploadProgress事件格式 observer.next({ type: HttpEventType.UploadProgress, loaded: payload.loaded, total: payload.total }); } if (payload.success) { // 上传完成,发送Response事件 observer.next({ type: HttpEventType.Response, body: payload.body }); eventSource.close(); observer.complete(); } if (payload.error) { observer.error(payload.error); eventSource.close(); } }; // 处理SSE错误 eventSource.onerror = (error) => { observer.error(error); eventSource.close(); }; // 发送FormData请求 const xhr = new XMLHttpRequest(); xhr.open('POST', `${this.apiUrl}/post/upload`); xhr.send(data); }); }
你的createPost组件方法不需要修改,因为它已经在监听HttpEventType.UploadProgress事件了。
方案二:前端直接上传到GCS(推荐)
这种方案跳过后端中转,前端用后端生成的签名URL直接上传到GCS,进度跟踪更准确,还能减轻后端压力。
第一步:后端添加生成签名URL的接口
app.get('/post/upload/signature', async (req, res) => { const { fileName, contentType } = req.query; if (!fileName || !contentType) { return res.status(400).json({ error: '缺少文件名或Content-Type参数' }); } const gcsFile = bucket.file(fileName); // 生成有效期为1小时的签名URL const [signedUrl] = await gcsFile.getSignedUrl({ action: 'write', expires: Date.now() + 3600 * 1000, contentType: contentType }); res.json({ url: signedUrl }); }); // 添加创建帖子的接口(只保存元数据) app.post('/post', async (req, res) => { // 这里写保存帖子信息到数据库的逻辑 const post = await yourPostModel.create(req.body); res.json(post); });
第二步:修改前端代码
更新PostsService添加新方法:
// 获取签名URL getUploadSignature(fileName: string, contentType: string): Observable<any> { return this.http.get<any>(`${this.apiUrl}/post/upload/signature`, { params: { fileName, contentType } }); } // 创建帖子(保存元数据) createPost(postData: any): Observable<any> { return this.http.post<any>(`${this.apiUrl}/post`, postData); }
修改组件的createPost方法:
createPost(formDirective: FormGroupDirective): void { if (this.postForm.valid && this.user?.fullName && this.user?.imageUrl && this.fileToUpload) { const val = this.postForm.value; this.isLoading = true; // 第一步:请求签名URL this.postsService.getUploadSignature(this.fileToUpload.name, this.fileToUpload.type) .subscribe((signature) => { // 第二步:直接上传到GCS const xhr = new XMLHttpRequest(); xhr.open('PUT', signature.url); xhr.setRequestHeader('Content-Type', this.fileToUpload.type); // 跟踪上传进度 xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { this.progress = Math.round((event.loaded / event.total) * 100); console.log(`Uploaded! ${this.progress}%`); } }); // 上传完成后创建帖子 xhr.addEventListener('load', () => { if (xhr.status === 200) { const videoUrl = `https://storage.googleapis.com/你的GCS桶名/${this.fileToUpload.name}`; const postData = { videoUrl, fullName: this.user.fullName, imageUrl: this.user.imageUrl, title: val.title }; this.postsService.createPost(postData) .subscribe(() => { console.log('帖子创建成功!'); formDirective.resetForm(); this.isLoading = false; this.fileToUpload = null; setTimeout(() => { this.progress = 0; }, 1500); }); } }); xhr.send(this.fileToUpload); }); } }
这种方案的进度是真实的前端到GCS的上传进度,不会出现跳100%的问题,而且后端不需要处理大文件,性能更好。
内容的提问来源于stack exchange,提问作者Reem Obeid
相关产品推荐
相关产品推荐

