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

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文件 -->

关键优势说明

  1. 单一职责Effect:每个Effect只处理一个动作,返回一个动作,符合NgRx最佳实践,避免无限循环问题。
  2. 状态持久化:保存每个文件的上传状态和已获取的ID,重试时仅上传失败的文件,无需重复上传已成功的文件。
  3. 流程可控:通过状态跟踪整体流程,支持中断后恢复,用户更换文件后可继续后续上传步骤。
  4. 错误隔离:单个文件失败不影响已成功的文件状态,用户可针对性处理错误。

内容的提问来源于stack exchange,提问作者dadelsvesken

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 02:17:05