NgRx中带错误处理的动作链实现方案咨询
解决方案:NgRx实现链式文件上传+错误重试流程
一、状态设计(State)
首先扩展状态结构,保存每个文件的上传状态、已获取的ID以及整体流程状态,支持单个文件重试而不重复上传已成功的文件:
export interface FileUploadState { formData: any; // 保存表单基础数据 files: { first: { status: 'idle' | 'loading' | 'success' | 'failed'; fileId?: string; error?: string; file?: File; }; second: { status: 'idle' | 'loading' | 'success' | 'failed'; fileId?: string; error?: string; file?: File; }; third: { status: 'idle' | 'loading' | 'success' | 'failed'; fileId?: string; error?: string; file?: File; }; }; overallStatus: 'idle' | 'uploading' | 'readyToCreate' | 'creating' | 'done' | 'error'; } // 初始状态 export const initialState: FileUploadState = { formData: null, files: { first: { status: 'idle' }, second: { status: 'idle' }, third: { status: 'idle' }, }, overallStatus: 'idle', };
二、动作定义(Actions)
调整动作体系,明确区分文件类型,支持重试动作以及最终创建对象的动作:
import { createAction, props } from '@ngrx/store'; // 表单提交,触发上传流程 export const formSubmit = createAction( '[Create Object] Form Submit', props<{ formData: any; firstFile: File; secondFile: File; thirdFile: File }>() ); // 上传单个文件(指定类型) export const uploadFile = createAction( '[Create Object] Upload File', props<{ fileType: 'first' | 'second' | 'third'; file: File }>() ); // 文件上传成功 export const uploadFileSuccess = createAction( '[Create Object] Upload File Success', props<{ fileType: 'first' | 'second' | 'third'; fileId: string }>() ); // 文件上传失败 export const uploadFileFailure = createAction( '[Create Object] Upload File Failure', props<{ fileType: 'first' | 'second' | 'third'; error: string }>() ); // 重试上传某个文件(携带新文件) export const retryUploadFile = createAction( '[Create Object] Retry Upload File', props<{ fileType: 'first' | 'second' | 'third'; newFile: File }>() ); // 所有文件上传完成,触发对象创建 export const createObject = createAction( '[Create Object] Create Final Object', props<{ formData: any; fileIds: { first: string; second: string; third: string } }>() ); // 对象创建成功/失败 export const createObjectSuccess = createAction('[Create Object] Create Success'); export const createObjectFailure = createAction('[Create Object] Create Failure', props<{ error: string }>());
三、Effects实现
拆分多个单一职责的Effect,遵循NgRx最佳实践:
1. 表单提交触发首个文件上传
import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { map } from 'rxjs/operators'; import * as CreateActions from './create-actions'; @Injectable() export class CreateEffects { constructor(private actions$: Actions) {} formSubmit$ = createEffect(() => this.actions$.pipe( ofType(CreateActions.formSubmit), map((action) => { return CreateActions.uploadFile({ fileType: 'first', file: action.firstFile, }); }) ) ); }
2. 处理文件上传请求
import { switchMap, catchError, map } from 'rxjs/operators'; import { of } from 'rxjs'; import { YourFileUploadService } from '../services/file-upload.service'; // 新增上传文件的Effect uploadFile$ = createEffect(() => this.actions$.pipe( ofType(CreateActions.uploadFile), switchMap((action) => this.fileUploadService.uploadFile(action.file).pipe( map((fileId) => CreateActions.uploadFileSuccess({ fileType: action.fileType, fileId, }) ), catchError((error) => of( CreateActions.uploadFileFailure({ fileType: action.fileType, error: error.message || '上传失败', }) ) ) ) ) ) );
3. 文件上传成功后触发下一个步骤
import { withLatestFrom } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { AppState } from '../app.state'; uploadFileSuccess$ = createEffect(() => this.actions$.pipe( ofType(CreateActions.uploadFileSuccess), withLatestFrom(this.store.select((state) => state.create)), // 获取当前状态 map(([action, state]) => { // 根据当前成功的文件类型,触发下一个文件上传或最终创建对象 switch (action.fileType) { case 'first': return CreateActions.uploadFile({ fileType: 'second', file: state.files.second.file!, }); case 'second': return CreateActions.uploadFile({ fileType: 'third', file: state.files.third.file!, }); case 'third': // 所有文件上传完成,收集ID触发对象创建 const fileIds = { first: state.files.first.fileId!, second: state.files.second.fileId!, third: state.files.third.fileId!, }; return CreateActions.createObject({ formData: state.formData, fileIds, }); default: throw new Error('未知文件类型'); } }) ) );
4. 处理重试上传
retryUploadFile$ = createEffect(() => this.actions$.pipe( ofType(CreateActions.retryUploadFile), map((action) => CreateActions.uploadFile({ fileType: action.fileType, file: action.newFile, }) ) ) );
5. 处理最终对象创建
import { YourObjectCreateService } from '../services/object-create.service'; createObject$ = createEffect(() => this.actions$.pipe( ofType(CreateActions.createObject), switchMap((action) => this.objectCreateService.createObject(action.formData, action.fileIds).pipe( map(() => CreateActions.createObjectSuccess()), catchError((error) => of( CreateActions.createObjectFailure({ error: error.message || '创建对象失败', }) ) ) ) ) ) );
四、Reducer实现
处理所有动作,更新状态:
import { createReducer, on } from '@ngrx/store'; import * as CreateActions from './create-actions'; import { initialState, FileUploadState } from './create.state'; export const createReducer = createReducer( initialState, // 表单提交:保存表单数据和文件 on(CreateActions.formSubmit, (state, action) => ({ ...state, formData: action.formData, files: { first: { ...state.files.first, file: action.firstFile, status: 'idle' }, second: { ...state.files.second, file: action.secondFile, status: 'idle' }, third: { ...state.files.third, file: action.thirdFile, status: 'idle' }, }, overallStatus: 'uploading', })), // 开始上传文件:设置对应文件状态为loading on(CreateActions.uploadFile, (state, action) => ({ ...state, files: { ...state.files, [action.fileType]: { ...state.files[action.fileType], status: 'loading', error: undefined, }, }, })), // 文件上传成功:保存ID,设置状态为success on(CreateActions.uploadFileSuccess, (state, action) => ({ ...state, files: { ...state.files, [action.fileType]: { ...state.files[action.fileType], status: 'success', fileId: action.fileId, }, }, })), // 文件上传失败:保存错误,设置状态为failed on(CreateActions.uploadFileFailure, (state, action) => ({ ...state, files: { ...state.files, [action.fileType]: { ...state.files[action.fileType], status: 'failed', error: action.error, }, }, overallStatus: 'error', })), // 重试上传:更新文件,重置状态为idle(等待uploadFile触发loading) on(CreateActions.retryUploadFile, (state, action) => ({ ...state, files: { ...state.files, [action.fileType]: { ...state.files[action.fileType], file: action.newFile, status: 'idle', error: undefined, }, }, overallStatus: 'uploading', })), // 对象创建成功/失败 on(CreateActions.createObjectSuccess, (state) => ({ ...state, overallStatus: 'done', })), on(CreateActions.createObjectFailure, (state, action) => ({ ...state, overallStatus: 'error', })) );
五、组件交互示例
在组件中监听状态,处理错误和重试:
import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import * as CreateActions from './store/create-actions'; import { FileUploadState } from './store/create.state'; @Component({ selector: 'app-create-object', templateUrl: './create-object.component.html', }) export class CreateObjectComponent { uploadState$: Observable<FileUploadState>; constructor(private store: Store<{ create: FileUploadState }>) { this.uploadState$ = this.store.select('create'); } onFormSubmit(formData: any, firstFile: File, secondFile: File, thirdFile: File) { this.store.dispatch(CreateActions.formSubmit({ formData, firstFile, secondFile, thirdFile })); } onRetryFileUpload(fileType: 'first' | 'second' | 'third', newFile: File) { this.store.dispatch(CreateActions.retryUploadFile({ fileType, newFile })); } }
模板中展示错误和重试按钮:
<!-- 第一个文件错误提示 --> <div *ngIf="(uploadState$ | async)?.files.first.status === 'failed'"> <p class="error">{{(uploadState$ | async)?.files.first.error}}</p> <input type="file" (change)="onRetryFileUpload('first', $event.target.files![0])"> <button>重新上传</button> </div> <!-- 同理处理second和third文件 -->
关键优势说明
- 单一职责Effect:每个Effect只处理一个动作,返回一个动作,符合NgRx最佳实践,避免无限循环问题。
- 状态持久化:保存每个文件的上传状态和已获取的ID,重试时仅上传失败的文件,无需重复上传已成功的文件。
- 流程可控:通过状态跟踪整体流程,支持中断后恢复,用户更换文件后可继续后续上传步骤。
- 错误隔离:单个文件失败不影响已成功的文件状态,用户可针对性处理错误。
内容的提问来源于stack exchange,提问作者dadelsvesken
相关产品推荐
相关产品推荐

