BFF與Nestjs實戰

淺談BFF

最近咱們後端夥伴開始採用了微服務架構,拆分了不少領域服務,身爲大前端的咱們確定也要作出改變,日常一個列表須要一個接口就能拿到數據,但微服務架構下就須要中間有一層專門爲前端聚合微服務架構下的n個接口,方便前端調用,因而咱們就採用了當下比較流行的BFF方式。html

bff和node沒有強綁定關係,但讓前端人員去熟悉node以外的後端語言學習成本過高,因此技術棧上咱們使用node做爲中間層,node的http框架咱們使用的是nestjs。前端

BFF做用

BFF(Backends For Frontends),就是服務於前端的後端,通過幾個項目的洗禮,我對它也有了一些看法,我認爲它主要有如下做用:node

  • 接口聚合和透傳:和上文所講的一致,聚合多個接口,方便前端調用
  • 接口數據格式化:前端頁面只負責 UI 渲染和交互,不處理複雜的數據關係,前端的代碼可讀性和可維護性會獲得改善
  • 減小人員協調成本:後端微服務和大前端bff落地而且完善後,後期部分需求只須要前端人員開發便可

適用場景

BFF雖然比較流行,但不能爲了流行而使用,要知足必定的場景而且基建很完善的狀況下才使用,不然只會增長項目維護成本和風險,收益卻很是小,我認爲的適用場景以下:ios

  • 後端有穩定的領域服務,須要聚合層
  • 需求變化頻繁,接口常常須要變更:後端有一套穩定的領域服務爲多個項目服務,變更的話成本較高,而bff層針對單一的項目,在bff層變更能夠實現最小成本的改動。
  • 有完善的基建:日誌,鏈路,服務器監控,性能監控等(必備條件)

Nestjs

本文我就以一名純前端入門後端的小白的視角來介紹一下Nestjs。nginx

Nest 是一個用於構建高效,可擴展的 Node.js 服務器端應用程序的框架git

前端發起請求後後端是怎麼作的

首先咱們發起一個GET請求github

fetch('/api/user')
    .then(res => res.json())
    .then((res) => {
    	// do some thing
    })
複製代碼

假設nginx的代理已經配置好(全部/api開頭的請求都到咱們的bff服務),後端會接收到咱們的請求,那麼問題來了,它是經過什麼接收的?web

首先咱們初始化一個Nestjs的項目,並建立user目錄,它的目錄結構以下spring

├── app.controller.ts # 控制器
├── app.module.ts # 根模塊
├── app.service.ts # 服務
├── main.ts # 項目入口,能夠選擇平臺、配置中間件等
└── src 業務模塊目錄
	├── user
    		├── user.controller.ts
    		├── user.service.ts
    		├── user.module.ts
複製代碼

Nestjs是在Controller層經過路由接收請求的,它的代碼以下:數據庫

user.controller.ts

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

@Controller('user')
export class UserController {
  @Get()
  findAll(@Req() request) {
    return [];
  }
}
複製代碼

在這裏先說明一下Nestjs的一些基礎知識 使用Nestjs完成一個基本服務須要有Module,Controller,Provider三大部分。

  • Module,字面意思是模塊,在nestjs中由@Module()修飾的class就是一個Module,在具體項目中咱們會將其做爲當前子模塊的入口,好比一個完整的項目可能會有用戶模塊,商品管理模塊,人員管理模塊等等。
  • Controller,字面意思是控制器,負責處理客戶端傳入的請求和服務端返回的響應,官方定義是一個由@Controller()修飾的類,上述代碼就是一個Controller,當咱們發起地址爲'/api/user'的get請求的時候,Controller就會定位到findAll的方法,這個方法的返回值就是前端接收到的數據。
  • Provider,字面意思是提供者,其實就是爲Controller提供服務的,官方的定義是由@Injectable()修飾的class,我簡單解釋一下:上述代碼直接在Controller層作業務邏輯處理,後續隨着業務迭代,需求愈來愈複雜,這樣的代碼會難以維護,因此須要一層來處理業務邏輯,Provider正是這一層,它須要@Injectable()修飾。

咱們再來完善一下上面的代碼,增長Provider,在當前模塊下建立user.service.ts

user.service.ts

import {Injectable} from '@nestjs/common';

@Injectable()
export class UserService {
    async findAll(req) {
        return [];
    }
}
複製代碼

而後咱們的Controller須要作一下更改

user.controller.ts

import {Controller, Get, Req} from '@nestjs/common';
import {UserService} from './user.service';

@Controller('user')
export class UserController {
    constructor( private readonly userService: UserService ) {}

