You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 19:57:38