不管做没做过软件开发, 我们可能都知道: 通过一个 URL 地址可以访问到一个网站的资源, 比如页面, 图片, 文件等等. 不同的地址, 可能最终访问到的内容不同, 也可能会访问到相同的内容. 其实, 每一个 URL 都是由网站的服务器端程序来接收并进行处理, 最终定向到相应的资源. 这种机制, 在服务端程序中被称作路由.
路由机制决定了请求与控制器之间的关系, 即一个请求被分派到哪个控制器进行处理. 通常服务端 web 框架都会有路由机制, 或简单, 或复杂, 但要实现的功能都是类似的.
比如在 Express.JS(也是 NestJS 的默认底层适配框架)中, 它的路由定义会是这样:
- // 一个简单的 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 类装饰成一个个路由控制器. 这个在我们的第一篇教程文章里生成的骨架代码中就已经看到过了:
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 装饰器的装饰. 该装饰器也可以传入一个路径参数, 作为访问这个控制器的主路径:
@Controller("home")
这样改写以后, 本地访问的 URL 就变成了:
http://localhost:3000/home
而 @Get 装饰器是众多 HTTP 方法处理装饰器中的一个(其他的有 @Post,@Put,@Delete,@Patch,@Options,@Head,@All), 经过它装饰的类方法, 可以对 HTTP 的 Get 方法请求进行响应. 它可以接受一个字符串或一个字符串数组作为参数, 这里的字符串可以是固定的路径, 也可以是通配符路径, 请看以下的例子组合:
// 主路径为 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. 标准模式的代码
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. 特定库模式的代码
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 试一下效果:
http://localhost:3000/home/greeting?from = 一斤代码
浏览器中访问的效果如下:
虽然两段代码功能相同, 但是写法上的差别看起来还是很明显的.
标准模式下的写法尽量避免使用特定的框架对象, 比如: 不会去直接使用底层框架的请求 (Request) 和响应 (Response) 对象及其属性 / 方法. 就如上面的代码所示, 当获取参数时, 只需通过 @Query 装饰器就可以把 URL 上携带的参数填充到控制器的函数参数中. 这样的代码保持了底层框架无关性, 更容易复用, 当替换底层框架的时候也更容易做迁移.
而特定库模式的写法, 就会为控制器函数注入特定底层框架 (比如示例代码中的 Express) 对象, 直接调用底层框架对象提供的功能. 这种方式带来的好处是更直接, 可以使用到上层框架中所没有提供的功能. 但是, 如果你的应用在将来可能计划做底层框架替换, 比如用性能更好的 Fastify 替换 Express, 那使用过多的特定库模式写法就会增加移植的工作量和难度.
所以在这两种模式的使用上, 需要权衡利弊. 大多数情况下, 推荐使用标准模式, 实在是遇到上层框架中完成不了的功能, 才考虑使用特定库模式.
其他常用装饰器的功能示例
一,@Param - 路径参数装饰器
当我们的 URL 中有一部分是动态的, 比如下面的三个:
http://www.myblog.com/articles/20191110 http://www.myblog.com/articles/20191111 http://www.myblog.com/articles/20191112
上面这些地址看起来是一个博客网站系统按日期查看文章的页面, 地址最后的日期部分, 肯定是不固定的, 输入每一个日期查看到的结果都可能会不一样. 对于这种情况, 服务端程序是不太可能会为每一个日期都编写一个控制器函数(除非写这个网站的程序员是个奇葩), 最可能的情况就是只有一个控制器函数, 这个函数能从 URL 上获取动态的日期这部分信息, 然后根据获取到的日期去数据库查询对应日期的文章信息.
如果用 NestJS 来实现, 看起来就会是这样:
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 请求以及获取请求体参数, 是这样做的:
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 格式的请求体参数给它:
{ "title": "逆天啦! 某程序员写了一斤代码!", "content": "据了解, 该程序员的硬盘重达一斤." }
则控制器的 create 函数参数 article 就会被接收到的 JSON 数据所填充, 控制台打印出来的内容如下:
三,@Headers 和 @Header - 获取请求头和设置响应头
我们经常会使用 HTTP 头来在客户端和服务端传递信息, 比如: 通过请求头来携带登录授权的 Authorization 令牌值; 或者为响应头设置 Access-Control-Allow-Origin 值, 指定可进行跨域调用的域名规则, 等等. 在 NestJS 中我们可以通过装饰器来很方便的实现对请求头的访问和操作:
@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 等, 都遵循着同样的底层协议体系, 相似的应用框架设计.
掌握它们吧! 让服务端程序在你的手中被精准的控制.
关注首发公众号: 默碟
来源: http://www.jianshu.com/p/520aabbe7f91