最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • BFF与Nestjs入门

    正文概述 掘金(wxz在掘金)   2021-02-04   970

    浅谈BFF

    最近我们后端伙伴开始采用了微服务架构,拆分了很多领域服务,身为大前端的我们肯定也要做出改变,平常一个列表需要一个接口就能拿到数据,但微服务架构下就需要中间有一层专门为前端聚合微服务架构下的n个接口,方便前端调用,于是我们就采用了当下比较流行的BFF方式。

    bff和node没有强绑定关系,但让前端人员去熟悉node之外的后端语言学习成本太高,所以技术栈上我们使用node作为中间层,node的http框架我们使用的是nestjs。

    BFF作用

    BFF(Backends For Frontends),就是服务于前端的后端,经过几个项目的洗礼,我对它也有了一些见解,我认为它主要有以下作用:

    • 接口聚合和透传:和上文所讲的一致,聚合多个接口,方便前端调用
    • 接口数据格式化:前端页面只负责 UI 渲染和交互,不处理复杂的数据关系,前端的代码可读性和可维护性会得到改善
    • 减少人员协调成本:后端微服务和大前端bff落地并且完善后,后期部分需求只需要前端人员开发即可

    适用场景

    BFF虽然比较流行,但不能为了流行而使用,要满足一定的场景并且基建很完善的情况下才使用,否则只会增加项目维护成本和风险,收益却非常小,我认为的适用场景如下:

    • 后端有稳定的领域服务,需要聚合层
    • 需求变化频繁,接口经常需要变动:后端有一套稳定的领域服务为多个项目服务,变动的话成本较高,而bff层针对单一的项目,在bff层变动可以实现最小成本的改动。
    • 有完善的基建:日志,链路,服务器监控,性能监控等(必备条件)

    Nestjs

    本我我就以一名纯前端入门后端的小白的视角来介绍一下Nestjs。

    前端发起请求后后端是怎么做的

    首先我们发起一个GET请求

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

    假设nginx的代理已经配置好(所有/api开头的请求都到我们的bff服务),后端会接收到我们的请求,那么问题来了,它是通过什么接收的?

    首先我们初始化一个Nestjs的项目,并创建user目录,它的目录结构如下

    ├── 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 CatsController {
      @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.ts'
    
    @Controller('user')
    export class CatsController {
      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.ts'
    
    @Module({
        controllers: [UserController],
        providers: [UserService]
    })
    export default class UserModule {}
    

    这样,我们的一个业务模块就完成了,剩下只需要将user.module.ts引入到项目总模块注入一下,启动项目后,访问'/api/user'就能获取到数据了,代码如下:

    app.module.ts

    import {Module} from '@nestjs/common';
    import {AppController} from './app.controller';
    import {AppService} from './app.service';
    import UserModule from './modules/user/user.module';
    
    @Module({
      imports: [UserModule], // 引入业务模块
      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() {
        // 使用NestFactory创建应用
        const app = await NestFactory.create(AppModule);
        await app.listen(3000);
    }
    bootstrap();
    

    装饰器

    装饰器是Nestjs中常用的功能,它内部提供了一些常用的请求体的装饰器,我们也可以自定义装饰器,你可以在任何你想要的地方很方便地使用它。

    BFF与Nestjs入门

    除了上面这些之外,还有一些修饰class内部方法的装饰器,最常见的就是@Get(),@Post(),@Put(),@Delete()等,这些用来修饰Contollor内部的方法,我相信绝大多数前端都可以看明白这些什么意思,就不再解释了。

    Middleware 中间件

    Nestjs是对Express的二次封装,Nestjs中的中间件等价于Express中的中间件,最常用的场景就是全局的日志、跨域、错误处理、cookie格式化等较为常见的api服务应用场景,官方解释如下:

    我们以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 异常过滤器

    BFF与Nestjs入门 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 {Request, 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 request = ctx.getRequest<Request>();
        const status = exception.getStatus();
    
        // 这里对res的处理就是全局错误请求返回的格式
        response
          .status(status)
          .json({
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
          });
      }
    }
    

    接下来我们绑定到全局,我们再次更改我们的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.ts'
    import UserModule from './modules/user/user.module';
    
    @Module({
      imports: [UserModule], // 引入业务模块
      controllers: [AppController],
      providers: [
          {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
          },
          AppService
      ]
    })
    export class AppModule {}
    

    这样我们初始化的项目就有了自定义的异常处理。

    Pipe 管道

    BFF与Nestjs入门

    这部分单从名称上看很难理解,但是从作用和应用场景上却很好理解,根据我的理解,管道就是在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.ts'
    import ValidationPipe from './common/pipes/validation.pipe.ts'
    import UserModule from './modules/user/user.module';
    
    @Module({
      imports: [UserModule], // 引入业务模块
      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;
    
      @IsString()
      breed: string;
    }
    

    然后我们在Controller层引入,代码如下:

    user.controller.ts

    import {Controller, Get, Req} from '@nestjs/common';
    import {UserService} from './user.service'
    import * as DTO from './createUser.dto';
    
    @Controller('user')
    export class CatsController {
      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 守卫

    BFF与Nestjs入门

    守卫,其实就是路由守卫,就是保护我们写的接口的,最常用的场景就是接口的鉴权,通常情况下对于一个业务系统每个接口我们都会有登录鉴权,所以通常情况下我们会封装一个全局的路由守卫,我们在项目的common/guards目录下新建auth.guard.ts,代码如下:

    auth.guard.ts

    import {Injectable, CanActivate, ExecutionContext} from '@nestjs/common';
    import {Observable} from 'rxjs';
    
    // 守卫需要@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_GUARD, 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 RolesGuard from './common/guards/auth.guard'
    import UserModule from './modules/user/user.module';
    
    @Module({
      imports: [UserModule], // 引入业务模块
      controllers: [AppController],
      providers: [
          // 全局异常处理
          {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
          },
          // 全局的数据格式验证管道
          {
          	provide: APP_PIPE,
          	useClass: ValidationPipe,
          },
          // 全局登录鉴权守卫
          {
            provide: APP_GUARD,
            useClass: RolesGuard,
          },
          AppService
      ]
    })
    export class AppModule {}
    

    这样,我们的应用就多了全局守卫的功能

    Interceptor 拦截器

    BFF与Nestjs入门

    从官方图上可以看出,拦截器可以拦截请求和响应,所以又分为请求拦截器和响应拦截器,前端目前很多流行的请求库也有这一个功能,比如axios,umi-request等,相信前端同学都接触过,其实就是在客户端和路由之间处理数据的程序。

    拦截器具有一系列有用的功能,它们可以:

    • 在函数执行之前/之后绑定额外的逻辑
    • 转换从函数返回的结果
    • 转换从函数抛出的异常
    • 扩展基本函数行为
    • 根据所选条件完全重写函数 (例如, 缓存目的)

    下面我们实现一个响应拦截器来格式化全局响应的数据,在/common/interceptors目录下新建res.interceptors.ts文件,内容如下:

    res.interceptors.ts

    import {Injectable, NestInterceptor, ExecutionContext, CallHandler, Optional} from '@nestjs/common';
    import {Observable} from 'rxjs';
    import {map} from 'rxjs/operators';
    
    export interface Response<T> {
        code: number;
        data: T;
    }
    
    @Injectable()
    export class TransformInterceptor<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 request = ctx.getRequest();
                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_INTERCEPTOR, APP_GUARD, 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 RolesGuard from './common/guards/auth.guard';
    import ResInterceptor from './common/interceptors/res.interceptor';
    import UserModule from './modules/user/user.module';
    
    @Module({
      imports: [UserModule], // 引入业务模块
      controllers: [AppController],
      providers: [
          // 全局异常处理
          {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
          },
          // 全局的数据格式验证管道
          {
          	provide: APP_PIPE,
          	useClass: ValidationPipe,
          },
          // 全局登录鉴权守卫
          {
            provide: APP_GUARD,
            useClass: RolesGuard,
          },
          // 全局响应拦截器
          {
            provide: APP_INTERCEPTOR,
            useClass: ResInterceptor,
          },
          AppService
      ]
    })
    export class AppModule {}
    

    这样,我们这个应用的所有接口的响应格式都固定了。

    Nestjs小总结

    经过上文的一系列步骤,我们已经搭建了一个小应用(没有日志和数据源),那么问题来了,前端发起请求后我们实现的应用内部是如何一步步处理并且响应数据的?步骤如下:

    其中Controllor层的路由处理函数会调用Provider,Provider负责获取底层数据并处理业务逻辑。

    总结

    经过上文我们可以对BFF层的概念有一个基本的了解,并且按照步骤可以自己搭建一个Nestjs小应用,但和企业级应用差距还很大。

    • 接BFF层需要有完善的基建和合适的业务场景,不要盲目接入
    • Nestjs基于Express实现,参考了springboot的设计思想,入门很简单,精通需要理解其原理,尤其是依赖注入的思想

    参考文献

    • 我理解的BFF
    • NestJs官方文档

    起源地下载网 » BFF与Nestjs入门

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元