Angular Http 相关知识

197 阅读19分钟

本文介绍 Angular HTTP 相关知识点。包括如下内容:

  1. Angular 中如何消耗 REST API.
  2. 使用 resolver 获取数据。
  3. 创建请求、响应拦截器。
  4. 在应用中缓存数据。
  5. 使用单元测试检测数据服务。

image.png image.png

1. Module 1

使用 HttpClientModule 的第一步就是在根模块中注入此模块。

image.png

这样做之后,你就可以在任何组件或者自定义服务中使用名为 HttpClient 的服务了。

image.png

image.png

2. Module 2

2.1 什么是 RESTFUL API

image.png

2.2 RESTFUL API 和 CRUD

如下所示,restful api 中的方法和 crud 可以对应起来,对应关系为:

  • create -- post
  • read -- get
  • update -- put
  • delete -- delete

image.png

2.3 Observable

由于使用 HttpClient 服务提供的方法时,各个方法的返回值都是 Observable 类型的,因此,必须了解如何处理 Observable 类型的数据。而 Observable 也是 Rxjs 或者 Angular 的重要组成部分。

下面是一个典型的示例。展示了如何在自定义服务中使用 HttpClient 示例发送网络请求并处理其返回值。

image.png

注意上面的类型约束,特别是 Observable 类型约束。

2.4 设置请求头

在发送网络请求的时候有的时候我们需要通过请求头传递给服务器额外的信息。例如令牌或者期望返回数据的格式等。

image.png

2.5 使用操作 Observable 对象的 operators

使用 operators 的优势在于:

  1. 操作一个 observable 数据并返回一个新的 observable 数据。
  2. 可以串联起来完成对数据的复杂转化。
  3. 具有极强的灵活性,可以将数据转换成你想要的任意格式。

image.png

下面的这个代码示例是展示 pipe 操作符将 map 和 tap 操作符联合起来使用。其中 map 操作符改变了网络请求返回值的数据结构。而 tap 操作符只是将改变之后的数据结构打印在控制台中,并没有对其进行修改。

image.png

2.6 CRUD 示例

image.png

image.png

3. Module 3 -- 处理错误及路由守卫

3.1 正确的处理网络请求错误

处理网络请求错误的一些原则:

  1. 将网络请求封装起来。
  2. 不要将实现细节溢出到具体的组件中去。
  3. 使用 RxJS 中的 catchError 操作符来捕获错误。
  4. 将处理之后的错误对象返回给调用者。

image.png

下面的例子展示了处理网络请求错误的标准流程:

image.png

image.png

在这个过程中,我们使用了 RxJS 提供的 pipe catchError throwError 这些 operators.

你可能会疑惑: catchError 虽然保证了错误不会外溢到 subscribe 中,但是在错误处理函数 handleHttpError 中又写了 throwError(() => dataError) ,错误难道不还是外溢到 subscribe 中去了吗?

getAllBooks(): Observable<Book[] | BookTrackerError> {
  console.log('Getting all books from the server.');
  return this.http.get<Book[]>('/api/books') // 假设API路径是正确的
    .pipe(
      catchError((err: HttpErrorResponse) => this.handleHttpError(err))
    );
}

private handleHttpError(error: HttpErrorResponse): Observable<BookTrackerError> {
  let dataError = new BookTrackerError();
  dataError.errorNumber = 10; // 假设这是错误编号
  dataError.message = error.statusText; // 使用错误的状态文本
  dataError.friendlyMessage = 'An error occurred retrieving data.'; // 用户友好的错误消息
  return throwError(() => dataError); // 返回一个包含错误的Observable
}

