從Express到Nestjs,談談Nestjs的設計思想和使用方法

yuxiaoliang發表於2019-02-16

  最近已經使用過一段時間的nestjs,讓人寫著有一種java spring的感覺,nestjs可以使用express的所有中介軟體,此外完美的支援typescript,與資料庫關係對映typeorm配合使用可以快速的編寫一個介面閘道器。本文會介紹一下作為一款企業級的node框架的特點和優點。

  • 從依賴注入(DI)談起
  • 裝飾器和註解
  • nestjs的“洋蔥模型”
  • nestjs的特點總結

原文在我的部落格中: github.com/fortheallli…

歡迎star和fork

一、從依賴注入(DI)談起

(1)、angular中的依賴注入

  從angular1.x開始,實現了依賴注入或者說控制反轉的模式,angular1.x中就有controller(控制器)、service(服務),模組(module)。筆者在早年間寫過一段時間的angular1.3,下面舉例來說明:

var myapp=angular.module('myapp',['ui.router']);
myapp.controller('test1',function($scope,$timeout){}
myapp.controller('test2',function($scope,$state){}

複製程式碼

  上面這個就是angular1.3中的一個依賴注入的例子,首先定義了模組名為“myapp”的module, 接著在myapp這個模組中定義controller控制器。將myapp模組的控制權交給了myapp.controller函式。具體的依賴注入的流程圖如下所示:

default

myapp這個模組如何定義,由於它的兩個控制器決定,此外在控制器中又依賴於scope、timeout等服務。這樣就實現了依賴注入,或者說控制反轉。

(2)、什麼是依賴注入

  用一個例子來通俗的講講什麼是依賴注入。

class Cat{

}
class Tiger{

}
class Zoo{
  constructor(){
     this.tiger = new Tiger();
     this.cat = new Cat();
  }
}


複製程式碼

  上述的例子中,我們定義Zoo,在其constructor的方法中進行對於Cat和Tiger的例項化,此時如果我們要為Zoo增加一個例項變數,比如去修改Zoo類本身,比如我們現在想為Zoo類增加一個Fish類的例項變數:

class Fish{}

class Zoo{
  constructor(){
     this.tiger = new Tiger();
     this.cat = new Cat();
     this.fish = new Fish();
  }
}

複製程式碼

  此外如果我們要修改在Zoo中例項化時,傳入Tiger和Cat類的變數,也必須在Zoo類上修改。這種反反覆覆的修改會使得Zoo類並沒有通用性,使得Zoo類的功能需要反覆測試。

我們設想將例項化的過程以引數的形式傳遞給Zoo類:

class Zoo{
  constructor(options){
     this.options = options;
  }
}
var zoo = new Zoo({
  tiger: new Tiger(),
  cat: new Cat(),
  fish: new Fish()
})
複製程式碼

  我們將實力化的過程放入引數中,傳入給Zoo的建構函式,這樣我們就不用在Zoo類中反覆的去修改程式碼。這是一個簡單的介紹依賴注入的例子,更為完全使用依賴注入的可以為Zoo類增加靜態方法和靜態屬性:

class Zoo{
  static animals = [];
  constructor(options){
     this.options = options;
     this.init();
  }
  init(){
    let _this = this;
    animals.forEach(function(item){
      item.call(_this,options);
    })
  }
  static use(module){
     animals.push([...module])
  }
}
Zoo.use[Cat,Tiger,Fish];
var zoo = new Zoo(options);

複製程式碼

  上述我們用Zoo的靜態方法use往Zoo類中注入Cat、Tiger、Fish模組,將Zoo的具體實現移交給了Cat和Tiger和Fish模組,以及建構函式中傳入的options引數。

(3)、nestjs中的依賴注入

  在nestjs中也參考了angular中的依賴注入的思想,也是用module、controller和service。

@Module({
  imports:[otherModule],
  providers:[SaveService],
  controllers:[SaveController,SaveExtroController]
})
export class SaveModule {}
複製程式碼

  上面就是nestjs中如何定一個module,在imports屬性中可以注入其他模組,在prividers注入相應的在控制器中需要用到的service,在控制器中注入需要的controller。

二、裝飾器和註解

  在nestjs中,完美的擁抱了typescript,特別是大量的使用裝飾器和註解,對於裝飾器和註解的理解可以參考我的這篇文章:Typescript中的裝飾器和註解。我們來看使用了裝飾器和註解後,在nestjs中編寫業務程式碼有多麼的簡潔:

import { Controller, Get, Req, Res } from '@nestjs/common';

@Controller('cats')

export class CatsController {
  @Get()
  findAll(@Req() req,@Res() res) {
    return 'This action returns all cats';
  }
}
複製程式碼

  上述定義兩個一個處理url為“/cats”的控制器,對於這個路由的get方法,定義了findAll函式。當以get方法,請求/cats的時候,就會主動的觸發findAll函式。

  此外在findAll函式中,通過req和res引數,在主題內也可以直接使用請求request以及對於請求的響應response。比如我們通過req上來獲取請求的引數,以及通過res.send來返回請求結果。

三、nestjs的“洋蔥模型”

  這裡簡單講講在nestjs中是如何分層的,也就是說請求到達服務端後如何層層處理,直到響應請求並將結果返回客戶端。

在nestjs中在service的基礎上,按處理的層次補充了中介軟體(middleware)、異常處理(Exception filters)、管道(Pipes),守衛(Guards),以及攔截器(interceptors)在請求到打真正的處理函式之間進行了層層的處理。

default

上圖中的邏輯就是分層處理的過程,經過分層的處理請求才能到達服務端處理函式,下面我們來介紹nestjs中的層層模型的具體作用。

(1)、middleware中介軟體

  在nestjs中的middle完全跟express的中介軟體一摸一樣。不僅如此,我們還可以直接使用express中的中介軟體,比如在我的應用中需要處理core跨域:

import * as cors from 'cors';
async function bootstrap() {
  onst app = await NestFactory.create(/* 建立app的業務邏輯*/)
  app.use(cors({
    origin:'http://localhost:8080',
    credentials:true
  }));
  await app.listen(3000)
}
bootstrap();

複製程式碼

在上述的程式碼中我們可以直接通過app.use來使用core這個express中的中介軟體。從而使得server端支援core跨域等。

初此之外,跟nestjs的中介軟體也完全保留了express中的中介軟體的特點:

  • 在中介軟體中接受response和request作為引數,並且可以修改請求物件request和結果返回物件response
  • 可以結束對於請求的處理,直接將請求的結果返回,也就是說可以在中介軟體中直接res.send等。
  • 在該中介軟體處理完畢後,如果沒有將請求結果返回,那麼可以通過next方法,將中介軟體傳遞給下一個中介軟體處理。

在nestjs中,中介軟體跟express中完全一樣,除了可以複用express中介軟體外,在nestjs中針對某一個特定的路由來使用中介軟體也十分的方便:

class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}
複製程式碼

上面就是對於特定的路由url為/cats的時候,使用LoggerMiddleware中介軟體。

(2)、Exception filters異常過濾器

  Exception filters異常過濾器可以捕獲在後端接受處理任何階段所跑出的異常,捕獲到異常後,然後返回處理過的異常結果給客戶端(比如返回錯誤碼,錯誤提示資訊等等)。

  我們可以自定義一個異常過濾器,並且在這個異常過濾器中可以指定需要捕獲哪些異常,並且對於這些異常應該返回什麼結果等,舉例一個自定義過濾器用於捕獲HttpException異常的例子。

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}
複製程式碼

  我們可以看到host是實現了ArgumentsHost介面的,在host中可以獲取執行環境中的資訊,如果在http請求中那麼可以獲取request和response,如果在socket中也可以獲取client和data資訊。

  同樣的,對於異常過濾器,我們可以指定在某一個模組中使用,或者指定其在全域性使用等。

