上文中,我们通过命令行建立了一个几乎是最小的Nest项目,通过了解目录和主要文件,对Nest的框架有了一个大概的概念。这离能够真正的开发还有相当一段距离,这里我们解决在Web开发中最常见的一些需求。
控制器
在Nest中,由“控制器”负责处理请求和返回业务。其中包含了:
- 控制请求的路由
- 控制请求的方法(Get Post等)
- 预处理请求的对象,其中包括了请求头、请求体(body)和参数(Parameter)
- 将请求对应到相应的资源中
- 处理返回的HTTP状态码、重定向
- 资源返回结果处理
控制器装饰器
一个普通的类,通过控制器装饰,被重新定义为一个控制器。我们看到app.controller.ts中,有@Controller()
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
如果你对装饰器还比较陌生,建议先看一下我之前的文章。我们可以跟踪Controller装饰器到其源代码位置详细了解,它支持四种参数:没有参数、字符串、字符串数组和对象。对象有两个字段,分别是路径和主机。
export function Controller(
prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {
const defaultPath = '/';
const [path, host, scopeOptions] = isUndefined(prefixOrOptions)
? [defaultPath, undefined, undefined]
: isString(prefixOrOptions) || Array.isArray(prefixOrOptions)
? [prefixOrOptions, undefined, undefined]
: [
prefixOrOptions.path || defaultPath,
prefixOrOptions.host,
{ scope: prefixOrOptions.scope },
];
return (target: object) => {
Reflect.defineMetadata(PATH_METADATA, path, target);
Reflect.defineMetadata(HOST_METADATA, host, target);
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
};
}
export interface ControllerOptions extends ScopeOptions {
path?: string | string[];
host?: string;
}
- 没有参数:默认路径
- 字符串:路径
- 字符串数组:多个别名路径
- 对象:路径和主机,如果定义了主机,则会对请求的主机(字符串)处理
例如,设置一个/article的控制器
@Controller({ path: 'article' })
export class ArticleController {}
换做使用Express框架,相当于如下代码:
app.use('/article', require('./routes/article'))
请求
路径与方法装饰器
上面通过控制器装饰器将一个类定义为控制器,并设置了控制器的路径(父级路径)。再通过方法装饰器,将类的某一个方法定义为一个Web的请求资源节点(子级路径),有:@Get() @Post() @Put() @Delete() @Patch() @Options() @Head() @All()
@Get('what')
what(): string {
return 'sunny';
}
装饰器(方法)仅接受一种可选(为空)字符串参数,就是子路径。如果没有输入参数,默认路径是控制器路径下的根路径/。这个参数还支持通配符和作为孙子路径的参数,例如@Get('get*') @Get(':id')。
多层级路径
简单说明一下:当工程的体量达到一定规模后,一个项目中会包含几十个控制器,而控制器映射的路径并不是根据模块化或者引用关系决定的,而是直接在控制器装饰器中定义的。例如先前@Controller('articles')如果有一个子控制器detail,那么detail控制需要定义为@Controller('articles/detail')。
参数装饰器
Nest有4种定义参数的装饰器@Body() @Param() @Query() @HostParam() ,与Express的机制一样,见下表:
| 装饰器 | Express 参数 | 应用方法 |
|---|---|---|
| @Param(key?: string) | req.params / req.params[key] | 请求的路径目录为参数 |
| @Body(key?: string) | req.body / req.body[key] | Put Post Delete在Body中的数据 |
| @Query(key?: string) | req.query / req.query[key] | 在请求url中?以后的参数 |
| @HostParam(property?: string | (Type | PipeTransform) | req.hosts | 请求主机的信息也作为参数处理 |
举一个简单的例子,我们修改一下app.controller:
// 参数测试
@Get()
getHello(@Query() q): string {
this.logger.log(q);
return this.appService.getHello();
}
curl --request GET 'http://localhost:3000/?a=1&b=2'
#{
# "a": "1",
# "b": "2"
#}
可以看到,控制台输出了一个对象。我们再稍微修改一下app.controller代码:
@Get()
getHello(
@Query('a') a: number,
@Query('b') b: number,
@Query('c') c: string,
): string {
this.logger.log(`a=${a} & b=${b} & c=${c}`);
return this.appService.getHello();
}
此时控制台输出 a=1 & b=2 & c=undefined。也就是说,这个参数装饰器中可选的参数如果不写,那么就代表了所有参数,如果写了,那就是具体的某一个参数
另外这里要说一个坑点(版本:7.6.5),就是Nest似乎对 form-data 格式的提交支持的不太好。需要中间做一个转换才可以被识别,但是使用x-www-form-urlencoded没有任何问题。需要利用中断器转换一下。
@Post('num')
@UseInterceptors(FileInterceptor(''))
test(@Body() _body: QFormDto) {
this.logger.log(`[num] ${typeof _body.num} ${_body.where}`);
return false;
}
如果把第二行注释掉,再执行以下代码,会发现无法获得_body信息。
curl --location --request POST 'localhost:3000/weather/num' --form 'num="3"' --form 'where="shanghai"'
如果将请求改为urlencoded之后,确是没有问题的。
curl --location --request POST 'localhost:3000/weather/num' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'num=3' \
--data-urlencode 'where=shanghai'
关于@HostParam有点特殊,配合这个装饰器,需要在控制器装饰器中定义Host信息。例如下面的代码
@Controller({ path: 'weather', host: ':ip1.:ip2.:ip3.:ip4' })
export class WeatherController {
logger = new Logger('weather');
@Get('/')
getAccount(
@HostParam('ip1') ip1: number,
@HostParam('ip2') ip2: number,
@HostParam('ip3') ip3: number,
@HostParam('ip4') ip4: number,
) {
this.logger.log(`ip: ${ip1}:${ip2}:${ip3}:${ip4}`);
return 'done';
}
}
然后访问http://127.0.0.1:3000/weather看看输出,再将其改为如下的代码,看看输出
@Controller({ path: 'weather', host: 'local:host' })
export class WeatherController {
logger = new Logger('weather');
@Get('/')
getAccount(@HostParam('host') host: string) {
this.logger.log(`host: ${host}`);
return 'done';
}
}
文件上传
针对文件上传,Nest官方内建了multer模块,使用起来也颇为简单:
import {
Post,
UseInterceptors,
UploadedFiles,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express/multer';
@Controller()
export class AppController {
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
uploadFile(@UploadedFiles() files): string {
const result = [];
files.map((file) => {
this.logger.log(file.originalname);
result.push(file.originalname);
});
return result.join(',');
}
}
以上代码有三个过程装饰器包含起来:
@UserInterceptors()使用一个中断器;FilesInterceptor()将multer封装为一个处理文件的中断器(IoC),指定处理专门的字段;@UploadedFiles()用装饰器将被上传的文件定义为方法的参数;
req对象
默认情况下,Nest封装了Express框架(目前官方还封装了Fastify框架),如果需要一些较为底层的操作,可以直接使用req对象,就像在Express中开发一样使用。
import {
Controller,
Get,
Logger,
Render,
Req
} from '@nestjs/common';
import { AppService } from './app.service';
import { Request } from 'express';
@Controller()
export class AppController {
logger = new Logger('root');
constructor(private readonly appService: AppService) {}
@Get()
getHello(@Req() req: Request): string {
this.logger.log(req.headers['user-agent']);
return this.appService.getHello();
}
}
返回
Nest利用方法装饰器会自动将返回的对象处理成HTTP的数据包。
返回对象
主要可以将其分为三个部分:HTTP状态码,Header部分和Body部分;
Body
如果返回本来就是string,那么返回的header中content type就是text/html,如果是对象,则会将对象序列化为Json,Header设置为application/json。
Header
例如需要返回(下载)一个文件,需要对返回内容的头信息进行一定处理,利用装饰器Header。它接受两个字符串为参数,分别是Header中的key和value。下面以文件下载的接口为例,来简单描述下Header的用法:
@Get('/file')
@Header('Content-Disposition', 'attachment; filename=controller.js')
getFile() {
const file = fs.readFileSync(join(__dirname, 'weather.controller.js'));
return file;
}
HTTP状态码
利用@HttpCode()装饰器,可以自定义返回的HTTP状态码,譬如说
@Get('pug')
@HttpCode(404)
@Render('index')
pug() {
return { title: `Pug ${Math.random()}`, message: 'Hello world' };
}
通过这种方式,开发者可以自行设计诸如未验证、无授权、未找到页面等等的异常状态页面。
HTML模板
Nest很好的继承了Express所支持的MVC模板,例如pug、hbs。使用之前先安装模板引擎
npm install --save hbs # hbs模板
npm install --save pug # pug模板
以pug为例,在main.ts中加入引用引擎
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('pug');
await app.listen(3000);
}
bootstrap();
注意:Nest的核心工厂,在创建时一定要定义一个泛型NestExpressApplication,app设置pug为视图引擎。相当于在Express框架中使用app.set('view engine', 'pug')。
在项目的更目录下创建一个views目录,新建一个index.pug:
html
head
title Pug模板
body
h1 #{title}
p #{message}
最后,创建一个控制器中的资源,可以被最终访问到:
@Get('pug')
@Render('index')
pug() {
return { title: 'Pug', message: 'Hello world' };
}
其中@Render()装饰器方法唯一一个参数是模板位置和名词字符串;
重定向
Nest主要有两种重定向的方法,@Redirect(url?: string, statusCode?: number)装饰器重定向和res.redirect(),在底层上,用了Web服务框架(Express)的res.redirect。Redirect中也可以不带任何参数,那么跳转的url就需要在return中定义。如果在装饰器设置了跳转地址同时又在返回值中定义了url,那么程序会执行return的url对象。
@Get('redirect')
@Redirect()
redirect() {
this.logger.log('redirect to pug');
return { url: '/pug', "statusCode":301 };
}
第二种方式是使用底层的对象req。需要引入express的Response
import { Response } from 'express';
// ...
@Get('redirect')
redirect(@Res() res: Response) {
this.logger.log('res.direct...');
res.redirect('/pug');
}
next
next是Express跳转到下一个中间件的函数,其功能有点类似于一种重定向,只不过他发生在一次请求的过程之中。
@Get()
getHello(
@Req() req: Request,
@Res() res: Response,
@Next() next: NextFunction,
) {
this.logger.log(req.headers['user-agent']);
res.setHeader('Warning', 'This is a demo');
next();
}
@Get('/')
getNext_1(@Next() next: NextFunction) {
this.logger.log('Do something different.');
next();
}
@Get('/*')
getNext_2(@Query('where') where: string): string {
this.logger.log(`${where}: Do it again.`);
return this.appService.getHello();
}
如果是在控制器内部,那么会按照方法的编写顺序,逐个传递执行。每个执行的方法请求(req)和响应(res)都是保持一致的。next函数不需要带有参数,next的第一个参数是传递错误对象。虽然可以,但是并不建议用next的方式做夸控制器之间的传递。
res对象
与req对象对应,返回的信息最后也会经由Request来处理,Nest同样提供了一个装饰器来继续从Web框架中再次接管返回处理。
import {Controller, Get, Logger, Res, Req } from '@nestjs/common';
import { AppService } from './app.service';
import { Response, Request } from 'express';
export class AppController {
logger = new Logger('root');
constructor(private readonly appService: AppService) {}
@Get()
getHello(@Req() req: Request, @Res() res: Response): void {
this.logger.log(req.headers['user-agent']);
res.setHeader('Warning', 'This is a demo');
res.send(this.appService.getHello());
}
}
需要注意:使用Res对象,就需要从Nest手中接管Response对象,那么这也就意味着后续的操作都将转交给用户操作,所以getHello方法如果还是return,则会无效。要避免这种情况,可以在装饰器中加入一个参数{ passthrough: true }来将控制权在方法内部逻辑执行完成之后再次交还给Nest。特别是当前方法同时还使用路由传递(Next)的功能。
@Get()
getHello(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
): string {
this.logger.log(req.headers['user-agent']);
res.setHeader('Warning', 'This is a demo');
return this.appService.getHello();
}