不管做没做过软件开发,我们可能都知道:通过一个URL地址可以访问到一个网站的资源,比如页面、图片、文件等等。不同的地址,可能最终访问到的内容不同,也可能会访问到相同的内容。其实,每一个URL都是由网站的服务器端程序来接收并进行处理,最终定向到相应的资源。这种机制,在服务端程序中被称作路由。
路由机制决定了请求与控制器之间的关系,即一个请求被分派到哪个控制器进行处理。通常服务端Web框架都会有路由机制,或简单、或复杂,但要实现的功能都是类似的。
比如在Express.js(也是NestJS的默认底层适配框架)中,它的路由定义会是这样:
代码语言:javascript复制// 一个简单的 GET 方法路由
app.get('/products', function (req, res) {
res.send('GET request handled!')
})
// 一个简单的 POST 方法路由
app.post('/products', function (req, res) {
res.send('POST request handled!')
})
上面的这种方式,比较简单直观,通过函数的形式定义了一个路由匹配路径规则和对应的业务处理函数间的关系。
路由装饰器
而NestJS采用了另一种方式:使用装饰器。NestJS框架中定义了若干个专门用于路由处理相关的装饰器,通过它们,可以非常容易的将普通的class类装饰成一个个路由控制器。这个在我们的第一篇教程文章里生成的骨架代码中就已经看到过了:
代码语言:javascript复制import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
每个要成为控制器的类,都需要借助 @Controller 装饰器的装饰。该装饰器也可以传入一个路径参数,作为访问这个控制器的主路径:
代码语言:javascript复制@Controller("home")
这样改写以后,本地访问的URL就变成了:
代码语言:javascript复制 http://localhost:3000/home
而 @Get 装饰器是众多HTTP方法处理装饰器中的一个(其他的有@Post,@Put,@Delete,@Patch,@Options,@Head,@All),经过它装饰的类方法,可以对HTTP的Get方法请求进行响应。它可以接受一个字符串或一个字符串数组作为参数,这里的字符串可以是固定的路径,也可以是通配符路径,请看以下的例子组合:
代码语言:javascript复制// 主路径为 home
@Controller("home")
// 1. 固定路径
// 可匹配到的访问路径:
// http://localhost:3000/home/greeting
@Get("greeting")
// 2. 通配符路径(通配符可以有 ?, , * 三种)
// 可匹配到的访问路径:
// http://localhost:3000/home/say_hi
// http://localhost:3000/home/say_hello
// http://localhost:3000/home/say_good
// ...
@Get("say_*")
// 3. 路径数组
// 可匹配到的访问路径:匹配上面1和2里的所有路径
@Get(["greeting", "say_*"])
// 4. 带参路径
// 可匹配到的访问路径:
// http://localhost:3000/home/greeting/hello
// http://localhost:3000/home/greeting/good-morning
// http://localhost:3000/home/greeting/xxxxx
// ...
@Get("greeting/:words")
标准模式和特定库模式
乍一看,标准模式和特定库模式,有点不知所云。那让我们再来回顾一下NestJS是一个什么样的框架,就能更清楚的了解这两个模式的区别。
如上图所示,NestJS是一个通过适配器来调用底层其他Web框架的一个上层框架。这些底层框架的API之间多多少少会存在一些差别,NestJS通过适配器抹平了大部分的差别,使得在大多数场景下,通过它封装的API就能完成工作。但是总会有些场景会用到那些没法被统一化封装的底层框架特有API,在这种情况下,我们需要获取和调用底层框架的原生对象或函数。
所以,使用NestJS通用API的方式称为标准模式;而使用特定底层库API的方式则被称为特定库模式。
下面来看看这两种模式下的代码有什么区别。我们来实现一个可以接受URL Query String参数的控制器方法。
1. 标准模式的代码
代码语言:javascript复制import { Controller, Get, Query } from '@nestjs/common';
@Controller("home")
export class AppController {
@Get("greeting")
getHello(@Query("from") from: string): string {
return `A greeting from ${from}`;
}
}
2.特定库模式的代码
代码语言:javascript复制import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller("home")
export class AppController {
@Get("greeting")
getHello(@Req() req: Request, @Res() res: Response) {
const { from } = req.query;
res.send(`A greeting from ${from}`);
}
}
以上两段代码实现了一样的功能,它们都可以接收一个名为from的URL查询字符串参数,然后将拼接后的整个问候语输出到请求响应中去。可以通过这个URL试一下效果:
代码语言:javascript复制http://localhost:3000/home/greeting?from=一斤代码
浏览器中访问的效果如下:
虽然两段代码功能相同,但是写法上的差别看起来还是很明显的。
标准模式下的写法尽量避免使用特定的框架对象,比如:不会去直接使用底层框架的请求(Request)和响应(Response)对象及其属性/方法。就如上面的代码所示,当获取参数时,只需通过@Query装饰器就可以把URL上携带的参数填充到控制器的函数参数中。这样的代码保持了底层框架无关性,更容易复用,当替换底层框架的时候也更容易做迁移。
而特定库模式的写法,就会为控制器函数注入特定底层框架(比如示例代码中的Express)对象,直接调用底层框架对象提供的功能。这种方式带来的好处是更直接,可以使用到上层框架中所没有提供的功能。但是,如果你的应用在将来可能计划做底层框架替换,比如用性能更好的Fastify替换Express,那使用过多的特定库模式写法就会增加移植的工作量和难度。
所以在这两种模式的使用上,需要权衡利弊。大多数情况下,推荐使用标准模式,实在是遇到上层框架中完成不了的功能,才考虑使用特定库模式。
其他常用装饰器的功能示例
一、@Param - 路径参数装饰器
当我们的URL中有一部分是动态的,比如下面的三个:
代码语言:javascript复制http://www.myblog.com/articles/20191110
http://www.myblog.com/articles/20191111
http://www.myblog.com/articles/20191112
上面这些地址看起来是一个博客网站系统按日期查看文章的页面,地址最后的日期部分,肯定是不固定的,输入每一个日期查看到的结果都可能会不一样。对于这种情况,服务端程序是不太可能会为每一个日期都编写一个控制器函数(除非写这个网站的程序员是个奇葩),最可能的情况就是只有一个控制器函数,这个函数能从URL上获取动态的日期这部分信息,然后根据获取到的日期去数据库查询对应日期的文章信息。
如果用NestJS来实现,看起来就会是这样:
代码语言:javascript复制import { Controller, Get, Param } from '@nestjs/common';
@Controller("articles")
export class ArticleController {
@Get(":date")
getArticles(@Param("date") date: string): string {
return `Articles for ${date}`;
}
}
二、@Post @Body - 获取POST请求的请求体
当我们向服务端发送POST请求的时候,参数一般都会是放入请求体进行携带的,它可以比URL查询字符串携带更多的数据量。在NestJS里处理POST请求以及获取请求体参数,是这样做的:
代码语言:javascript复制interface CreateArticleDto {
title: string;
content: string;
}
// ....
@Post()
async create(@Body() article: CreateArticleDto) {
console.log(article);
this.articleService.create(article);
return 'New article is created';
}
如果我们去请求这个POST形式的API,并传入一个JSON格式的请求体参数给它:
代码语言:javascript复制{
"title": "逆天啦!某程序员写了一斤代码!",
"content": "据了解,该程序员的硬盘重达一斤。"
}
则控制器的 create 函数参数 article 就会被接收到的JSON数据所填充,控制台打印出来的内容如下:
三、@Headers和@Header - 获取请求头和设置响应头
我们经常会使用HTTP头来在客户端和服务端传递信息,比如:通过请求头来携带登录授权的Authorization令牌值;或者为响应头设置Access-Control-Allow-Origin值,指定可进行跨域调用的域名规则,等等。在NestJS中我们可以通过装饰器来很方便的实现对请求头的访问和操作:
代码语言:javascript复制@Post("test")
@Header('x-my-resp', '123')
test(@Headers("x-my-val") myHeaderVal: string) {
return `x-my-val is ${myHeaderVal}`
}
上面的代码中,我们通过 @Headers 装饰器获取请求头中名为x-my-val的头信息;并使用 @Header 装饰器在相应头中添加了一个名为x-my-resp的自定义头。
下面是使用API测试工具进行测试的结果:
总结
路由和控制器是编写服务端API的工作中,非常基础又非常重要的一环,先熟悉和理解基本的用法,然后深入思考和研究它们的实现原理,这些知识在服务端编程中都是共通的,无论在Node.js、Java、亦或是Go等,都遵循着同样的底层协议体系、相似的应用框架设计。掌握它们吧!让服务端程序在你的手中被精准的控制。