從Nest到Nesk -- 模組化Node框架的實踐

iKcamp發表於2018-04-02

文: 達孚(滬江Web前端架構師)

本文原創,轉至滬江技術

首先上一下專案地址(:>):

Nest:github.com/nestjs/nest

Nesk:github.com/kyoko-df/ne…

Nest初認識

Nest是一個深受angular激發的基於express的node框架,按照官網說明是一個旨在提供一個開箱即用的應用程式體系結構,允許輕鬆建立高度可測試,可擴充套件,鬆散耦合且易於維護的應用程式。

在設計層面雖然說是深受angular激發,但其實從後端開發角度來說類似於大家熟悉的Java Spring架構,使用了大量切面程式設計技巧,再通過裝飾器的結合完全了關注上的分離。同時使用了Typescript(也支援Javascript)為主要開發語言,更保證了整個後端系統的健壯性。

強大的Nest架構

那首先為什麼需要Nest框架,我們從去年開始大規模使用Node來替代原有的後端View層開發,給予了前端開發除了SPA以外的前後端分離方式。早期Node層的工作很簡單-渲染頁面代理介面,但在漸漸使用中大家會給Node層更多的寄託,尤其是一些內部專案中,你讓後端還要將一些現有的SOA介面進行包裝,對方往往是不願意的。那麼我們勢必要在Node層承接更多的業務,包括不限於對資料的組合包裝,對請求的許可權校驗,對請求資料的validate等等,早期我們的框架是最傳統的MVC架構,但是我們翻閱業務程式碼,往往最後變成複雜且很難維護的Controller層程式碼(從許可權校驗到頁面渲染一把擼到底:))。

那麼我們現在看看Nest可以做什麼?從一個最簡單的官方例子開始看:

async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
複製程式碼

這裡就啟動了一個nest例項,先不看這個ValidationPipe,看ApplicationModule的內容:

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewaresConsumer): void {
    consumer
      .apply(LoggerMiddleware)
      .with('ApplicationModule')
      .forRoutes(CatsController);
  }
}
複製程式碼
@Module({
  controllers: [CatsController],
  components: [CatsService],
})
export class CatsModule {}
複製程式碼

這裡看到nest的第一層入口module,也就是模組化開發的根本,所有的controller,component等等都可以根據業務切分到某個模組,然後模組之間還可以巢狀,成為一個完整的體系,借用張nest官方的圖:

從Nest到Nesk -- 模組化Node框架的實踐

在nest中的component概念其實一切可以注入的物件,對於依賴注入這個概念在此不做深入解釋,可以理解為開發者不需要例項化類,框架會進行例項化且儲存為單例供使用。

@Controller('cats')
@UseGuards(RolesGuard)
@UseInterceptors(LoggingInterceptor, TransformInterceptor)
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  @Roles('admin')
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }

  @Get(':id')
  findOne(
    @Param('id', new ParseIntPipe())
    id,
  ): Promise<Cat> {
    return this.catsService.findOne(id);
  }
}
複製程式碼

Controller的程式碼非常精簡,很多重複的工作都通過guards和interceptors解決,第一個裝飾器Controller可以接受一個字串引數,即為路由引數,也就是這個Controller會負責/cats路由下的所有處理。首先RolesGuard會進行許可權校驗,這個校驗是自己實現的,大致結構如下:

@Guard()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(request, context: ExecutionContext): boolean {
    const { parent, handler } = context;
    const roles = this.reflector.get<string[]>('roles', handler);
    if (!roles) {
      return true;
    }

    // 自行實現
  }
}
複製程式碼

context可以獲取controller的相關資訊,再通過反射拿到handler上是否有定義roles的元資訊,如果有就可以在邏輯里根據自己實現的auth方法或者使用者型別來決定是否讓使用者訪問相關handler。

interceptors即攔截器,它可以:

  • 在方法執行之前/之後繫結額外的邏輯
  • 轉換從函式返回的結果
  • 轉換從函式丟擲的異常
  • 根據所選條件完全重寫函式 (例如, 快取目的)

本示例有兩個攔截器一個用來記錄函式執行的時間,另一個對結果進行一層包裝,這兩個需求都是開發中很常見的需求,而且攔截器會提供一個rxjs的觀察者流來處理函式返回,支援非同步函式,我們可以通過map()來mutate這個流的結果,可以通過do運算子來觀察函式觀察序列的執行狀況,另外可以通過不返回流的方式,從而阻止函式的執行,LoggingInterceptor例子如下:

@Interceptor()
export class LoggingInterceptor implements NestInterceptor {
  intercept(dataOrRequest, context: ExecutionContext, stream$: Observable<any>): Observable<any> {
    console.log('Before...');
    const now = Date.now();

    return stream$.do(
      () => console.log(`After... ${Date.now() - now}ms`),
    );
  }
}
複製程式碼

