Angular 实践:如何优雅地发起和处理请求

Angular 实践:如何优雅地发起和处理请求
最新回答
水样年华

2023-11-08 19:54:23

在 Angular 中优雅地发起和处理请求,可通过自定义 rxAsync 指令结合 ObservableInput 装饰器实现,核心思路是利用响应式编程(RxJS)统一管理请求状态(loading、success、error)和触发逻辑(初始加载、重试、重新加载)。以下是具体实现方案:

一、核心设计思路
  1. 请求触发场景

    首次渲染:通过 startWith(null) 触发初始请求。

    用户主动重新加载:通过内部 reload() 方法或外部 Subject(如按钮点击)触发。

    错误自动重试:通过 retry(n) 操作符根据配置次数自动重试。

  2. 状态管理

    使用 context 对象动态绑定模板中的 loading、error、data 状态,避免手动控制 DOM。

    通过 finalize() 确保请求结束时(无论成功/失败)关闭 loading 状态。

  3. 参数变更处理

    使用 combineLatest 监听 fetcher、params 等输入变化,但忽略 retryTimes 变更(仅影响错误状态)。

    若参数变化时请求未完成,自动取消旧请求(通过 disposeSub())。

(流程图:覆盖初始加载、重试、重新加载的完整状态流转)二、指令实现代码@Directive({ selector: '[rxAsync]' })export class AsyncDirective<T, P, E = HttpErrorResponse> implements OnInit, OnDestroy { // 输入参数:上下文、请求函数、请求参数、外部重试触发器、重试次数 @ObservableInput() @Input('rxAsyncContext') private context$!: Observable<any>; @ObservableInput() @Input('rxAsyncFetcher') private fetcher$!: Observable<Callback<[P], Observable<T>>>; @ObservableInput() @Input('rxAsyncParams') private params$!: Observable<P>; @Input('rxAsyncRefetch') private refetch$$ = new Subject<void>(); @ObservableInput() @Input('rxAsyncRetryTimes') private retryTimes$!: Observable<number>; private destroy$$ = new Subject<void>(); private reload$$ = new Subject<void>(); private context = { reload: this.reload.bind(this) } as IAsyncDirectiveContext<T, E>; private viewRef: Nullable<ViewRef>; private sub: Nullable<Subscription>; constructor( private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef, ) {} reload() { this.reload$$.next(); } ngOnInit() { combineLatest([ this.context$, this.fetcher$, this.params$, this.refetch$$.pipe(startWith(null)), this.reload$$.pipe(startWith(null)), ]) .pipe( takeUntil(this.destroy$$), withLatestFrom(this.retryTimes$), // 仅获取最新重试次数 ) .subscribe(([[context, fetcher, params], retryTimes]) => { this.disposeSub(); // 取消旧请求 Object.assign(this.context, { loading: true, error: null }); // 重置状态 this.sub = fetcher.call(context, params) .pipe( retry(retryTimes), finalize(() => { this.context.loading = false; this.viewRef?.detectChanges(); }), ) .subscribe( data => (this.context.$implicit = data), error => (this.context.error = error), ); if (!this.viewRef) { this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef, this.context); } }); } ngOnDestroy() { this.disposeSub(); this.destroy$$.next(); this.destroy$$.complete(); this.viewRef?.destroy(); } disposeSub() { this.sub?.unsubscribe(); this.sub = null; }}三、使用示例@Component({ template: ` <button (click)="refetch$$.next()">外部重新加载</button> <div *rxAsync=" let todo; let loading = loading; let error = error; let reload = reload; context: context; fetcher: fetchTodo; params: todoId; refetch: refetch$$; retryTimes: retryTimes "> <button (click)="reload()">内部重新加载</button> 状态: {{ loading ? '加载中' : error ? '错误' : '成功' }} 数据: {{ todo | json }} </div> `, changeDetection: ChangeDetectionStrategy.OnPush,})class DemoComponent { context = this; @Input() todoId = 1; @Input() retryTimes = 0; refetch$$ = new Subject<void>(); constructor(private http: HttpClient) {} fetchTodo(id: number) { return this.http.get<Todo>(`
https://jsonplaceholder.typicode.com/todos/
${id}`); }}四、关键点解析
  1. ObservableInput 装饰器

    自动将 @Input() 转换为 Observable,简化对输入参数变化的监听(如 params$、fetcher$)。

    无需手动调用 async 管道或订阅多个 Observable。

  2. 请求取消机制

    每次参数变化时,通过 disposeSub() 取消未完成的请求,避免竞态条件。

    使用 Subject(如 reload$$、refetch$$)统一管理重新加载触发。

  3. 状态隔离

    context 对象将模板变量(如 loading、error)与指令逻辑解耦,提升可维护性。

    配合 OnPush 变更检测策略,仅在状态变化时更新视图。

五、适用场景
  • 数据表格分页/筛选:参数变化时自动重新加载数据。
  • 表单提交后刷新:通过 refetch$$ 触发外部数据更新。
  • 复杂状态管理:统一处理加载中、错误、空数据等 UI 状态。

通过此方案,可避免命令式编程中大量的 if-else 分支和手动状态管理,实现声明式的请求处理逻辑。