  @Get()
    findAll(@Req() request) {
        return this.userService.findAll(request);
    }
}
複製代碼

這樣咱們的Controller和Provider就完成了,兩層各司其職,代碼可維護性加強。

接下來,咱們還須要將Controller和Provider注入到Module中,咱們新建一個user.module.ts文件,編寫如下內容:

user.module.ts

import {Module} from '@nestjs/common';
import {UserController} from './user.controller';
import {UserService} from './user.service';

@Module({
    controllers: [UserController],
    providers: [UserService]
})
export class UsersModule {}
複製代碼

這樣,咱們的一個業務模塊就完成了,剩下只須要將user.module.ts引入到項目總模塊注入一下,啓動項目後,訪問'/api/user'就能獲取到數據了,代碼以下:

app.module.ts

import {Module} from '@nestjs/common';
import {APP_FILTER} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {UsersModule} from './users/users.module';

@Module({
    // 引入業務模塊
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        AppService
    ]
})
export class AppModule {}
複製代碼

Nestjs經常使用模塊

經過閱讀上文咱們瞭解了跑通一個服務的流程和nestjs的接口是如何相應數據的,但還有不少細節沒有講,好比大量裝飾器(@Get,@Req等)的使用,下文將爲你們講解Nestjs經常使用的模塊

  • 基礎功能
    • Controller 控制器
    • Provider 提供者(業務邏輯)
    • Module 一個完整的業務模塊
    • NestFactory 建立 Nest 應用的工廠類
  • 高級功能
    • Middleware 中間件
    • Exception Filter 異常過濾器
    • Pipe 管道
    • Guard 守衛
    • Interceptor 攔截器

Controller、Provider、Module上文中已經提過,這裏就不進行二次講解,NestFactory其實就是用來建立一個Nestjs應用的一個工廠函數,一般在入口文件來建立,也就是上文目錄中的main.ts,代碼以下:

main.ts

import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}
bootstrap();

複製代碼

decorator 裝飾器

裝飾器是Nestjs中經常使用的功能,它內部提供了一些經常使用的請求體的裝飾器,咱們也能夠自定義裝飾器,你能夠在任何你想要的地方很方便地使用它。

除了上面這些以外,還有一些修飾class內部方法的裝飾器,最多見的就是@Get(),@Post(),@Put(),@Delete()等路由裝飾器,我相信絕大多數前端均可以看明白這些什麼意思,就再也不解釋了。

Middleware 中間件

Nestjs是對Express的二次封裝,Nestjs中的中間件等價於Express中的中間件,最經常使用的場景就是全局的日誌、跨域、錯誤處理、cookie格式化等較爲常見的api服務應用場景,官方解釋以下:

中間件函數可以訪問請求對象 (req)、響應對象 (res) 以及應用程序的請求/響應循環中的下一個中間件函數。下一個中間件函數一般由名爲 next 的變量來表示。

咱們以cookie格式化爲例,修改後的main.ts的代碼以下:

import {NestFactory} from '@nestjs/core';
import * as cookieParser from 'cookie-parser';
import {AppModule} from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    // cookie格式化中間件,通過這個中間件處理,咱們就能在req中拿到cookie對象
    app.use(cookieParser());
    await app.listen(3000);
}
bootstrap();

複製代碼

Exception Filter 異常過濾器

Nestjs內置異常層,內置的異常層負責處理整個應用程序中的全部拋出的異常。當捕獲到未處理的異常時,最終用戶將收到友好的響應。

身爲前端的咱們確定收到過接口報錯,異常過濾器就是負責拋出報錯的,一般咱們項目須要自定義報錯的格式,和前端達成一致後造成必定的接口規範。內置的異常過濾器給咱們提供的格式爲:

{
  "statusCode": 500,
  "message": "Internal server error"
}
複製代碼

通常狀況這樣的格式是不知足咱們的需求的,因此咱們須要自定義異常過濾器並綁定到全局,下面咱們先實現一個簡單的異常過濾器:

咱們在此項目的基礎上增長一個common文件夾,裏面存放一些過濾器,守衛,管道等,更新後的目錄結構以下:

├── app.controller.ts # 控制器
├── app.module.ts # 根模塊
├── app.service.ts # 服務
├── common 通用部分
├	├── filters
├	├── pipes
├	├── guards
├	├── interceptors
├── main.ts # 項目入口,能夠選擇平臺、配置中間件等
└── src 業務模塊目錄
	├── user
    		├── user.controller.ts
    		├── user.service.ts
    		├── user.module.ts
複製代碼

咱們在filters目錄下增長http-exception.filter.ts文件

