qiansc / progressive

Progressive Learning
0 stars 0 forks source link

NestJS官方教程(完结) #5

Open qiansc opened 5 years ago

qiansc commented 5 years ago

NestJS的官方教程起点有一定高度,如果没有扎实的NG、Express经验前,需要对一些概念进行了解。

服务与依赖注入 Provider Dependency Injection

服务是一个广义的概念,它包括应用所需的任何值、函数或特性。狭义的服务是一个明确定义了用途的类。它应该做一些具体的事,并做好。

Nest 把控制器和服务区分开,以提高模块性和复用性。

通过把控制器中和逻辑有关的功能与其他类型的处理分离开,你可以让控制器类更加精简、高效。 理想情况下,控制器的工作只管申明装饰器和响应数据,而不用顾及其它。 它应该提供请求和响应桥梁,以便作为视图(由模板渲染)和应用逻辑(通常包含一些模型的概念)的中介者。 控制器不需要定义任何诸如从客户端获取数据、验证用户输入或直接往控制台中写日志等工作。 而要把这些任务委托给各种服务。通过把各种处理任务定义到可注入的服务类中,你可以让它可以被任何控制器使用。 通过在不同的环境中注入同一种服务的不同提供商,你还可以让你的应用更具适应性。 Nest 不会强制遵循这些原则。它只会通过依赖注入让你能更容易地将应用逻辑分解为服务,并让这些服务可用于各个控制器中。 控制器是服务的消费者,也就是说,你可以把一个服务注入到控制器中,让控制器类得以访问该服务类。 那么服务就是提供者,基本上,几乎所有事情都可以看作是提供者—服务、存储库、工厂、助手等等。它们都可以通过构造函数注入依赖关系,这意味着它们可以彼此创建各种关系。 在 Nest 中,要把一个类定义为服务,就要用 @Injectable 装饰器来提供元数据,以便让 Nest 可以把它作为依赖注入到控制器中。 同样,也要使用 @Injectable 装饰器来表明一个控制器或其它类(比如另一个服务、模块等)拥有一个依赖。 依赖并不必然是服务,它也可能是函数或值等等。 依赖注入(通常简称 DI)被引入到 Nest 框架中,并且到处使用它,来为新建的控制器提供所需的服务或其它东西。 注入器是主要的机制。你不用自己创建 Nest 注入器。Nest 会在启动过程中为你创建全应用级注入器。 该注入器维护一个包含它已创建的依赖实例的容器,并尽可能复用它们。 提供者是创建依赖项的配方。对于服务来说,它通常就是这个服务类本身。你在应用中要用到的任何类都必须使用该应用的注入器注册一个提供商,以便注入器可以使用它来创建新实例。

作者:jiayisheji 链接:https://www.jianshu.com/p/f0a4944e8fb9

客户端请求 ---> 中间件 ---> 守卫 ---> 拦截器之前 ---> 管道 ---> 控制器处理并响应 ---> 拦截器之后 ---> 过滤器

qiansc commented 5 years ago

Part.概述.1 firststeps

起步很简单,以及所有官方示例的demo托管在Github

Part.概述.2 controllers

controllers有两种操作响应的方式:基于装饰器及基于类库方法。

按装饰器类型操作响应

可以限定匹配路由规则如@Get('/s') @Delete(':id') @Get('music***land')等形式

@HttpCode(200) 可以指定返回的状态码

@Header('Cache-Control', 'none') 定制Headers其他参数

装饰器 参数
@Request() req
@Response() res
@Next() next
@Session() req.session
@Param(param?: string) req.params / req.params[param]
@Body(param?: string) req.body / req.body[param]
@Query(param?: string) req.query / req.query[param]
@Headers(param?: string) req.headers / req.headers[param]

参数装饰器很明显是基于依赖注入实现的:

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request) {
    return 'This action returns all cats';
  }
}

async / await

支持异步写法,返回可以是普通对象(Promise会封装)或者Observable(RxJS对象)

请求负载

@Body支持注入 DTO类型参数(待研究)

挂载Controller到app.module.ts

基于类库方法编写Controller

(略),传统的lib方法调用。

qiansc commented 5 years ago

Part.概述.3 providers

service, repository, factory, helper 皆可是provider,可以通过 constructor 注入依赖关系,provider是一个用@Injectable() 装饰器注解的简单类。

要使用 CLI 创建服务类,只需执行 $ nest g service cats/cats 命令。

Nest 是建立在强大的设计模式, 通常称为依赖注入。我们建议在官方的 Angular文档中阅读关于这个概念的伟大文章。

233333:)

创建Provider

可选的Provider(待确定是注入阶段,还是定义阶段设置@Optional,待研究):

@Injectable()
export class HttpService<T> {
  constructor(
    @Optional() @Inject('HTTP_OPTIONS') private readonly httpClient: T,
  ) {}
}

基于属性的注入:

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

Part.概述.4 modules

模块是基于功能粒度,组织应用程序结构,如CatsController 和 CatsService 属于同一个应用程序域。 应该考虑将它们移动到一个功能模块下,即 CatsModule。