(3)、Pipes管道

  Pipes一般使用者驗證請求中引數是否符合要求,起到一個校驗引數的功能。

  比如我們對於一個請求中的某些引數,需要校驗或者轉化引數的型別:

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed');
    }
    return val;
  }
}

複製程式碼

  上述的ParseIntPipe就可以把引數轉化成十進位制的整型數字。我們可以這樣使用:


@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
  return await this.catsService.findOne(id);
}

複製程式碼

  對於get請求中的引數id,呼叫new ParseIntPipe方法來將id引數轉化成十進位制的整數。

(4)、Guards守衛

  Guards守衛,其作用就是決定一個請求是否應該被處理函式接受並處理,當然我們也可以在middleware中介軟體中來做請求的接受與否的處理,與middleware相比,Guards可以獲得更加詳細的關於請求的執行上下文資訊。

通常Guards守衛層,位於middleware之後,請求正式被處理函式處理之前。

下面是一個Guards的例子:

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

複製程式碼

這裡的context實現了一個ExecutionContext介面,該介面中具有豐富的執行上下文資訊。


export interface ArgumentsHost {
  getArgs<T extends Array<any> = any[]>(): T;
  getArgByIndex<T = any>(index: number): T;
  switchToRpc(): RpcArgumentsHost;
  switchToHttp(): HttpArgumentsHost;
  switchToWs(): WsArgumentsHost;
}

export interface ExecutionContext extends ArgumentsHost {
  getClass<T = any>(): Type<T>;
  getHandler(): Function;
}

複製程式碼

除了ArgumentsHost中的資訊外,ExecutionContext還包含了getClass使用者獲取對於某一個路由處理的,控制器。而getClass用於獲取返回對於指定路由後臺處理時的處理函式。

對於Guards處理函式,如果返回true,那麼請求會被正常的處理,如果返回false那麼請求會丟擲異常。

(5)、interceptors攔截器

   攔截器可以給每一個需要執行的函式繫結,攔截器將在該函式執行前或者執行後執行。可以轉換函式執行後返回的結果等。

概括來說:

interceptors攔截器在函式執行前或者執行後可以執行,如果在執行後執行,可以攔截函式執行的返回結果,修改引數等。

再來舉一個超時處理的例子:

@Injectable()
export class TimeoutInterceptor implements NestInterceptor{
  intercept(
    context:ExecutionContext,
    call$:Observable<any>
  ):Observable<any>{
    return call$.pipe(timeout(5000));
  }
}
複製程式碼

該攔截器可以定義在控制器上,可以處理超時請求。

四、nestjs的特點總結

  最後總結一下nestjs的優缺。

nestjs的優點:

  • 完美的支援typescript,因此可以使用日益繁榮的ts生態工具

  • 相容express中介軟體,因為express是最早出現的輕量級的node server端框架,nestjs能夠利用所有express的中介軟體,使其生態完善

  • 層層處理,一定程度上可以約束程式碼,比如何時使用中介軟體、何時需要使用guards守衛等。

  • 依賴注入以及模組化的思想,提供了完整的mvc的鏈路,使得程式碼結構清晰,便於維護,這裡的m是資料層可以通過modules的形式注入,比如通過typeorm的entity就可以在模組中注入modules。

  • 完美支援rxjs

相關文章