Angular’s HttpClient module, in addition to making web requests, also provides web interceptor functionality. In interceptors, you can get both the request object and the server-side response object for the request, so you can handle all kinds of net-related functions in a unified manner. After all, uniformity means better maintainability, more robust applications, and, most importantly, more “slack.” After all, laziness is a software engineer’s virtue! 😂

Angular registers an interceptor, which is a class that implements the HttpInterceptor interface:

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

export class ExampleInjector implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any> > {returnnext.handle(req); }}Copy the code

This interface has only one Intercept method, which takes two arguments:

  • HttpRequestRepresents the request object through which the request can be modified;
  • HttpHandlerRepresents the next interceptor, passing through layers of interceptors untilHttpBackendIs responsible for sending requests through the browser’s HTTP API to the back-end server.

    而 HttpHandler 的 handleMethod, and finally returns oneObservableSubscriptions are available at all stages of the requestHttpEventType. For example, upload/download progress, custom events, back-end responses, etc.

    throughObservable çš„ pipeMethod, you can make any changes to the response.

To make the interceptor work, inject it directly into the root module:

@NgModule({...providers: [{provide: HTTP_INTERCEPTORS, useClass: ExampleInjector, multi: true}].bootstrap: [AppComponent]
})

export class AppModule {}Copy the code

Note the multi parameter, which means that multiple interceptors can be injected in a typical chchain manner, in the order of the providers array, the requests in reverse order, and the responses in reverse order.

In addition, with the exception of HttpClient, the common Axios library is similar in function and implementation, but the engineering practice is the same.

In common projects, the use of interceptors can be divided into three main types.

1. Modify the request

Modifying requests is the most common case for interceptors. Note that HttpRequest is read-only, and since a network request object can be retried multiple times, you must ensure that it is the original request object each time it passes through the interceptor. To modify the request object, use its Clone method to create a copy and pass it to the next interceptor.

Modify the URL

For example, let’s go from HTTP to HTTPS.

// Clone the request and replace http:// with https://
const reqClone = req.clone({
    url: req.url.replace('http://'.'https://')});return next.handler(reqClone)
Copy the code

Or add server side IP and port, responsible for the API uniform prefix.

req.clone({
    url: environment.serverUrl + request.url
});
Copy the code

However, in most cases, there is a better solution, for example HTTPS can be implemented with ng serve-ssl. And request path requirements, configuration CLI Webpack development server is more suitable, in addition to rewrite the path, it can also support detailed configuration proxy, to solve the cross domain.

Set the Header

Changing the Header of a request is much more common than changing the URL or requesting the Body. For example, the previous article described how to use JWT for login authentication, and the Header needs to carry this JWT when the front end initiates a request.

In addition, since it is too common to clone a request and set a new header at the same time, there is a shortcut to setHeaders:

const reqClone = req.clone({ setHeaders: { Authorization: JWT } });
return next.handler(reqClone)
Copy the code

2. Unified processing

It is much better to have uniform implicit processing through interceptors than to explicitly handle some task on each request. Error handling, for example, is something you don’t like to write error handling in then/ SUBSCRIBE after each request returns a Promise/Observable. This is troublesome, repetitive work, but also prone to error.

logging

Interceptors can retrieve both requests and responses, making them ideal for logging the full life cycle of HTTP requests. For example, capture request and response times and record elapsed time results.

const started = Date.now()
let ok: string = ' ';

return next.handle(req).pipe(
    tap(
        (event: HttpEvent<any>) = > ok = event instanceof HttpResponse ? 'succeeded' : ' '.(error: HttpEventResponse) = > ok = 'failed'
    ),

    // Log when the response ends
    finalize(() = > {
        const elapsed = Date.now() - started;
        const msg = `${req.method} "${req.urlWithParams}" ${ok} in ${elapsed} ms.`
        console.log(msg); }))Copy the code

Error handling

If a network error occurs after the response is returned, handle the error according to the HTTP status code.

Configure various HTTP status prompts first:

// Prompts corresponding to various HTTP status codes
const HTTP_MESSAGES = new Map([[200.'Server successfully returned requested data'],
    [201.'Created or modified data successfully'],
    [202.'Request has been queued in background (asynchronous task)'],
    [204.'Data deleted successfully'],
    [400.'Error request sent, server did not create or modify data'],
    [401.'User has no permissions (wrong token, username, password)'],
    [403.'User is authorized, but access is forbidden.'],
    [404.'Request made for nonexistent record, server did not act on it'],
    [406.'Requested format not available'],
    [410.'Requested resource is permanently deleted and will not be retrieved'],
    [422.'A validation error occurred while creating an object'],
    [500.'Server error, please check server'],
    [502.'Gateway error'],
    [503.'Service unavailable, server temporarily overloaded or maintained'],
    [504.'Gateway timed out']]);Copy the code

If an error occurs in the request, an error message is displayed according to the Status attribute of HttpErrorResponse.

return next.handle(req).pipe(
    catchError((err: HttpErrorResponse) = > {
        let errorMessage: { message: string; };
        if (HTTP_MESSAGES.has(err.status)) {
            errorData = { message: CODE_MESSAGE.get(err.status) };
        } else {
            errorData = { message: 'Unknown error, please contact administrator' };
        }
        // Encapsulates a hint service class, decoupled from third-party UI libraries.
        this.MessageService.error(errorData.message);

         // Throw an exception message
        returnthrowError(errorData); }));Copy the code

For a specific status code, jump to the page:

switch (err.status) {
    case 401:
        this.router.navigateByUrl('/authentication/login');
        break;
    case 403:
    case 404:
    case 500:
        this.router.navigateByUrl(`/exception/${err.status}`);
        break;
}
Copy the code