模块元素

@module() 装饰器接受一个描述模块属性的对象:

Name  
providers 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享
controllers 必须创建的一组控制器
imports 导入模块的列表,这些模块导出了此模块中所需提供者
exports 由本模块提供并应在其他模块中可用的提供者的子集

要使用 CLI 创建模块,只需执行 $ nest g module cats 命令。

@Module({
  // controllers: [xxxController], //定义模块controller
  // providers: [xxxService], //定义模块provider
  imports: [CatsModule], // 引入模块
})
export class ApplicationModule {}

目录结构:

src
├──cats
│    ├──dto
│    │   └──create-cat.dto.ts
│    ├──interfaces
│    │     └──cat.interface.ts
│    ├─cats.service.ts
│    ├─cats.controller.ts
│    └──cats.module.ts
├──app.module.ts
└──main.ts

共享模块

多个Module导入同个Module可以共享其export(里的实例)

模块重新导出

其依赖(imports)重新导出(exports)

@Module({
  imports: [CommonModule],
  exports: [CommonModule],
})
export class CoreModule {}

依赖注入

Provider注入到Module中,多用于配置目的(待研究)

// cats.module.ts
@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {
  constructor(private readonly catsService: CatsService) {}
}

全局模块:

@Global修饰

动态模块:

模块定义时候可以写导出Modules的方法,模块imports时候可以指定这个方法传入动态模块 (动态使用的场景这个...真只有到用时候才能体会...看过先放在这...待研究)

qiansc commented 5 years ago

Part.概念.5 middlewares

中间件函数可以执行以下任务:

执行任何代码。 对请求和响应对象进行更改。 结束请求-响应周期。 调用堆栈中的下一个中间件函数。 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 next() 将控制传递给下一个中间件函数。否则, 请求将被挂起。

Nest 中间件可以是一个函数,也可以是一个带有 @Injectable() 装饰器的类。 这个类应该实现 NestMiddleware 接口。resolve() 方法必须返回类库特有的常规中间件 (req, res, next) => any

// logger.middleware.ts
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  resolve(...args: any[]): MiddlewareFunction {
    return (req, res, next) => {
      console.log('Request...');
      next();
    };
  }
}

应用中间件:

中间件不能在 @Module() 装饰器中列出。我们必须使用模块类的 configure() 方法来设置它们。包含中间件的模块必须实现 NestModule 接口。我们将 LoggerMiddleware 设置在 ApplicationModule 层上。

// app.module.ts
import { LoggerMiddleware } from './common/middlewares/logger.middleware';

@Module({
  imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) // Here Apply
      // .forRoutes('/cats');
      // .forRoutes({ path: 'ab*cd', method: RequestMethod.ALL })
      .forRoutes({ path: 'cats', method: RequestMethod.GET });
  }
}

路由通配符

? 、 + 、 * 以及 () 是它们的正则表达式对应项的子集。连字符 (-) 和点 (.) 按字符串路径解析。

中间件消费者

在 forRoutes() 可采取一个字符串、多个字符串、RouteInfo 对象、控制器类甚至多个控制器类。

consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);

consumer
      .apply(LoggerMiddleware)
      .exclude(
        { path: 'cats', method: RequestMethod.GET },
        { path: 'cats', method: RequestMethod.POST },
      )
      .forRoutes(CatsController);

因此,除了传递给 exclude() 的两个路由之外, LoggerMiddleware 将绑定在 CatsController 中其余的路由上。 请注意,exclude() 方法不适用于函数式中间件。 此外,此功能不排除来自更通用路由(例如通配符)的路径。 在这种情况下,您应该将路径限制逻辑直接放在中间件中,例如,比较请求的URL这种逻辑就应该放在中间件中。(待研究)

可配置中间件

有时中间件的行为取决于自定义值,例如用户角色数组,选项对象等。我们可以将其他参数传递给 resolve() 来使用 with() 方法。


// app.moudle.ts
@Module({
imports: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(LoggerMiddleware)
.with('ApplicationModule')
.forRoutes(CatsController);
}
}

// logger.middleware.ts

