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

Angular中如何让Observable Subject返回同步数据解决图片上传resize报错

问题根因
  • resizeService逻辑错误:不需要压缩的图片会直接通过resizedFileSubject.next()发射单个File对象,后续处理完所有图片又会发射一次整个数组,结合组件里的take(1)只会取第一个发射的值,若第一张图刚好不需要压缩,订阅拿到的data就是单个File对象而非数组,循环赋值时data[i]自然为undefined。
  • 异步计数逻辑错误:image.onload是异步回调,不同图片加载速度不同,用i + 1 === length判断处理结束不可靠,加载速度快的大序号图片可能先触发回调,此时数组内还没放入前面的图片处理结果,导致最终发射的数组长度不足。
  • 不需要压缩的图片没有推入结果数组,即便走到最终发射数组的逻辑,这部分图片也会缺失。
  • 缩放尺寸计算逻辑写反,会导致压缩后的图片尺寸超出设置的最大限制。
修复方案

1. 重构resizeService逻辑

直接使用rxjs自带的forkJoin处理多个异步任务,不需要手动维护计数和Subject,天然保证所有任务完成后按输入顺序返回结果数组:

import { Injectable } from '@angular/core';
import { Observable, forkJoin } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class resizeService {

  constructor() { }

  resizeImage(files: FileList, maxWidth: number, maxHeight: number): Observable<Blob[]> {
    // 每个图片的压缩处理封装为独立Observable
    const resizeTasks = Array.from(files).map(file => {
      return new Observable<Blob>(subscriber => {
        const image: HTMLImageElement = new Image();
        image.src = URL.createObjectURL(file);

        image.onload = () => {
          let width = image.width;
          let height = image.height;
          let resultBlob: Blob;

          // 不需要压缩直接返回原文件
          if (width <= maxWidth && height <= maxHeight) {
            resultBlob = file;
          } else {
            // 修复等比缩放计算逻辑
            const ratio = width / height;
            let newWidth, newHeight;
            if (width > height) {
              newWidth = maxWidth;
              newHeight = maxWidth / ratio;
            } else {
              newHeight = maxHeight;
              newWidth = maxHeight * ratio;
            }

            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext("2d");
            canvas.width = newWidth;
            canvas.height = newHeight;
            ctx.drawImage(image, 0, 0, newWidth, newHeight);
            resultBlob = this.b64ToBlob(canvas.toDataURL("image/jpeg"));
          }

          // 释放临时资源避免内存泄漏
          URL.revokeObjectURL(image.src);
          subscriber.next(resultBlob);
          subscriber.complete();
        };

        image.onerror = (err) => {
          URL.revokeObjectURL(image.src);
          subscriber.error(err);
        };
      });
    });

    // 等待所有异步任务完成后按输入顺序返回结果数组
    return forkJoin(resizeTasks);
  }

  /**
  * Convert BASE64 to BLOB
  * @param base64Image Pass Base64 image data to convert into the BLOB
  */
  private b64ToBlob(base64Image: string) {
    const parts = base64Image.split(';base64,');
    const imageType = parts[0].split(':')[1];
    const decodedData = window.atob(parts[1]);
    const uInt8Array = new Uint8Array(decodedData.length);

    for (let i = 0; i < decodedData.length; ++i) {
      uInt8Array[i] = decodedData.charCodeAt(i);
    }

    return new Blob([uInt8Array], { type: imageType });
  }
}

2. 组件代码可直接复用

现有组件的onFileSelected逻辑不需要修改,返回的结果数组长度和输入文件长度完全一致,不会出现缺项、顺序错乱问题,原有的setTimeout workaround可以完全删除。

额外优化建议

可以直接在resizeService里把返回的Blob包装为File对象,不需要订阅后再循环赋值name属性,更符合规范:

// 把subscriber.next(resultBlob)替换为以下代码
subscriber.next(new File([resultBlob], file.name, { type: resultBlob.type }));

内容的提问来源于stack exchange,提问作者Maurício

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 15:36:02