Angular拦截器中如何先执行文件上传API再发起主请求?
问题描述
我的应用包含多个表单,表单生成的JSON对象结构多样,存在不同层级的嵌套对象与数组;这些表单支持用户上传文件,对象内会存储包含base64字符串、文件名等信息的数组。
目前的流程是:将文件转为base64字符串后,在发送包含文件的主POST请求前,先调用文件上传API,等待上传完成并获取响应后,替换请求体中的base64文件对象,再发送主请求。但当前遇到两个问题:
- 文件上传请求与主请求并行执行,请求体未被正确修改
- 文件上传请求会无原因地多次执行
相关代码如下:
export class FilesCheckerInterceptor implements HttpInterceptor { constructor(private filesService: FilesService) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const data = request.body; if (data) { const uploaded: File[] = []; this.traverse(data, files => { files.forEach(file => { const base64 = file.data; const result = this.filesService.toFile(base64, file.name); uploaded.push(result); }); }); console.log(request); return this.filesService.uploadFile(uploaded).pipe( mergeMap(response => { this.traverse(data, files => { for (let i = 0; i < response.length; i++) { files[i] = response[i]; } }); return next.handle(request.clone({ body: data })); }), ); } else { return next.handle(request); } } traverse(obj: any, action: (value: InternalFile[]) => void) { if (obj !== null && typeof obj === 'object') { Object.entries(obj).forEach(([key, value]) => { if (key === 'attachments' && Array.isArray(value)) { action(value); } else { this.traverse(value, action); } }) } } }
问题原因与修复方案
问题原因
- 文件替换逻辑错误:当前代码会遍历所有
attachments数组并收集所有文件,但替换时会把每个attachments数组都覆盖成同一个上传响应结果,导致多组文件的场景下替换完全错误,同时如果上传响应长度与文件总数不匹配,还会出现越界问题。 - 多次触发上传:Angular HTTP拦截器返回的Observable如果被多次订阅(比如请求重试、组件重复订阅),会重复执行文件上传逻辑,导致多次上传。
修复代码
import { shareReplay } from 'rxjs/operators'; export class FilesCheckerInterceptor implements HttpInterceptor { constructor(private filesService: FilesService) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const data = request.body; if (data) { // 收集所有需要替换的文件位置,以及对应的File对象 const fileReplacements: { array: InternalFile[], index: number, file: File }[] = []; this.traverse(data, (files) => { files.forEach((file, index) => { const base64 = file.data; const result = this.filesService.toFile(base64, file.name); fileReplacements.push({ array: files, index, file: result }); }); }); // 没有文件需要上传,直接走主请求 if (fileReplacements.length === 0) { return next.handle(request); } // 提取待上传的File集合 const uploadedFiles = fileReplacements.map(item => item.file); return this.filesService.uploadFile(uploadedFiles).pipe( mergeMap((responses) => { // 按顺序替换每个原始文件的位置 fileReplacements.forEach((item, index) => { item.array[item.index] = responses[index]; }); // 深拷贝请求体,避免修改原始引用带来的副作用 const modifiedBody = structuredClone(data); // 克隆请求并传入修改后的body const modifiedRequest = request.clone({ body: modifiedBody }); return next.handle(modifiedRequest); }), // 保证Observable只被执行一次,防止多次上传 shareReplay(1) ); } else { return next.handle(request); } } traverse(obj: any, action: (value: InternalFile[]) => void) { if (obj !== null && typeof obj === 'object') { Object.entries(obj).forEach(([key, value]) => { if (key === 'attachments' && Array.isArray(value)) { action(value); } else { this.traverse(value, action); } }); } } }
关键修复点说明
- 精准替换文件:通过
fileReplacements数组记录每个待上传文件所在的数组和索引,上传完成后按顺序精准替换,解决多组attachments数组的替换错误问题。 - 防止多次上传:添加
shareReplay(1)操作符,确保拦截器返回的Observable仅被执行一次,避免重复订阅导致的多次上传。 - 深拷贝请求体:使用
structuredClone深拷贝请求体,避免修改原始引用带来的意外副作用。 - 空文件判断:没有文件时直接跳过上传逻辑,减少无效请求。
内容的提问来源于stack exchange,提问作者user13749429
相关产品推荐
相关产品推荐