notification

In addition to common network errors, the backend usually returns a specific status code, along with a corresponding user prompt, which can be handled uniformly in the interceptor. Also, when the processing is complete, you can return a pure data value, simplifying the code when subscribing to the response.

return next.handle(req).pipe(
    mergeMap((ev) = > {
        // Handle only events whose HttpEvent type is the response object
        if (ev instanceof HttpResponse) {
            // 20000 is successfully executed
            if (body && body.code === '20000') {
                return of({ data: body.data });
            } else {
                // Execution fails, an error message is displayed, and null is returned
                this.MessageService.error(errorData.message);
                return of(null); }}return of(ev); }));Copy the code

Loading status

In some cases, the design may want to display a global loading animation when a page jumps or a network request, like the loading progress bar at the top of Angular’s official website. The network request part can be implemented through interceptors.

Start by creating a service that is used to share services between interceptors and components.

@Injectable({ providedIn: 'root' })
export class LoaderService {

    private isGlobalLoading = new BehaviorSubject<boolean> (false);
    public isGlobalLoading$ = this.isGlobalLoading.asObservable();

    public show(): void {
        this.isGlobalLoading.next(true);
    }

    public hide(): void {
        this.isGlobalLoading.next(false); }}Copy the code

In this interceptor, loaderService is injected. The animation is enabled when the request is initiated, and the finalize operator is used to close the animation after the response.

intercept(req: HttpRequest<any>, next: HttpHandler){
    loaderService.show()
    return next.handle(req).pipe(
        finalize(() = > loaderService.hide())
    );
}
Copy the code

In the component that is responsible for loading the animation globally, loaderService is also injected, isGlobalLoading$is assigned to the binding property of the animation, and the async pipeline is used to implement explicit and implicit switching of the animation.

@Component({
  selector: 'app-loader'.template: ` 
      
`
,})export class LoaderComponent { isLoading: Subject<boolean> = this.loaderService.isLoading; constructor(private loaderService: LoaderService){}}Copy the code

3. Response processing

As mentioned earlier, the data of the response object extraction, simplify the request callback call (to avoid such a call: httpResponse. Data. Data. Something), in addition to this, can do simulation interface, format processing in the interceptor, the cached data, etc.

Simulation of the response

In the case of separate front-end and back-end development, often the back-end interface is not ready, but the development related to network requests, such as loading animations, interface services, response callbacks, and so on, can be carried out without any problems simply by simulating the interface data during development.

As mentioned earlier, the filter passes requests through layers of next.Handle (REQ). So stop calling this method in the mock response interceptor and return an Observable containing mock data.

private mockApiList = new Map<string.object>([
    ['/example/getData', {
        code: '20000'.data: { example: true}}]]);intercept(req: HttpRequest<any>, next: HttpHandler) {
    if(this.mockApiList.has(req.url){
        // If the interface path to emulate matches, the simulated data is returned
        return of(this.mockApiList.get(req.url)); 
    }else{
        // Other requests are normally passed to the next interceptor
        returnnext.handle(req); }}Copy the code

To work with filters such as caching or data filtering, filters that simulate responses are placed at the end of Providers.

Further, we could maintain a service, like mockApiList, that requires the interface address and mock data to be emulated, and normally requests other unconfigured interface paths. As the Delon/Mock library does, it becomes a general-purpose Mock interface library.

Format conversion

Sometimes someone on the back end might return a field like “SOMETHING_EXAMPLE”, but for the sake of front-end engineering consistency, it’s better to convert fields to a small hump. For simplicity, Lodash’s mapKey and camelCase are used.

return next.handle(req).pipe(
    map((event: HttpEvent<any>) = > {
        if (event instanceof HttpResponse) {
            let camelCaseBody = mapKeys(event.body, (v,k) = > camelCase(k));
            const modEvent = event.clone({ body: camelCaseBody });

            returnmodEvent; }}))Copy the code

Data cache

During the application lifecycle, some interfaces may need to be retrieved only once and then retrieved from the cache to improve performance. These interfaces, which need to be cached, can be uniformly configured in the interceptor.

Define a list of interfaces that need to be cached. Determine whether each request needs to be cached. If no, skip it. If it does, it tries to fetch the data from the cache service. If it does, it skips all interceptors and returns the data directly. If it does not, it initiates a request to fetch the data and stores it in the cache service.

private cacheApis = ['/user/userDetail'];
// Inject the cache service
constructor(private cache: CacheService) {}

intercept(req: HttpRequest<any>, next: HttpHandler) {
    if(! isNeedCache(req)) {return next.handle(req); }

    // Try to fetch it from the cache service
    const cachedResponse = this.cache.get(req);
    if(cachedResponse){ 
        return of(cachedResponse)  // Return the cached data directly
    }
    else{ 
        return next.handle(req).pipe(
            tap(event= > {
                if (event instanceof HttpResponse) {
                    cache.put(req, event); // Get the data from the background and store it in the cache service}})); } } isNeedCache():boolean {
    return this.cacheApis.indexOf(req.url) ! = = -1;
}
Copy the code

conclusion

Interceptor design ideas can be found in many frameworks, such as Spring Security, which is implemented through a chain of filters, Servlet filters, or the common Axios library. It is suitable for implicitly completing all kinds of universal network operations. There are many usages in projects. Here, it just lists the parts used in ordinary projects, hoping to explore more creative development practices.

In addition, as you can see from the analog interface mentioned earlier, in addition to the above basic use, interceptors can also be used to encapsulate more convenient, rich library, simplify development, unified handling of network transactions, just like the Delon library. Expect more open source libraries to enrich the framework ecology 😋