The interceptor
Interceptors, like pipes and guards, exist as classes (or as a function) and give dependencies to Nest to host with @Injectable(). And the implementation of NestInterceptor interface. This interface has only one method to implement intercept(Context, Next). Let’s look at the source code:
export interface NestInterceptor<T = any, R = any> {
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<R> | Promise<Observable<R>>;
}
Copy the code
After each interceptor is activated, two parameters are passed in: the current context and a handle to the method to be executed. If you don’t know much about ExecutionContext, refer to the Guard section. Both are completely one instance. Next is the Observable asynchronous interface, which is obviously the next process to be performed. If you do not use next.handle() during the interception, the request process will stop at this point, so the process will contain the logic before and after the execution, which is called a slice (during the request and response).
Section-oriented programming
Taking a cue from section-oriented programming (AOP), interceptors have a set of capabilities:
- Bind additional business logic before or after a method execution; Such as parameter conversion (string conversion to DTO required type) or result conversion (uniform API return result);
- Extend the basic functionality of a method;
- Block a method under certain conditions;
Binding interceptor
Let’s start by creating a conversion interceptor, Converter
nest g in Converter
Copy some of the code from the guard and modify it slightly (note: the next variable conflicts with the interface parameter name) :
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable(a)export class ConverterInterceptor implements NestInterceptor {
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any> > {const [request, response] = context.getArgs();
switch (context.getType()) {
case 'http':
const [request, response, next_] = context.getArgs();
console.log(
`${request.protocol}: / /${request.hostname}${ request.path }The ${JSON.stringify(request.query)}= >${context.getClass().name}.${ context.getHandler().name }`,);break;
case 'ws':
break;
case 'rpc':
break;
}
returnnext.handle(); }}Copy the code
Those of you who have seen the Middleware section will be asking if you can use context.getargs () to call the business you are about to execute if you get a next. The answer is no, there is no business execution stack for this request, next is an empty stack retrieved from the context, and execution can only be continued with the utility entry parameter next.
Interceptors are bound in a similar way to exception filters and guards, by decorators to classes or methods, and even registered as global interceptors in modules. Take a look at the source code for the decorator, UseInterceptors, as usual:
export function UseInterceptors(. interceptors: (NestInterceptor |Function) []) :MethodDecorator & ClassDecorator {
return (
target: any, key? :string| symbol, descriptor? : TypedPropertyDescriptor<any>,
) = > {
const isInterceptorValid = <T extends Function | Record<string, any>>(
interceptor: T,
) =>
interceptor &&
(isFunction(interceptor) ||
isFunction((interceptor as Record<string, any>).intercept));
if (descriptor) {
validateEach(
target.constructor,
interceptors,
isInterceptorValid,
'@UseInterceptors',
'interceptor',
);
extendArrayMetadata(
INTERCEPTORS_METADATA,
interceptors,
descriptor.value,
);
return descriptor;
}
validateEach(
target,
interceptors,
isInterceptorValid,
'@UseInterceptors',
'interceptor',
);
extendArrayMetadata(INTERCEPTORS_METADATA, interceptors, target);
return target;
};
}
Copy the code
For those of you who have perused guard decorators, this code should be familiar. The logic is almost identical. Again: receives one or more interceptors, verifies their validity and stores them as metadata for classes or methods. Its classes, methods, and global binding methods are not described here, but for those who are less clear, refer to the section on guards or exception filters. Then we bind to methods like controllers:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { ConverterInterceptor } from './converter.interceptor';
@Controller(a)export class AppController {
constructor(private readonly appService: AppService) {}
@Get(a)@UseInterceptors(ConverterInterceptor)
getHello(): string {
console.log('in controller.');
return this.appService.getHello(); }}Copy the code
When we perform:
curl -X GET "http://localhost:3000/"
# http://localhost/{} => AppController.getHello
Copy the code
Multiple interceptors
Multiple interceptors can be a decorator that uses multiple interceptors at the same time (at the same level), or it can be binding and functioning through global, class, and method bindings (at different levels). We can understand its process through an experiment. We add three interceptors, namely global interceptors, class interceptors and method interceptors, and add two interceptors to a method. After running, observe its execution process, and the code is similar to the following:
@Injectable(a)export class MethodInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Method Interceptor');
return next.handle().pipe(
tap((data) = > {
console.log(`Result(Method):${data}`); })); }}Copy the code
See the obvious loading and unloading process:
Global Interceptor
Class Interceptor
Method Interceptor
http://localhost/async{} => AppController.getHelloAsync
Result:Hello World!
Result(Method):Hi World!
Result(Class):Hi World!
Result(Global):Hi World!
Copy the code
Intercept subsequent logic
The previous article focused on what was done before the interception, and after the call to next.handle(), the next business process is moved on. If the requirement is to modify the return value based on some condition, then some logic needs to be performed on the return result. Looking back at the code, the intercept method returns a value of type Observable, which is the core type of RxJS, and it’s worth taking a look at this.
RxJS is the simplest way to get started
Limited by space, this is just a brief introduction to the Nest related parts of this excellent framework. Those who are interested can refer to the official documentation. Rx = Reactive Extension, RxJS is an implementation framework for Reactive programming (RP). An Observable literally means that when an event occurs, the data input is processed by the function assigned before the event occurs. The process of assignment is subscription, and the process of event occurrence is a publication. For example, the ups and downs of stocks are added to a time axis to form a rectangular wave chart. This series of data forms a data “stream”. If the logic of this action is to “chase the rally and kill the fall”, then the simulation code is as follows:
import { fromEvent } from 'rxjs';
const VOLUME=10;
let observable$ = fromEvent(stockMaotai, 'fluctuate').subscribe((value) = > {
if (value > 0) {
buyin(VOLUME);
}else{ sellout(VOLUME); }});Copy the code
If the entire process produces data only once, it is called a single value stream (most Nest requests are single-valued). Flow is the core concept of responsive programming. There are three types of values in this stream: the normally generated value, the raised exception, and the entire end-of-stream signal. The process of generating values can be synchronous or asynchronous, and so can the methods in the Controller. With RxJS, the two have been perfectly unified, so that whether you are synchronous or asynchronous (the method in the Controller or the next interceptor), the operation is the same.
RxJS has two types of operators. One is the operator to create a stream, such as fromEvent, and the common of, etc. The other is the pipe operator, which is like adding some chemicals in the pipe, so that the data flowing into the pipe changes to a certain extent, and then flows out.
The Pipe operator is commonly used
observable.pipe(op1,op2,op3…) The PIPE method can input the pipe operator.
- Map: You can modify the input value and output it. Similar to JS map function;
- Tap: receives a value each time it is entered without changing the value;
- Timeout: Sets an expiration time. If no completion signal is received within this time, the logic will start.
- CatchError: operation performed when an exception (throw) occurs in a pipe
intercepts
Now that we have a basic understanding of the basic concepts of RxJSstream, it’s easier to understand the rest of the interceptor. We print the result of executing the business:
@Injectable(a)export class ClassInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Class Interceptor');
return next.handle().pipe(
tap((data) = > {
console.log(`Result(Class):${data}`); })); }}Copy the code
Note that next is the object that points to the next procedure. The Handle () method gets the Observable object, and the pipe() method adds the desired operation logic. If you need to convert the result, you can use the map operator:
return next.handle().pipe(
map((data) = > {
console.log(`Result:${data}`);
return data.replace('Hello'.'Hi'); }));Copy the code
Skip logic
If the business is cached, it will return the cache content based on the criteria (I personally recommend using decorators for cache policy related operations) or otherwise return the value in the current interceptor, instead of directly returning the Response type. Because remember the limitations of the interface Observable
?
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable(a)export class MethodInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Method Interceptor');
return of('Nothing'); }}Copy the code
Here we need to use RxJS’s create stream operator of to create a new stream.
Timeout and exception catching
This is a very practical function. When some requests are abnormal (for example, no correct results are returned or a long wait is caused, for example, wechat callback requires a response within 5 seconds, otherwise wechat service will think that the server is abnormal and trigger the same request for many times), the timeout operator can be used. Note: Since timeouts are exceptions, RxJS simply throws an exception after the specified response time, which requires the developer to write the relevant logic. The catchError(err) operator is used with timeout: catchError(err)
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, of, throwError, TimeoutError } from 'rxjs';
import { catchError, tap, timeout } from 'rxjs/operators';
@Injectable(a)export class GlobalInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Global Interceptor');
return next.handle().pipe(
timeout(1000),
catchError((err) = > {
if (err instanceof TimeoutError) {
return of('Timeout Infomation');
}
return throwError(err);
}),
tap((data) = > {
console.log(`Result(Global):${data}`); })); }}Copy the code
Note that the catchError return also follows the excuse constraint and must construct a new stream return.