最近咱们后端伙伴开始采用了微服务架构,拆分了不少领域服务,身为大前端的咱们确定也要作出改变,日常一个列表须要一个接口就能拿到数据,但微服务架构下就须要中间有一层专门为前端聚合微服务架构下的n个接口,方便前端调用,因而咱们就采用了当下比较流行的BFF方式。html
bff和node没有强绑定关系,但让前端人员去熟悉node以外的后端语言学习成本过高,因此技术栈上咱们使用node做为中间层,node的http框架咱们使用的是nestjs。前端
BFF(Backends For Frontends),就是服务于前端的后端,通过几个项目的洗礼,我对它也有了一些看法,我认为它主要有如下做用:node
BFF虽然比较流行,但不能为了流行而使用,要知足必定的场景而且基建很完善的状况下才使用,不然只会增长项目维护成本和风险,收益却很是小,我认为的适用场景以下:ios
本文我就以一名纯前端入门后端的小白的视角来介绍一下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的接口是如何相应数据的,但还有不少细节没有讲,好比大量装饰器(@Get
,@Req
等)的使用,下文将为你们讲解Nestjs经常使用的模块
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();
复制代码
装饰器是Nestjs中经常使用的功能,它内部提供了一些经常使用的请求体的装饰器,咱们也能够自定义装饰器,你能够在任何你想要的地方很方便地使用它。
除了上面这些以外,还有一些修饰class内部方法的装饰器,最多见的就是@Get()
,@Post()
,@Put()
,@Delete()
等路由装饰器,我相信绝大多数前端均可以看明白这些什么意思,就再也不解释了。
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();
复制代码
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 {}
复制代码
这样咱们初始化的项目就有了自定义的异常处理。
这部分单从名称上看很难理解,可是从做用和应用场景上却很好理解,根据个人理解,管道就是在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);
}
}
复制代码
若是客户端传递过来参数不符合规范,该请求讲直接抛错,不会继续处理。
守卫,其实就是路由守卫,就是保护咱们写的接口的,最经常使用的场景就是接口的鉴权,一般状况下对于一个业务系统每一个接口咱们都会有登陆鉴权,因此一般状况下咱们会封装一个全局的路由守卫,咱们在项目的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 {}
复制代码
这样,咱们的应用就多了全局守卫的功能
从官方图上能够看出,拦截器能够拦截请求和响应,因此又分为请求拦截器和响应拦截器,前端目前不少流行的请求库也有这一个功能,好比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 {}
复制代码
这样,咱们这个应用的全部接口的响应格式都固定了。
通过上文的一系列步骤,咱们已经搭建了一个小应用(没有日志和数据源),那么问题来了,前端发起请求后咱们实现的应用内部是如何一步步处理而且响应数据的?步骤以下:
客户端请求 -> Middleware 中间件 -> Guard 守卫 -> 请求拦截器(咱们这没有)-> Pipe 管道 -> Controllor层的路由处理函数 -> 响应拦截器 -> 客户端响应
其中Controllor层的路由处理函数会调用Provider,Provider负责获取底层数据并处理业务逻辑;异常过滤器会在这个程序抛错后执行。
通过上文咱们能够对BFF层的概念有一个基本的了解,而且按照步骤能够本身搭建一个Nestjs小应用,但和企业级应用差距还很大。
企业级应用还须要接入数据源(后端接口数据、数据库数据、apollo配置数据)、日志、链路、缓存、监控等必不可少的功能。