Angular 中的加载指示

2025-06-04

Angular 中的加载指示

最初于 2019 年 1 月 30 日在nils-mehlhorn.de发布

一个常见的愿望是:让某个东西旋转或飞来飞去,给用户带来乐趣,同时性能一般的后端却不知从哪里抓取数据。虽然从 CodePen 借用一个旋转器并在服务器往返时显示它似乎很容易,但我们会澄清一些常见的误解和陷阱。

等待数据

让我们从一个常规任务开始:我们希望显示一个通过服务异步获取的用户列表。一个虽然不太成熟但技术上可行的解决方案如下:

export class UserComponent implements OnInit  {

  users: User[]
  loading = false

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.loading = true
    this.userService.getAll().pipe(
      finalize(() => this.loading = false)
    ).subscribe(users => this.users = users)
  }
}
Enter fullscreen mode Exit fullscreen mode

一个实例变量用于保存用户,另一个用于设置标志,指​​示用户是仍在加载还是已经到达。在订阅(从而触发异步调用)之前,加载标志会被更新。调用完成后,使用操作符重置该标志finalize。传递给该操作符的回调函数将在可观察对象调用完成后调用,无论其结果如何。如果这些操作只是在订阅回调中执行,则加载标志只会在调用成功后重置,而不会在发生错误时重置。相应的视图可能如下所示:

<ul *ngIf="!loading">
  <li *ngFor="let user of users">
    {{ user.name }}
  </li>
</ul>
<loading-indicator *ngIf="loading"></loading-indicator>
Enter fullscreen mode Exit fullscreen mode

然而,对于大多数需要直接在视图中显示的数据的调用,可以使用AsyncPipe简化此设置。我们的组件将缩短为以下内容:

export class UserComponent implements OnInit  {

  users$: Observable<User[]>

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    this.users$ = this.userService.getAll()
  }
}
Enter fullscreen mode Exit fullscreen mode

现在组件直接将用户流暴露给视图。我们将使用async as以下语法更新视图,以便在流发出后将
其值绑定到一个单独的users变量:

<ul *ngIf="users$ | async as users; else indicator">
  <li *ngFor="let user of users">
    {{ user.name }}
  </li>
</ul>
<ng-template #indicator>
  <loading-indicator></loading-indicator>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

else通过为代码块提供视图模板,*ngIf我们无需再显式地管理加载标志。这种方法更具声明性,因为它通过 if-else 连接连接两个视图状态,而不是使用两个单独的 if 代码块。此外,我们无需再自行管理流的订阅,因为这已由管道完成(包括在组件销毁时取消订阅)。

等待行动

当我们处理诸如点击按钮创建新用户之类的操作时,AsyncPipe 会让我们失望。当你无法将可观察对象通过管道传回视图时,你就必须在组件内部进行订阅。

首先,虽然有些人可能不同意,但我认为这次使用标记方法是合理的。不要听信那些谴责哪怕一丁点冗余的假先知。很多时候,代码应该易于理解、测试和删除,而不是最终以尽可能少的行数收场。所以,这样做基本上没问题:

<button (click)="create()">Create User</button>
<div *ngIf="loading">
  Creating, please wait <loading-indicator></loading-indicator>
</div>
Enter fullscreen mode Exit fullscreen mode
export class UserComponent  {

  loading: boolean

  constructor(private userService: UserService) {}

