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
相关产品推荐
相关产品推荐