http-exception.filter.ts

import {ExceptionFilter, Catch, ArgumentsHost, HttpException} from '@nestjs/common';
import {Response} from 'express';

// 須要Catch()修飾且須要繼承ExceptionFilter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    // 過濾器須要有catch(exception: T, host: ArgumentsHost)方法
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const status = exception.getStatus();
        const msg = exception.message;

        // 這裏對res的處理就是全局錯誤請求返回的格式
        response
            .status(status)
            .json({
                status: status,
                code: 1,
                msg,
                data: null
            });
    }
}
複製代碼

接下來咱們綁定到全局,咱們再次更改咱們的app.module.ts

app.module.ts

import {Module} from '@nestjs/common';
import {APP_FILTER} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {UsersModule} from './users/users.module';

@Module({
    // 引入業務模塊
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // 全局異常過濾器
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        AppService
    ]
})
export class AppModule {}
複製代碼

這樣咱們初始化的項目就有了自定義的異常處理。

Pipe 管道

這部分單從名稱上看很難理解,可是從做用和應用場景上卻很好理解,根據個人理解,管道就是在Controllor處理以前對請求數據的一些處理程序

一般管道有兩種應用場景:

  • 請求數據轉換
  • 請求數據驗證:對輸入數據進行驗證,若是驗證成功繼續傳遞; 驗證失敗則拋出異常

數據轉換應用場景很少,這裏只講一下數據驗證的例子,數據驗證是中後臺管理項目最多見的場景。

一般咱們的Nest的應用會配合class-validator來進行數據驗證,咱們在pipes目錄下新建validation.pipe.ts

validation.pipe.ts

import {PipeTransform, Injectable, ArgumentMetadata, BadRequestException} from '@nestjs/common';
import {validate} from 'class-validator';
import {plainToClass} from 'class-transformer';

// 管道須要@Injectable()修飾,可選擇繼承Nest內置管道PipeTransform
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
    // 管道必須有transform方法,這個方法有兩個參數,value :當前處理的參數, metadata:元數據
    async transform(value: any, {metatype}: ArgumentMetadata) {
        if (!metatype || !this.toValidate(metatype)) {
            return value;
        }
        const object = plainToClass(metatype, value);
        const errors = await validate(object);
        if (errors.length > 0) {
            throw new BadRequestException('Validation failed');
        }
        return value;
    }

    private toValidate(metatype: Function): boolean {
        const types: Function[] = [String, Boolean, Number, Array, Object];
        return !types.includes(metatype);
    }
}

複製代碼

而後咱們在全局綁定這個管道,修改後的app.module.ts內容以下:

import {Module} from '@nestjs/common';
import {APP_FILTER, APP_PIPE} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {ValidationPipe} from './common/pipes/validation.pipe';
import {UsersModule} from './users/users.module';

@Module({
    // 引入業務模塊
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // 全局異常過濾器
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        // 全局的數據格式驗證管道
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
        AppService
    ]
})
export class AppModule {}
複製代碼

這樣,咱們的應用程序就加入了數據校驗功能,好比咱們編寫須要數據驗證的接口,咱們須要先新建一個createUser.dto.ts的文件,內容以下:

import {IsString, IsInt} from 'class-validator';

export class CreateUserDto {
  @IsString()
  name: string;

  @IsInt()
  age: number;
}
複製代碼

而後咱們在Controller層引入,代碼以下:

user.controller.ts

import {Controller, Get, Post, Req, Body} from '@nestjs/common';
import {UserService} from './user.service';
import * as DTO from './createUser.dto';


@Controller('user')
export class UserController {
    constructor( private readonly userService: UserService ) {}

    @Get()
    findAll(@Req() request) {
        return this.userService.findAll(request);
    }

    // 在這裏添加數據校驗
    @Post()
    addUser(@Body() body: DTO.CreateUserDto) {
        return this.userService.add(body);
    }
}

複製代碼

若是客戶端傳遞過來參數不符合規範,該請求講直接拋錯,不會繼續處理。

Guard 守衛

守衛,其實就是路由守衛,就是保護咱們寫的接口的,最經常使用的場景就是接口的鑑權,一般狀況下對於一個業務系統每一個接口咱們都會有登陸鑑權,因此一般狀況下咱們會封裝一個全局的路由守衛,咱們在項目的common/guards目錄下新建auth.guard.ts,代碼以下:

auth.guard.ts

import {Injectable, CanActivate, ExecutionContext} from '@nestjs/common';
import {Observable} from 'rxjs';

function validateRequest(req) {
    return true;
}