  create(name = "John Doe"): void {
    this.loading = true
    this.userService.create(new User(name)).pipe(
      finalize(() => this.loading = false)
    ).subscribe()
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,让我们看看如果您坚决反对这两行在每个组件中明确切换加载标志,我们可以做些什么。

拦截器方法

我见过有人建议使用HttpInterceptor来监听是否有任何调用正在被处理。这样的拦截器可以像这样:

@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
  constructor(private loadingService: LoadingService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // you could also check for certain http methods here
    this.loadingService.attach(req);
    return next.handle(req).pipe(
      finalize(() => this.loadingService.detach(req)),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

通过此实现,当请求通过 开始时,会通知一个单独的 LoadingService attach(req)。请求结束后(无论结果如何),该服务都会再次通过 收到通知detach(req)。该服务可以在每次调用时检查是否仍有未完成的请求,从而管理全局的加载标志。

我自己也使用过这种方法——甚至在 AngularJS 中。
虽然我认为这是一种向用户提供应用程序加载时间的通用指示的得体​​方法,但您实际上
必须考虑三件事:

1)您失去了特异性。由于您没有每个请求都有一个加载标志,而是有一个全局标志,所以您无法确定哪个请求仍在花费时间。虽然您可以将服务注入到任何组件中,并以此方式显示本地指示——但您真正拥有的信息是在全局应用程序级别。将其用作单个请求的指示器在语义上是错误的。2

用户要使用全局加载指示做什么?只要您的应用程序中仍有任何内容正在加载,您是否要禁用所有内容?用户是否应该等到全局指示完成?如果与用户当前任务无关的请求卡住了怎么办? 3)你们的耦合很奇怪。您已经尽力将 HTTP 调用隐藏在服务后面,并仔细地将其与视图逻辑分离,这样您现在就可以在后台进行操作了。我不会完全谴责这种做法,只是一些值得思考的事情。

知道你想要什么

如果您对拦截器方法的权衡感到满意,则可以将其用于全局指示,例如从应用程序窗口左上角到右上角的进度条 - 如下图所示。

全球负荷指示

全球负荷指示

一些主流应用程序都是这样操作的,看起来有点花哨,并且已经有一些教程和不错的库可以完全处理这种行为。

尽管如此,如果我们想告诉用户什么才是真正浪费他的时间,或者甚至同时禁用某些交互(再次参见下面的说明),我们就必须想出其他办法。

上下文加载指示

上下文加载指示

了解其中的区别至关重要。否则,你可能会发现你的表单操作(例如图中左下角)仍在加载,但实际上来自页面其他地方的调用(例如图中最右侧的小部件)才是真正的罪魁祸首。

不要被简单所迷惑,而是要知道你想要什么,然后我们才能弄清楚如何构建它。

反应式语境方法

如果您想要特定的、上下文相关的加载指示,而又不想显式地切换加载状态,可以使用 RxJS 操作符来实现。从 RxJS 6 开始,您可以以纯函数的形式定义自己的操作符。首先,我们将定义一个在订阅时调用回调的操作符。这可以使用 RxJS 的defer方法来实现:

export function prepare<T>(callback: () => void): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => defer(() => {
    callback();
    return source;
  });
}
Enter fullscreen mode Exit fullscreen mode

现在我们创建另一个操作符,接受一个 subject 作为加载状态的接收器。使用新创建的prepare操作符,我们将在通过 订阅实际源流时更新此 subject indicator.next(true)。同样,我们使用该finalize操作符通过 通知它加载已完成indicator.next(false)

export function indicate<T>(indicator: Subject<boolean>): (source: Observable<T>) => Observable<T> {
  return (source: Observable<T>): Observable<T> => source.pipe(
    prepare(() => indicator.next(true)),
    finalize(() => indicator.next(false))
  )
}
Enter fullscreen mode Exit fullscreen mode

indicate然后我们可以在组件中使用新运算符,如下所示:

export class UserComponent  {
  loading$ = new Subject<boolean>()

  constructor(private userService: UserService) {}

  create(name = "John Doe"): void {
    this.userService.create(new User(name))
    .pipe(indicate(this.loading$))
    .subscribe()
  }
}
Enter fullscreen mode Exit fullscreen mode
<button (click)="create()">Create User</button>
<div *ngIf="loading$ | async">
  Creating, please wait <loading-indicator></loading-indicator>
</div>
Enter fullscreen mode Exit fullscreen mode

我已将这些代码片段整理成 StackBlitz 上的完整示例。刷新页面即可查看第一个指示符,点击“创建用户”即可查看第二个指示符。另外,不要被 StackBlitz 上的蓝色指示符分散注意力,我们的是红色的。

文章来源:https://dev.to/angular/loading-induction-in-angular-52b6
PREV
使用新的 Angular Clipboard CDK 与剪贴板进行交互
NEXT
如何在不到10分钟的时间内构建史诗级的Angular应用!⏱️