这是正确的。在catchError中使用throwError确实意味着错误最终会外溢到subscribeerror回调中。这样做的原因和意义包括:

  1. 错误传递:在RxJS中,使用throwError是将错误传递给订阅者的error回调的一种方式。这是符合Observable规范的做法,确保错误可以按照正常的流程被处理。

  2. 错误消费:即使在catchError中捕获了错误,通常也需要让订阅者知道发生了错误。通过throwError,订阅者可以在subscribeerror回调中消费这个错误。

  3. 控制流程:通过在catchError中使用throwError,开发者可以控制错误处理的流程。例如,可以在catchError中执行一些逻辑,然后重新抛出错误,让外部的subscribe处理。

  4. 链式调用:RxJS鼓励使用链式调用。在catchError中使用throwError允许开发者在Observable链中继续使用其他操作符,即使发生了错误。

  5. 错误转换:在handleHttpError中,原始错误被转换为一个更友好或更适合应用的错误类型(BookTrackerError)。这样,即使错误被传递到订阅者,它也是一个经过处理和转换的错误对象。

  6. 保持Observable的完整性:通过使用throwError,Observable序列可以保持其完整性。错误被视为序列中的一个元素,而不是一个异常情况。

  7. 灵活性:这种方式提供了灵活性,开发者可以选择在catchError中处理错误,并决定是否将错误传递给订阅者,或者替换为其他值。

  8. 统一的错误处理:通过集中处理错误,可以在一个地方记录错误、发送错误报告或执行其他错误响应逻辑,然后决定是否将错误传递出去。

总之,虽然catchErrorthrowError的使用确实将错误传递到了订阅者的error回调中,但这是为了保持Observable的完整性和控制错误处理流程。这种做法允许开发者在一个地方处理错误,同时给订阅者提供了处理错误的机会。

3.2 Resolvers

什么是 Resolvers? 有什么优势?

  1. 在发生路由跳转之前获取数据。
  2. 可以用来阻止“白屏”的出现。
  3. 可以用来阻止“有内部错误”的组件的渲染。
  4. 在某些情况下能带来更好的用户体验(如果某一个页面的数据由唯一的数据接口提供的话)。

image.png

Route Transitions With Resolvers 图解

image.png

下面使用 resolver 实现核心数据加载完成之后再进行路由跳转的功能。

  1. 第一步明确 resolver 本质上是一个实现了 Resolve 接口的自定义 service,然后完成其基本结构的构建。

image.png

  1. 第二步补充 resolve 接口内容即可。

image.png

  1. 第三步修改路由配置 在对应的路由上添加 resolve 配置:

image.png

  1. 第四步从路由上获取网络数据而不是重新发起网络请求 由于在 resolve 中已经发起请求了,请求的数据会保存在路由信息对象中。因此目标组件无需再次发起请求。

image.png

4. Module 4 -- 请求/响应拦截器

首先需要明确的是所谓拦截器,本质上是实现了 HttpInterceptor 接口的自定义服务,又分为请求拦截器和响应拦截器两种。

image.png

拦截器的原理如下所示:

image.png

拦截器的作用为:

  1. 给每一个请求统一添加请求头信息。
  2. 输出网络请求相关的日志
  3. 用来检测网络请求的进度
  4. 实现客户端的缓存

4.1 拦截器实现

实现一个简单的拦截器,如下所示:

image.png

也许您对 axios 拦截器更加熟悉,如果对比来看,可以发现,Angular 中的拦截器有以下几个特点:

1. 本质是一个服务,需要被 @Injectable 修饰符修饰;实现了 HttpInterceptor 接口的 intercept 方法。

2. 作为请求拦截器,需要将请求对象拷贝一份,因为请求对象是 immutable 不可变对象。

3. 请求拦截器和响应拦截器不是分开的,一个服务既可以拦截请求也可以拦截响应,起作用的代码的位置不同而已。

4. 虽然可以将拦截请求和响应的逻辑写在一个服务中,但是实践中还是推荐将它们分开来写。

4.2 拦截器服务注入方式

拦截器本质上是服务,所以必须先被 providers 才能在组件中使用。

image.png

从上面的图中不难发现,网络拦截器不只有一个,它可以有多个,因此我们将 multi 的值设置为 true.

4.3 req.clone 方法

下面我们通过 clone 方法给拦截到的网络请求加上新的请求头。

image.png

那么这个拦截器是如何生效的呢?

  • 我们将这个服务注入到 sharedModule 中,或者叫做 coreModule 中去。
  • 然后在需要的模块中引入 coreModule 即可。
  • 注意无需在 component 中手动调用相关