@Injectable() export class LoggerMiddleware implements NestMiddleware { resolve(name: string): MiddlewareFunction { // 该 name 的属性值将是 ApplicationModule return (req, res, next) => { console.log([${name}] Request...); // [ApplicationModule] Request... next(); }; } }

### 异步中间件

```Typescript
//  logger.middleware.ts

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  async resolve(name: string): Promise<MiddlewareFunction> {
    await someAsyncJob();

    return async (req, res, next) => {
      await someAsyncJob(); // Here Do Sth.
      console.log(`[${name}] Request...`); // [ApplicationModule] Request...
      next();
    };
 }
}

函数式中间件

简单的中间件可以使用一个函数来实现

// logger.middleware.ts

export function logger(req, res, next) {
  console.log(`Request...`);
  next();
};

// in app.module.ts
consumer.apply(logger).forRoutes(CatsController);

多个中间件

apply(cors(), helmet(), logger)

全局中间件

const app = await NestFactory.create(ApplicationModule);
app.use(logger);
await app.listen(3000);
qiansc commented 5 years ago

Part.概念.6 Exceptionfilters

基础异常类(Base exceptions)


@Post()
async create(@Body() createCatDto: CreateCatDto) {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

HttpException 构造函数采用 string | object 作为第一个参数。如果您要传递「对象」而不是「字符串」, 则将完全覆盖响应体。

{
    "status": 403,
    "error": "This is a custom message"
}

异常过滤器(Exception Filters)

每个异常过滤器都应该实现通用的 ExceptionFilter 接口。它强制你提供具有正确特征的 catch(exception: T, host: ArgumentsHost)的方法。T 表示异常的类型 @Catch(HttpException, ....) 装饰器绑定所需的元数据到异常过滤器上。它告诉 Nest,这个过滤器正在寻找 HttpException(等),留空则捕获所有异常

绑定过滤器

将 HttpExceptionFilter 以装饰器方式绑定到 create() 方法上,如:

// cats.controller.ts
@Post()
// @UseFilters(HttpExceptionFilter)
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
  throw new ForbiddenException();
}

异常过滤器除了可以作用在方法,同样可以作用在Class、Controller、Module范围,甚至全局。

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(ApplicationModule);
  app.useGlobalFilters(new HttpExceptionFilter());
  await app.listen(3000);
}
// app.module.ts
@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: HttpExceptionFilter,
    },
  ],
})

继承(完全定制的异常过滤)

(待研究)

qiansc commented 5 years ago

Part.概念.7 Pipies

管道是具有 @Injectable() 装饰器的类。管道应实现 PipeTransform 接口。 管道将输入数据转换为所需的输出。另外,它可以处理验证,因为当数据不正确时可能会抛出异常。

import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}

每个管道必须提供 transform() 方法。 这个方法有两个参数: value 是当前处理的参数,而 metadata 是其元数据。

export interface ArgumentMetadata {
    type: 'body' | 'query' | 'param' | 'custom';
    metatype?: new (...args) => any;
    data?: string;
}
参数 描述
type 告诉我们该属性是一个 body @Body(),query @Query(),param @Param() 还是自定义参数 在这里阅读更多。
metatype 属性的元类型,例如 String。 如果在函数签名中省略类型声明,或者使用原生JavaScript,则为 undefined。
data 传递给装饰器的字符串,例如 @Body('string')。 如果您将括号留空,则为 undefined。

不同地方(参数、方法、Class、全局等)使用修饰符可以注入一些处理特定功能的管道,如ValidationPipe

绑定管道

// cats.controler.ts

@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

@Post()
@UsePipes(ValidationPipe)
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

// main.ts

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

外部注册的全局管道可以按以下方式注入:

// app.module.ts

@Module({
  providers: [
    {
      provide: APP_PIPE,
      useClass: CustomGlobalPipe,
    },
  ],
})
export class ApplicationModule {}

转换管道

从transform 函数返回的值完全覆盖了参数的前一个值。有时从客户端传来的数据需要经过一些修改。

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

@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
  return userEntity;
}

内置验证管道

看起来是管道方法的参数可以实现一些配置功能,比如transform: true开启转换,虽然这样显的职责不单一啊 (待研究)

qiansc commented 5 years ago

Part.概念.8 Guards

守卫是一个使用 @Injectable() 装饰器的类。 守卫应该实现 CanActivate 接口。

授权看守卫

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

canActivate() 函数采用单参数 ExecutionContext 实例。ExecutionContext 从 ArgumentsHost 继承。

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; // 当前引用
}

基于角色的认证

继承自CanActivate

export class RolesGuard implements CanActivate

// cats.controller.ts

@Controller('cats')
@UseGuards(RolesGuard)
// @UseGuards(new RolesGuard())
export class CatsController {}

同之前管道、异常注入(不再展开):

守卫可以是控制器范围的,方法范围的和全局范围的。为了建立守卫,我们使用 @UseGuards() 装饰器。这个装饰器可以有无数的参数。也就是说,你可以传递几个守卫并用逗号分隔它们。

反射器

为了让守卫能够复用处理不同角色,可以使用@ReflectMetadata() 装饰器附加自定义元数据的能力。

// cats.controller.ts
@Post()
@ReflectMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}

直接使用 @ReflectMetadata() 并不是一个好习惯。 相反,你应该总是创建你自己的装饰器。

// roles.decorator.ts
export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);

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

最后使用它 (待实践)

// roles.guard.ts

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const hasRole = () => user.roles.some((role) => roles.includes(role));
    return user && user.roles && hasRole();
  }
}
qiansc commented 5 years ago

Part.概念.9 Interceptors

拦截器是@Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。

拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:

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

(后面暂时没看懂,待结合实践研究)

Part.概念.10 Custom Decorators

总结下装饰器的特性及使用场景:

qiansc commented 5 years ago

第二遍重读后对于几种概念的通俗总结