返回博客首页
← 所有文章

在 NativeScript-Angular 应用中显示加载指示器的技巧

2019年8月8日 — 作者:Alexander Vakrilov

在您的应用等待从后端获取一些数据时,始终向用户显示一个视觉指示器是一个好主意。这通常不是一项难事 - 您只需在开始获取数据时显示加载指示器,并在数据到达时隐藏它。但是,在应用中的每个页面(或服务)中实际执行此操作可能是一项繁琐的任务。

幸运的是,Angular 为我们提供了工具,可以使用 HttpInterceptors 一劳永逸地解决整个应用中的此问题。

xhr-interceptor

HttpInterceptor

通过实现 HttpInterceptor,我们将进入 HttpClient 管道并控制 http 请求和响应。您可以将其用于各种用途 - 例如添加标头、缓存或在错误时重定向。在我们的例子中,我们只需要知道是否有请求正在等待解决。

我们还将创建一个单独的 HttpLoaderService 来跟踪活动请求的数量,以便我们知道是否应该显示或隐藏加载指示器。以下是这两个服务的代码

http-loader.service.ts

@Injectable({
    providedIn: "root"
})
export class HttpLoaderService {
    activeRequests$: BehaviorSubject<number>;
    isLoading$: Observable<boolean>;

    constructor() {
        this.activeRequests$ = new BehaviorSubject(0);
        this.isLoading$ = this.activeRequests$.pipe(
            map(requests => requests > 0)
        );
    }

    public onRequestStart() {
        setTimeout(() => this.activeRequests$.next(this.activeRequests$.value + 1), 10);
    }

    public onRequestEnd() {
        setTimeout(() => this.activeRequests$.next(this.activeRequests$.value - 1), 10);
    }
}

http-interceptor.service.ts

import { Injectable } from "@angular/core";
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { HttpLoaderService } from "./http-loader.service"

@Injectable()
export class HttpInterceptorService implements HttpInterceptor {
    constructor(private httpLoaderService: HttpLoaderService) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        this.onStart(req.url);

        return next.handle(req).pipe(
            tap((event: HttpEvent<any>) => {
                if (event instanceof HttpResponse) {
                    this.onEnd(event.url);
                }
            }, (err: any) => {
                this.onEnd(req.url);
            }));
    }

    private onStart(url: string) {
        this.httpLoaderService.onRequestStart();
    }

    private onEnd(url: string): void {
        this.httpLoaderService.onRequestEnd();
    }
}

注册

我们应该在根模块中注册我们的拦截器。不幸的是,我们不能简单地通过 providedIn: "root" 来做到这一点,因为 http-拦截器是通过使用 HTTP_INTERCEPTORS 令牌的多提供程序注册的。这样做的原因是您可能有多个拦截器到位。注册如下所示

import { HTTP_INTERCEPTORS } from "@angular/common/http";

@NgModule({
    // ...
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: HttpInterceptorService,
            multi: true
        }]
})
export class AppModule { }

显示加载指示器

放置指示器的最佳位置是 NativeScript 应用的根组件 - 这将确保它对所有页面都处于活动状态

app.component.html

<GridLayout>
    <page-router-outlet></page-router-outlet>

    <!-- Busy indicator with an overlay -->
    <GridLayout *ngIf="loaderService.isLoading$ | async" backgroundColor="#33252525" (tap)="true">
        <ActivityIndicator width="100" height="100" busy="true" class="activity-indicator"></ActivityIndicator>
    </GridLayout>
</GridLayout>

请注意奇怪的 (tap)="true" 代码。这是可选的 - 仅当您希望在指示器显示时阻止用户与应用交互时。

使用 HttpClient

从现在开始,我们无需执行任何特殊操作。只需在任何服务中使用 HttpClient 即可通知拦截器和加载服务,它们将显示加载指示器。

data-service.ts

export interface DataItem {
    title: string;
    body: string;
}

const URL = "https://jsonplaceholder.typicode.com/posts";

@Injectable({
    providedIn: "root"
})
export class DataService {
    constructor(private http: HttpClient) { }

    public getItems(): Observable<DataItem[]> {
        return this.http.get<DataItem[]>(URL);
    }
}

非 HttpClient 请求

注意!仅在使用 HttpClient 时才会调用 HttpInterceptors。如果您直接使用 fetchxhr,则会绕过它。此外,如果您正在使用库或 SDK(通过您的后端)可能不使用 HttpClient,则您需要向 HttpLoaderService 添加一些调用,以便为库发出的 http 调用显示加载指示器。

使用路由解析器预取数据

wasn't sure which stick you threw - so i got them all

Angular 的另一个很酷的功能是 路由解析器。它们允许您在实际导航到需要数据的页面**之前**预取一些数据。它可以避免您显示空白或部分呈现的无数据页面。

实现解析器非常简单。

data-resolver.service.ts

@Injectable({
    providedIn: "root"
})
export class DataResolverService implements Resolve<DataItem[]> {
    constructor(private dataService: DataService) {
    }

    public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DataItem[]> {
        return this.dataService.getItems();
    }
}

不要忘记将解析器添加到您的 route-config

{ path: "items", component: ItemsComponent, resolve: { items: DataResolverService }

它也与 http-拦截器方法很好地配合使用。结果是在当前页面上看到加载指示器,并且当数据加载完成后,会触发导航并以其所有荣耀显示新页面,因为所有数据都已存在。作为奖励,您还可以拥有一个加载覆盖层,以防止用户在加载数据时点击其他按钮发出并发请求。

总结

我们学到的内容的快速总结

  • 如何实现 http-拦截器来跟踪活动 http 请求
  • 显示应用程序范围的加载指示器
  • 使用路由解析器预取数据

奖励:在此示例中几乎没有特定于 NativeScript 的代码(实际上只有 AppComponent 中的标记)。您可以在您的 Web 或 代码共享应用 中与 NativeScript 一起重用所有学到的知识!

这是一个 NativeScript Playground 项目,您可以使用它来查看所有这些内容!