image.png

4.4 使用多个拦截器

使用下面的代码创建一个新的拦截器。

image.png

然后和上面的拦截器一起使用。

image.png

需要注意的是,这里使用了多个拦截器,所以它们之间肯定是有先后顺序的。

5. Module 5 -- 缓存网络请求

首先,需要明确这么做的意义,也就是为什么要进行网络请求的缓存。

  1. 减少前端发起请求的次数。
  2. 减少服务器压力。
  3. 提高数据响应速度。

image.png

制作一个用来缓存网络请求的服务如下:

image.png

然后我们创建一个拦截器服务来使用上面的缓存服务:

import { Injectable } from '@angular/core';  
import { HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpInterceptor } from '@angular/common/http';  
import { Observable, of } from 'rxjs';  
import { tap } from 'rxjs/operators';  
import { HttpCacheService } from 'app/services/http-cache.service';  
  
@Injectable()  
export class CacheInterceptor implements HttpInterceptor {  
  constructor(private cacheService: HttpCacheService) {}  
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
    // pass along non-cacheable requests and invalidate cache  
    if (req.method !== 'GET') {  
      console.log(`Invalidating cache: ${req.method} ${req.url}`);  
      this.cacheService.invalidateCache();  
      return next.handle(req);  
    }  
  
    const cachedResponse: HttpResponse<any> = this.cacheService.get(req.url);  
    // return cached response  
    if (cachedResponse) {  
      console.log(`Returning a cached response: ${cachedResponse.url}`);  
      console.log(cachedResponse);  
      return of(cachedResponse);  
    }  
  
    // send request to server and add response to cache  
    return next.handle(req).pipe(  
      tap(event => {  
        if (event instanceof HttpResponse) {  
          console.log(`Adding item to cache: ${req.url}`);  
          this.cacheService.put(req.url, event);  
        }  
      })  
    );  
  }  
}

在上面的代码中有几点需要注意:

  1. 我们缓存的对象是连续的 GET 请求,正如代码展示的,一旦某个请求不为 GET 那么就会清除当前缓存,原因在于除了 GET 请求,POST PUT DELETE 都会改变数据库导致已经缓存的 GET 不准确;这里实际上可以优化一下,没必要清除所有的缓存,只需要清除受影响的缓存即可。
  2. 当成功获取客户端缓存之后,不再调用 next.handle 传递此请求,而是直接 return 被 of 包裹的缓存数据。
  3. 没有获取缓存,则使用响应拦截器,在成功得到数据之后保存到缓存中。

6. Module 6 -- 网络请求单元测试

6.1 使用 Angular 自带的单元测试工具

回顾一下单元测试的基本结构:

image.png

在 Angular 中,专门针对 Http 进行测试的模块有两个,它们是:HttpClientTestingModuleHttpTestingController.

6.2 beforeEach 中的一般内容

在下面的代码中,展示单元测试时候 beforeEach 钩子的一般内容,其中 DataService 是待测试对象,而 HttpClientTestingModuleHttpTestingController 则是上文提到的辅助模块。

image.png

最引入注目的是,在单元测试中,实例化用的不是 new 而是 TestBed.get 方法。

6.3 单元测试的基本结构

在 beforeEach 中提供测试所需的支持之后,就可以针对用例进行测试了,一个基本的测试结构如下所示:

image.png

上面的代码展示了我们如何通过 dataService 发起网络请求,并使用 httpTestingController 接受响应值。

6.4 一个实际的例子

首先必须要说的是,所有的测试都应该放在名为 .spec.ts 后缀的 ts 文件中

代码示例如下:

import { TestBed } from '@angular/core/testing';  
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';  
import { DataService } from './data.service';  
import { Book } from 'app/models/book';  
  