回到最初的ValidationPipe,它是一個強大的校驗工具,我們看到前面的controller程式碼中插入操作中有一個CreateCatDto,dto是一種資料傳輸物件,一個dto可以這樣定義:

export class CreateCatDto {
  @IsString() readonly name: string;

  @IsInt() readonly age: number;

  @IsString() readonly breed: string;
}
複製程式碼

然後ValidationPipe會檢查body是否符合這個dto,如果不符合就會就會執行你在pipe中設定的處理方案。具體是如何實現的可以再寫一篇文章了,所以我推薦你看nest中文指南(順便感謝翻譯的同學們)

示例的完整程式碼可以看01-cats-app

也就是說業務團隊中的熟練工或者架構師可以開發大量的模組,中介軟體,異常過濾器,管道,看守器,攔截器等等,而不太熟練的開發者只需要完成controller的開發,在controller上像搭積木般使用這些設施,即完成了對業務的完整搭建。

Nesk-一個落地方案的嘗試

雖然我個人很喜歡Nest,但是我們公司已經有一套基於koa2的成熟框架Aconite,而Nest是基於express的,檢視了下Nest的原始碼,對express有一定的依賴,但是koa2和express在都支援async語法後,差異屬於可控範圍下。另外nest接受一個express的例項,在nesk中我們只需要調整為koa例項,那麼也可以是繼承於koa的任何專案例項,我們的框架在2.0版本也是一個在koa上繼承下來的node框架,基於此,我們只需要一個簡單的adapter層就可以無縫接入Aconite到nesk中,這樣減少了nesk和內部服務的捆綁,而將所有的公共內部服務整合保留在Aconite中。Nest對於我們來說只是一個更完美的開發正規化,不承接任何公共模組。

所以我們需要的工作可以簡單總結為:

  1. 支援Koa
  2. 適配Aconite

支援Koa我們在Nest的基礎上做了一些小改動完成了Nesk來相容Koa體系。我們只需要完成Nesk和Aconite中間的Adapter層,就可以完成Nesk的落地,最後啟動處的程式碼變成:

import { NeskFactory } from '@neskjs/core';
import { NeskAconite } from '@hujiang/nesk-aconite';
import { ApplicationModule } from './app.module';
import { config } from './common/config';
import { middwares } from './common/middlware';

async function bootstrap() {
  const server = new NeskAconite({
    projectRoot: __dirname,
    middlewares,
    config
  });
  const app = await NeskFactory.create(ApplicationModule, server);
  await app.listen(config.port);
}
複製程式碼

最後Nest有很多@nest scope下的包,方便一些工具接入nest,如果他們與express沒有關係,我們其實是可以直接使用的。但是包內部往往依賴@nest/common或者@nesk/core,這裡可以使用module-alias,進行一個重指向(你可以嘗試下graphql的例子):

"_moduleAliases": {
  "@nestjs/common": "node_modules/@neskjs/common",
  "@nestjs/core": "node_modules/@neskjs/core"
}
複製程式碼

Nesk的地址Nesk,我們對Nesk做了基本流程測試目前覆蓋了common和core,其它的在等待改進,歡迎一切願意一起改動的開發者。

不足與期待

其實從一個更好的方面來說,我們應當允許nest接受不同的底層框架,即既可以使用express,也可以使用koa,通過一個adapter層抹平差異。不過這一塊的改造成本會大一些。

另一方面nest有一些本身的不足,在依賴注入上,還是選擇了ReflectiveInjector,而Angular已經開始使用了StaticInjector,理論上StaticInjector減少了對Map層級的查詢,有更好的效能,這也是我們決定分叉出一個nesk的原因,可以做一些更大膽的內部程式碼修改。另外angular的依賴注入更強大,有例如useFactory和deps等方便測試替換的功能,是需要nest補充的.

最後所有的基於Koa的框架都會問到一個問題,能不能相容eggjs(:)),其實無論是Nest還是Nesk都是一個強制開發規範的框架,只要eggjs還建立在koa的基礎上,就可以完成整合,只是eggjs在啟動層面的改動較大,而且開發正規化和nest差異比較多,兩者的融合並沒有顯著的優勢。

總之Node作為一個比較靈活的後端開發方式,每個人心中都有自己覺得合適的開發正規化,如果你喜歡這種方式,不妨嘗試下Nest或者Nesk。

從Nest到Nesk -- 模組化Node框架的實踐


從Nest到Nesk -- 模組化Node框架的實踐

2019年,iKcamp原創新書《Koa與Node.js開發實戰》已在京東、天貓、亞馬遜、噹噹開售啦!

相關文章