// 守衛須要@Injectable()修飾並且須要繼承CanActivate
@Injectable()
export class AuthGuard implements CanActivate {
    // 守衛必須有canActivate方法,此方法返回值類型爲boolean
    canActivate(
        context: ExecutionContext,
    ): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();
        // 用於鑑權的函數,返回true或false
        return validateRequest(request);
    }
}

複製代碼

而後咱們將它綁定到全局module,修改後的app.module.ts內容以下:

import {Module} from '@nestjs/common';
import {APP_FILTER, APP_PIPE, APP_GUARD} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {ValidationPipe} from './common/pipes/validation.pipe';
import {AuthGuard} from './common/guards/auth.guard';
import {UsersModule} from './users/users.module';

@Module({
    // 引入業務模塊
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // 全局異常過濾器
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        // 全局的數據格式驗證管道
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
        // 全局登陸鑑權守衛
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
        AppService
    ]
})
export class AppModule {}
複製代碼

這樣,咱們的應用就多了全局守衛的功能

Interceptor 攔截器

從官方圖上能夠看出,攔截器能夠攔截請求和響應,因此又分爲請求攔截器和響應攔截器,前端目前不少流行的請求庫也有這一個功能,好比axios,umi-request等,相信前端同窗都接觸過,其實就是在客戶端和路由之間處理數據的程序。

攔截器具備一系列有用的功能,它們能夠:

  • 在函數執行以前/以後綁定額外的邏輯
  • 轉換從函數返回的結果
  • 轉換從函數拋出的異常
  • 擴展基本函數行爲
  • 根據所選條件徹底重寫函數 (例如, 緩存目的)

下面咱們實現一個響應攔截器來格式化全局響應的數據,在/common/interceptors目錄下新建res.interceptors.ts文件,內容以下:

res.interceptors.ts

import {Injectable, NestInterceptor, ExecutionContext, CallHandler} from '@nestjs/common';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

export interface Response<T> {
    code: number;
    data: T;
}

@Injectable()
export class ResInterceptor<T> implements NestInterceptor<T, Response<T>> {

    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(map(data => {
            const ctx = context.switchToHttp();
            const response = ctx.getResponse();
            response.status(200);
            const res = this.formatResponse(data) as any;
            return res;
        }));
    }

    formatResponse<T>(data: any): Response<T> {
        return {code: 0, data};
    }
}
複製代碼

這個響應守衛的做用就是將咱們的接口返回數據格式化成{code, data}的格式,接下來咱們須要將這個守衛綁定到全局,修改後的app.module.ts內容以下:

import {Module} from '@nestjs/common';
import {APP_FILTER, APP_PIPE, APP_GUARD, APP_INTERCEPTOR} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {ValidationPipe} from './common/pipes/validation.pipe';
import {AuthGuard} from './common/guards/auth.guard';
import {ResInterceptor} from './common/interceptors/res.interceptors';
import {UsersModule} from './users/users.module';

@Module({
    // 引入業務模塊
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // 全局異常過濾器
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        // 全局的數據格式驗證管道
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
        // 全局登陸鑑權守衛
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
        // 全局響應攔截器
        {
            provide: APP_INTERCEPTOR,
            useClass: ResInterceptor,
        },
        AppService
    ]
})
export class AppModule {}

複製代碼

這樣,咱們這個應用的全部接口的響應格式都固定了。

Nestjs小總結

通過上文的一系列步驟,咱們已經搭建了一個小應用(沒有日誌和數據源),那麼問題來了,前端發起請求後咱們實現的應用內部是如何一步步處理而且響應數據的?步驟以下:

客戶端請求 -> Middleware 中間件 -> Guard 守衛 -> 請求攔截器(咱們這沒有)-> Pipe 管道 -> Controllor層的路由處理函數 -> 響應攔截器 -> 客戶端響應

其中Controllor層的路由處理函數會調用Provider,Provider負責獲取底層數據並處理業務邏輯;異常過濾器會在這個程序拋錯後執行。

總結

通過上文咱們能夠對BFF層的概念有一個基本的瞭解,而且按照步驟能夠本身搭建一個Nestjs小應用,但和企業級應用差距還很大。

企業級應用還須要接入數據源(後端接口數據、數據庫數據、apollo配置數據)、日誌、鏈路、緩存、監控等必不可少的功能。

  • 接BFF層須要有完善的基建和合適的業務場景,不要盲目接入
  • Nestjs基於Express實現,參考了springboot的設計思想,入門很簡單,精通須要理解其原理,尤爲是依賴注入的設計思想

參考文獻

相關文章
相關標籤/搜索