describe('DataService Tests', () => {  
  let dataService: DataService;  
  let httpTestingController: HttpTestingController;  
  let testBooks: Book[] = [  
    { bookID: 1, title: 'Goodnight Moon', author: 'Margaret Wise Brown', publicationYear: 1947 },  
    { bookID: 2, title: 'Winnie-the-Pooh', author: 'A. A. Milne', publicationYear: 1926 },  
    { bookID: 3, title: 'The Hobbit', author: 'J. R. R. Tolkien', publicationYear: 1937 }  
  ];  
  
  beforeEach(() => {  
    TestBed.configureTestingModule({  
      imports: [HttpClientTestingModule],  
      providers: [DataService]  
    });  
    dataService = TestBed.get(DataService);  
    httpTestingController = TestBed.get(HttpTestingController);  
  });  
  
  afterEach(() => {  
    httpTestingController.verify();  
  });  
  
  it('should GET all books', () => {  
    dataService.getAllBooks().subscribe((data: Book[]) => {  
      expect(data.length).toBe(3);  
      let booksRequest: TestRequest = httpTestingController.expectOne('/api/books');  
      expect(booksRequest.request.method).toEqual('GET');  
      booksRequest.flush(testBooks);  
    });  
  });  
  
  it('should return an error', () => {  
    dataService.getAllBooks().subscribe(  
      (data: Book[]) => fail('this should have been an error'),  
      (err: any) => {  
        // Note: Assuming BookTrackerError is a custom error class. If not, replace with appropriate error type.  
        // Also, the exact properties of the error may vary depending on your implementation.  
        expect(err.status).toEqual(500);  
        expect(err.statusText).toEqual('Server Error');  
        let booksRequest: TestRequest = httpTestingController.expectOne('/api/books');  
        booksRequest.flush('error', { status: 500, statusText: 'Server Error' });  
      }  
    );  
  });  
});

上述代码详细解释如下:

这段代码是一个Angular单元测试,用于测试名为DataService的服务。这个服务很可能涉及到HTTP请求,用于获取书籍数据。测试使用了Angular的测试工具,特别是TestBedHttpTestingController,来模拟HTTP请求和响应。

导入依赖

首先,代码导入了必要的依赖项,包括Angular的测试模块、HTTP测试模块,以及要测试的服务和模型。

测试设置

describe块中,设置了测试环境。定义了两个主要变量:dataService(被测试的服务实例)和httpTestingController(用于模拟HTTP请求的控制器)。还定义了一个testBooks数组,用于模拟从服务器返回的书籍数据。

beforeEach 和 afterEach

  • beforeEach:在每个测试用例执行之前运行。这里,它配置了测试模块,包括导入HttpClientTestingModule和提供DataService。然后,它初始化dataServicehttpTestingController
  • afterEach:在每个测试用例执行之后运行。这里,它调用httpTestingController.verify()来确保没有未完成的HTTP请求。

测试用例

  1. 'should GET all books' :这个测试用例模拟了一个成功的GET请求,用于获取所有书籍。

    • 通过dataService.getAllBooks()发起请求。
    • 使用subscribe处理响应,期望返回的书籍数组长度为3。
    • 使用httpTestingController.expectOne('/api/books')来捕获这个请求,并验证请求方法是GET。
    • 使用booksRequest.flush(testBooks)来模拟服务器响应,返回预定义的testBooks数组。
  2. 'should return an error' :这个测试用例模拟了一个失败的GET请求,用于测试错误处理。

    • 同样通过dataService.getAllBooks()发起请求。
    • subscribe的第一个回调函数中,使用fail函数来确保这个回调不应该被调用(因为这应该是一个错误情况)。
    • subscribe的第二个回调函数(错误处理回调)中,验证返回的错误状态码和状态文本是否符合预期(500和'Server Error')。
    • 使用booksRequest.flush来模拟一个错误响应。

总结

这段代码是一个典型的Angular服务单元测试,用于验证DataService中的getAllBooks方法在各种情况下的行为。通过使用HttpTestingController,它能够在不依赖实际后端服务的情况下模拟HTTP请求和响应,从而使测试更加独立和可控。


实践和练习

1. 在项目中实践下面代码

