Open qiansc opened 5 years ago
起步很简单,以及所有官方示例的demo托管在Github
controllers有两种操作响应的方式:基于装饰器及基于类库方法。
修饰controller类 @Controller('cats') ,cats为controller的BasePath
修饰controller方法 @Get @Put @Delete (等type类型装饰器)以@Delete为例,需要请求时附带的type为delete(ajaxOptions.type),在HttpHeaders中则为Request Method: DELETE。(这点官网没有细解释,坑了很久)
可以限定匹配路由规则如@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';
}
}
支持异步写法,返回可以是普通对象(Promise会封装)或者Observable(RxJS对象)
@Body支持注入 DTO类型参数(待研究)
挂载Controller到app.module.ts
(略),传统的lib方法调用。
service, repository, factory, helper 皆可是provider,可以通过 constructor 注入依赖关系,provider是一个用@Injectable() 装饰器注解的简单类。
要使用 CLI 创建服务类,只需执行 $ nest g service cats/cats 命令。
Nest 是建立在强大的设计模式, 通常称为依赖注入。我们建议在官方的 Angular文档中阅读关于这个概念的伟大文章。
233333:)
可选的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;
}
模块是基于功能粒度,组织应用程序结构,如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时候可以指定这个方法传入动态模块 (动态使用的场景这个...真只有到用时候才能体会...看过先放在这...待研究)
中间件函数可以执行以下任务:
执行任何代码。 对请求和响应对象进行更改。 结束请求-响应周期。 调用堆栈中的下一个中间件函数。 如果当前的中间件函数没有结束请求-响应周期, 它必须调用 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);
@Post()
async create(@Body() createCatDto: CreateCatDto) {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
HttpException 构造函数采用 string | object 作为第一个参数。如果您要传递「对象」而不是「字符串」, 则将完全覆盖响应体。
{
"status": 403,
"error": "This is a custom message"
}
每个异常过滤器都应该实现通用的 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,
},
],
})
(待研究)
管道是具有 @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开启转换,虽然这样显的职责不单一啊 (待研究)
守卫是一个使用 @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();
}
}
拦截器是@Injectable() 装饰器注解的类。拦截器应该实现 NestInterceptor 接口。
拦截器具有一系列有用的功能,这些功能受面向切面编程(AOP)技术的启发。它们可以:
- 在函数执行之前/之后绑定额外的逻辑
- 转换从函数返回的结果
- 转换从函数抛出的异常
- 根据所选条件完全重写函数 (例如, 缓存目的)
(后面暂时没看懂,待结合实践研究)
总结下装饰器的特性及使用场景:
Controller 顾名思义,一般用于映射路由和行为(如调用Service方法等)
Service 一般实现具体业务逻辑,如操作Entity
Module 个人理解为每个Module为一层IOC,往Module上定义各种Provider并在运行时注入
Middleware 接受路由的操作回调next,然后可以在next()前后做一些事情,如日志,但next是个void返回的回调,Middleware对上下文无感知
Interceptor 拦截器和Middleware有点类似,但是他可以得到上下文,并且可以通过next.handle()执行结果,甚至加以改变。理论上可以认为Interceptor可以实现Middleware包括后续介绍其他修饰的所实现的功能。
Filter 处理所有返回异常,再返回一个结果Object or Text
Pipe 改变传入值,如修改params
Guard 守卫 前置做一些事情,返回true / false决策后续是否进行
上述大部分都是Injectable,可以在路由方法、路由类、Module、App全局等不同层面注入
AOP 切面编程 OOP编程比较难解决一个横线的通用功能的代码散布在不同业务代码里,如日志。AOP的思想就是提供一种机制,像刀一样可以把业务模块横切开,做完通用的事情后,业务模块还能还原。Nest中的体现就是,各种通用功能抽象为Middleware、Interceptor、Pipe等等,然后通过注解方式附着在数据流程上。
NestJS的官方教程起点有一定高度,如果没有扎实的NG、Express经验前,需要对一些概念进行了解。