Nest.js 中实现 API 多版本

参考:

Nest.js:https://docs.nestjs.cn/

API 多版本:https://docs.nestjs.cn/9/techniques?id=api-%e5%a4%9a%e7%89%88%e6%9c%ac

在日常开发中,一个非常常见的需求就是 api 开发完成后,又需要进行调整,同时要不影响旧 api 的使用,以免影响到用户体验,此时就需要对 API 进行多版本管理,实现不同版本的 api 共存,以达到兼容旧版本 api 的目的。

下面来看一下在 Nest.js 中如何实现这一点。

开始

Nest.js 支持一下 4 种形式的版本管理:

类型 说明
URI 版本类型 版本在请求的 URI 中传递 (默认)
Header 版本管理 在自定义的 Header 中传递版本
Media Type 版本管理 Accept 头部标签中声明版本
自定义版本管理 请求的任何部分都可用于指定版本,需要提供一个自定义函数来提取所述版本

下面来逐一介绍一下,并说明笔者自己的选择。

版本管理

URI 版本类型

这种方法就是在 uri 中添加版本号,例如https://example.com/v1/route,这里的 v1就是版本号

以下是具体写法。

const app = await NestFactory.create(AppModule);
// or "app.enableVersioning()" 
app.enableVersioning({
  type: VersioningType.URI,
});
await app.listen(3000);

在 uri 中添加版本号是非常有效且常见的做法,可以非常方便的通过路由来划分不同版本的 api。

但缺点也很明显,那就是在 uri 中写死了版本号,不利于后续的升级。

Header 版本类型

在 Header 版本管理中,使用用户指定的自定义头部标签来标明使用的请求版本,即:


const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.HEADER,
  header: 'Api-Version', // 在 header 中标记的 Api-Version 标签就是指定的版本号
});
await app.listen(3000);

笔者认为这种写法是相当不错的,可以复用旧版本的 url,只需要添加一个 Api-Version 就可以了。

不过缺点就是依赖于后端对 Api-Version 的解析,前端如果到处都要改的话工作量也不小。

Media Type 版本类型

Accept 头部标签内, 版本将与媒体类型用分号;分隔。它应该包含一个键值对,表示要用于请求的版本,例如 Accept: application/json;v=2。在配置包含键和分隔符的版本时,可以设置 key 的值作为键的前缀值。

跟 Header 版本类似的是,Media 版本类型使用 Accept 头部标签来声明请求的版本,即:


const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.MEDIA_TYPE,
  key: 'v=',
});
await app.listen(3000);

在笔者看来,提供 Media 版本管理和通过 Header 版本管理并无太大区别,故优缺点也类似,不再赘述。

自定义版本类型

最后则是自定义版本控制,想要怎么实现版本控制就自己写。

自定义版本控制使用请求的任何部分来指定一个或多个版本,并使用返回字符串或字符串数组的 extractor 函数分析传入请求。

// Example extractor that pulls out a list of versions from a custom header and turns it into a sorted array.
// This example uses Fastify, but Express requests can be processed in a similar way.
const extractor = (request: FastifyRequest): string | string[] =>
  [request.headers['custom-versioning-field'] ?? '']
     .flatMap(v => v.split(','))
     .filter(v => !!v)
     .sort()
     .reverse()

const app = await NestFactory.create(AppModule);
app.enableVersioning({
  type: VersioningType.CUSTOM,
  extractor,
});
await app.listen(3000);

忽略版本控制

在实际开发中,很多时候接口升级都是逐步的,并不会一次性升级全部接口,此时,对于旧版本的接口,以前并没有做版本控制的接口,也需要做一个兼容,让它们继续工作。

有的控制器或路由不关心接口版本并且无论版本如何,都将具有相同的功能,为了适应这种情况,可以将版本设置为 VERSION_NEUTRAL

传入的请求将映射到 VERSION_NEUTRAL 控制器或路由,而不管请求中发送的版本如何,或者请求中根本不包含版本信息。

app.enableVersioning({
  defaultVersion: VERSION_NEUTRAL // 将全局的默认版本指定为 VERSION_NEUTRAL 即可
});

此时需要注意一个问题,要将设置了版本号的接口放在没设置版本的接口前面,以优先匹配到有版本的接口,参考如下:

import { Controller, Get, Version } from '@nestjs/common'
import { AppService } from './app.service'

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) { }

    @Version('1')
    @Get('/')
    getHelloV1() {
        return this.appService.getHello('1')
    }

    @Version('2')
    @Get('/')
    getHelloV2() {
        return this.appService.getHello('2')
    }

    @Get('/')
    getHello() {
        return this.appService.getHello()
    }

}

此时如果用户传递的版本号为1,那么就执行 getHelloV1(),为 2 就执行 getHelloV2();如果什么都不传就执行 getHello()

这样一来就可以逐步对旧版本 api 进行升级而不影响到原有接口了。

结语

在 api 的开发中,难免需要对 api 进行升级,但又要避免影响到原有用户,所以需要对 api 进行版本管理。通常,在一个项目刚开始的时候是没有对 api 进行版本管理的,所以后续升级的时候往往都是逐步升级的,因此要避免对默认版本的影响,只对设置了版本号的接口进行改动,尽可能减少影响范围。


评论

发表回复