getAllBooks(): Observable<Book[] | BookTrackerError> {  
  console.log('Getting all books from the server.');  
  return this.http.get<Book[]>('/api/books'// 假设API路径是正确的  
    .pipe(  
      catchError((err: HttpErrorResponse) => this.handleHttpError(err))  
    );  
}  
  
private handleHttpError(errorHttpErrorResponse): Observable<BookTrackerError> {  
  let dataError = new BookTrackerError();  
  dataError.errorNumber = 10// 假设这是错误编号  
  dataError.message = error.statusText// 使用错误的状态文本  
  dataError.friendlyMessage = 'An error occurred retrieving data.'// 用户友好的错误消息  
  return throwError(dataError); // 返回一个包含错误的Observable  
}

这是在项目中的实践代码:

    getAllAlarms(): Observable<IAlarm[] | AlarmTrackerError> {
        return this.http.get<IAlarm[]>('/api/alarms').pipe(
            catchError((err: HttpErrorResponse) => this.handleHttpError(err))
        )
    }

    private handleHttpError(err: HttpErrorResponse) {
        let dataError = new AlarmTrackerError();
        dataError.code = 10;
        dataError.message = err.statusText;
        dataError.friendlyMessage = 'An error occured retrieving data';
        return throwError(dataError);
    }
export class AlarmTrackerError {
    code: number;
    message: string;
    friendlyMessage: string;
}
  async ngOnInit() {
    this.alarmDataService.getAllAlarms().subscribe({
      next: (data: IAlarm[]) => {
        console.log('data:', data);
        this.alarms = data;
      },
      error: (error: AlarmTrackerError) => {
        console.log('error:', error);
        this.error = error;
      }
    })
  }

2. 搭建本地后端环境,实现下面的功能,并提供对应的代码

见附录,如何搭建后端环境,以及和前端工程化项目做好的融合。

2.1 在开发环境下,使用请求拦截器配置统一的测试 api 服务器的地址


在Angular中,实现一个请求拦截器并在其中添加base_url的功能,你可以按照以下步骤进行:

  1. 创建拦截器类: 创建一个新的拦截器类,实现HttpInterceptor接口,并在intercept方法中修改请求的URL,添加base_url

  2. 注册拦截器在你的Angular模块中(通常是app.module.ts),将这个拦截器注册到HTTP_INTERCEPTORS提供者中。

  3. 配置base_url: 你可以将base_url配置在环境变量中,以便在不同环境中使用不同的base_url

以下是具体的实现步骤:

1. 创建拦截器类
import { Injectable } from '@angular/core';
import {
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';

@Injectable()
export class BaseUrlInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

        const target = req.url;
        if (target && target.startsWith(`/api/`)) {
            // 获取环境变量中的base_url  
            const baseUrl = environment.baseUrl;

            // 修改请求的URL,添加base_url  
            const updatedReq = req.clone({
                url: `${baseUrl}${req.url}`
            });

            // 将修改后的请求传递给下一个处理器  
            return next.handle(updatedReq);
        } else {
            return next.handle(req);
        }

    }
}
2. 注册拦截器

在你的Angular模块中注册这个拦截器:

import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { BaseUrlInterceptor } from './interceptors/base-url.interceptor';

@NgModule({
  // ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: BaseUrlInterceptor,
      multi: true
    }
  ]
})
export class AppModule { }
3. 配置base_url

在你的环境配置文件中设置baseUrl

// environment.ts
export const environment = {
  production: false,
  baseUrl: 'http://localhost:3333/api/'
};

// environment.prod.ts
export const environment = {
  production: true,
  baseUrl: 'https://your-production-api.com/api/'
};

现在,每当你使用HttpClient发出请求时,请求的URL都会自动添加上你在环境变量中配置的baseUrl。这个拦截器会拦截所有的HTTP请求,并在请求发出之前修改它们的URL。


2.2 给每一个请求统一添加请求头信息

和 2.1 过程基本相同。

import { Injectable } from '@angular/core';
import {
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpInterceptor,
    HttpHeaders
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthHeaderInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // 创建新的请求头,添加或修改Author字段  
        const newHeaders = new HttpHeaders({
            'Author': 'Supcon',
            // 可以在这里添加或覆盖其他请求头  
        });

        // 修改请求的头部信息  
        const updatedReq = req.clone({
            headers: newHeaders
        });

        // 将修改后的请求传递给下一个处理器  
        return next.handle(updatedReq);
    }
}

注册过程:

    {
      provide: HTTP_INTERCEPTORS,
      useClass: BaseUrlInterceptor,
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHeaderInterceptor,
      multi: true,
    },

后端增加中间件对其进行检验:

// 中间件:检查Author请求头  
app.use((req, res, next) => {
  const author = req.headers['author'];
  if (author !== 'Supcon') {
    return res.status(401).send('Unauthorized');
  }
  next();
});

上述过程实际上是在模拟鉴权的过程。

2.3 使用多个拦截器,输出网络请求相关的日志

功能为,检测 localStorage 中的 log 是否为 true 如果是的话,此拦截器会将每一个以 /api/ 开头的请求的响应值打印出来。

要创建一个Angular响应拦截器,并实现检测localStorage中的log是否为"true",如果是的话,将每一个以/api/开头的请求的响应值打印出来的功能,你可以按照以下步骤进行:

  1. 创建拦截器类: 首先,你需要创建一个拦截器类,实现HttpInterceptor接口。

  2. 注册拦截器: 然后,你需要在你的Angular模块中注册这个拦截器,以便Angular知道要使用它。

  3. 实现拦截逻辑: 在拦截器的intercept方法中,实现你的逻辑:检查localStorage,打印响应等。

下面是具体的实现步骤:

1. 创建拦截器类
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { environment } from '../environments/environment';

@Injectable()
export class ResponseInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          // 检查localStorage中的log是否为"true"  
          if (localStorage.getItem('api_log') === 'true' && req.url.startsWith(`${environment.baseUrl}/api/`)) {
            // 打印响应值  
            console.log(
              '%cAPI Response Log:',
              'color:#fff;background:#d06e23;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
              event.body
            );
          }
        }
      })
    );
  }
}
2. 注册拦截器

在你的Angular模块中(通常是app.module.ts),注册这个拦截器:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ResponseInterceptor } from './path/to/response.interceptor';

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

确保将'./path/to/response.interceptor'替换为拦截器类的实际路径。

3. 使用拦截器

现在,每当你的Angular应用发送一个以/api/开头的HTTP请求时,响应拦截器都会检查localStorage中的api_log项。如果该项为"true",则响应值将被打印到控制台。

这样,你就成功创建了一个Angular响应拦截器,用于在特定条件下打印HTTP响应值。

2.4 用来检测网络请求的进度,显示到进度条中

这里就不用进度条显示了,直接打印在控制台上好了。需要注意的是在网络请求的不同阶段会产生不同的事件,见如下代码:

import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HttpHeaderResponse,
  HttpProgressEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class ProgressInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // 修改请求以报告进度  
    const progressReq = req.clone({
      reportProgress: true,
    });

    return next.handle(progressReq).pipe(
      tap((event: HttpHeaderResponse | HttpResponse<any> | HttpProgressEvent) => {
        if (event instanceof HttpHeaderResponse) {
          console.log(
            '%cRequest Start',
            "color:#fff;background:#292c30;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;"
          );

        } else if (event instanceof HttpResponse) {
          // 请求完成时也可以做一些事情  
          console.log(
            '%cRequest Complete',
            "color:#fff;background:#292c30;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;"
          );
        } else {
          const { type, total, loaded } = event;
          if (
            type &&
            total &&
            loaded &&
            type === 3
          ) {
            const percentDone = Math.round(100 * loaded / total);
            console.log(
              '%cRequest Progress:',
              "color:#fff;background:#292c30;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;",
              ` ${percentDone}%`
            );
          }
        }
      })
    );
  }
}

2.5 实现客户端的缓存

缓存的原理有两点:

  1. 如果一直进行 get 请求,在其中并没有穿插 post put delete 这些请求,那么按道理数据库中的数据是不可能发生变化的,因此只要一直进行 get 请求,理论上就可以返回缓存的数据给页面。
  2. 使用请求拦截器实现网络资源缓存的功能。原理在于:如果是非 get 请求,则清空缓存池。如果是 get 请求则在缓存池中查找有无此 url 对应的缓存记录,如果有的话则直接 return 而不去调用 next.handle 这样便不会发起请求。
  3. 缓存池通过 cacheService 实现,因为服务本质上就是单例。
  4. 需要注意的是此拦截器的位置需要仔细考虑,到底是在拼接 base_url 之前呢,还是之后呢,需要认真考虑一下。

拦截器代码:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpInterceptor } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { HttpCacheService } from '../services/http-cache-service';

@Injectable()
export class CacheInterceptor implements HttpInterceptor {
    constructor(private cacheService: HttpCacheService) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // pass along non-cacheable requests and invalidate cache  
        if (req.method !== 'GET') {
            console.log(
                '%cInvalidating cache:',
                'color:#fff;background:#008800;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
                `${req.method} ${req.url}`,
            );
            this.cacheService.invalidateCache();
            return next.handle(req);
        }

        const cachedResponse: HttpResponse<any> = this.cacheService.get(req.url);
        // return cached response  
        if (cachedResponse) {
            console.log(
                '%cReturning a cached response:',
                'color:#fff;background:#008800;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
                `${cachedResponse.url}`
            );
            return of(cachedResponse);
        }

        // send request to server and add response to cache  
        return next.handle(req).pipe(
            tap(event => {
                if (event instanceof HttpResponse) {
                    console.log(
                        '%cAdding item to cache:',
                        'color:#fff;background:#008800;marign-right:12px;padding:2px 6px;display:inline-block;border-radius:4px;',
                        this.cacheService.put(req.url, event)
                    )
                }
            })
        );
    }
}

缓存池代码:

import { Injectable } from "@angular/core";
import { HttpResponse } from "@angular/common/http";

@Injectable({
    providedIn: 'root'
})
export class HttpCacheService {
    private requests: any = {};

    constructor() { }

    put(url: string, response: HttpResponse<any>): void {
        this.requests[url] = response;
    }

    get(url: string): HttpResponse<any> | undefined {
        return this.requests[url];
    }

    invalidateUrl(url: string): void {
        this.requests[url] = undefined;
    }

    invalidateCache(): void {
        this.requests = {};
    }
}

注册顺序:

    {
      provide: HTTP_INTERCEPTORS,
      useClass: CacheInterceptor,
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: BaseUrlInterceptor,
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHeaderInterceptor,
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ResponseInterceptor,
      multi: true
    },

2.6 完成请求拦截器和响应拦截器的分离实践

已经完成,如下所示:

    {
      provide: HTTP_INTERCEPTORS,
      useClass: BaseUrlInterceptor,
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHeaderInterceptor,
      multi: true,
    },
    { 
      provide: HTTP_INTERCEPTORS, 
      useClass: ResponseInterceptor, 
      multi: true 
    },

代码文件夹结构为:

image.png

3. 使用 resolver 预先获取页面所需数据,并使用 observable 的操作符将多个端口数据合为一处

实践结果如下:

// resolvers/AlarmResolverService.ts
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
// import { SomeService } from './some.service'; // 假设的服务

@Injectable({
    providedIn: 'root',
})
export class AlarmResolverService implements Resolve<any> {
    constructor() { }

    resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<any> | Promise<any> | any {
        // 调用服务方法获取数据
        return of([
            { value: '', label: 'None', },
            { value: 'Running', label: 'Running', },
            { value: 'Offline', label: 'Offline', },
        ]);
    }
}
const routes: Routes = [
  {
    path: 'iotalarmdetails/:id',
    component: AlarmDetailsComponent,
    canActivate: [RouteGuard],
    resolve: {
      resolvedDeviceStatus: AlarmResolverService,
    }
  }
];
this.resolvedDeviceStatusOptions = this.route.snapshot.data['resolvedDeviceStatus'];
<mat-select formControlName="deviceStatus">
<mat-option
  *ngFor="let status of resolvedDeviceStatusOptions"
  [value]="status.value"
>
  <!-- *ngFor="let status of deviceStatusOptions$ | async as deviceStatusOptions" -->
  {{status.label}}
</mat-option>
</mat-select>

4. 完成一个网络数据缓存器的制作,并实践 Post Put 之前缓存,Post Put 之后刷新的功能

附录 -- Angular 前端项目和开发 api 服务器的融合

使用到的技术有:concurrently 和 nodemon, 接下来我们分步骤讲述这个过程。

  1. 在 Angular 项目的根文件外层创建一个和根目录同级的目录,比如起名为:service, 合理的做法是将 api 服务器放在单独的目录中而不是和前端项目混合在一起,因为这样做的话会产生依赖之间的相互干扰,这是不好的实践。
  2. 接下来在此目录中启动一个后端 api 服务器,具体做法为:
  • npm init -y
  • npm install -g nodemon concurrently
  • npm install express cors
  • touch service.js
const express = require('express');
const cors = require('cors');

const app = express();
const port = 3333;

app.use(cors());
app.use(express.json());

// 中间件:检查Author请求头  
app.use((req, res, next) => {
  const author = req.headers['author'];
  if (author !== 'Supcon') {
    return res.status(401).send('Unauthorized');
  }
  next();
});

// 定义一个GET路由,并设置跨域响应头  
app.get('/api/deviceStatusOption', (req, res) => {
  // 设置允许跨域的域名,*表示允许所有域名  
  res.header('Access-Control-Allow-Origin', '*');
  // 设置允许的HTTP方法  
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  // 设置允许的HTTP头  
  res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
  // 设置允许发送Cookie  
  res.header('Access-Control-Allow-Credentials', true);

  res.json([
    { value: '', label: 'None', },
    { value: 'Running', label: 'Running', },
    { value: 'Offline', label: 'Offline', },
  ]);
});

// 定义POST路由处理函数  
app.post('/api/alarmData', (req, res) => {
  // 从请求体中提取id  
  const { id } = req.body;
  console.log('id:', id)

  // 判断id是否存在  
  if (id) {
    // 如果提供了id,则返回预设的数据结构  
    const responseData = {
      data: {
        baseDetail: {
          alarmID: ["ALM#219481951"],
          alarmStatus: ["$Active"],
          alarmType: ["$Temperature"],
          alarmPriority: ["Normal"],
          alarmRaiseDate: [1719906188309],
          alarmDescription: ["SD20240101"],
          errorCode: ["400"],
          errorDescription: ["unconnected"],
          workordernum: ["WD#12345"],
          ackID: ["XXXXXX"],
          createdBy: ["TOM CARVERS"],
          createdTime: [1719906188309],
          updatedBy: ["TOM CARVERS"],
          updatedTime: [1719906188309]
        },
        relatedDevice: [
          {
            deviceNum: ["ENO#219481951"],
            deviceStatus: "Running",
            deviceEUI: ["24E124723D495091"],
            deviceShortDesp: ["It is a temperature sensor"],
            lowTmp: ["-12.5"],
            highTmp: ["35.5"]
          }
        ]
      },
      message: 'success',
      success: true,
      code: 200
    };
    res.json(responseData);
  } else {
    // 如果没有提供id,则返回错误信息  
    const errorResponse = {
      data: {},
      message: 'failed',
      success: false,
      code: 404
    };
    res.status(404).json(errorResponse);
  }
});

// 启动服务器  
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});
  1. 然后在前端项目的 package.json 中增加新的 script 命令,并使用刚才全局安装的 concurrentlynodemon, 这里的 script 的写法是有讲究的:
    "start": "ng start",
    "startservice": "nodemon ../iot-service/service.js",
    "locstart": "concurrently --kill-others \"npm run start\" \"npm run startservice\"",
  1. 最后在前端项目的根目录下运行 npm run locstart 就可以看到前端项目和后端的 api 服务器同时在运行了。