目录

24.接口如何实现多版本共存

目录

应用开发完一版上线之后,还会不断的迭代。

后续可能需要修改已有的接口,但是为了兼容,之前版本的接口还要保留。

那如何同时支持多个版本的接口呢?

Nest 内置了这个功能,我们来试一下:

nest new version-test

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dbc6fecf784a40ffbd3c823017362a69~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=856&h=710&s=260160&e=png&b=010101

创建个 nest 项目。

进入项目,创建 aaa 模块:

nest g resource aaa --no-spec

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf3332080b664801b11e248725f7a073~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=774&h=360&s=88876&e=png&b=191919

把服务跑起来:

npm run start:dev

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/21b85d2cd9344f2384a446d9b2e58367~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1590&h=548&s=232419&e=png&b=181818

postman 里访问下:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4d41c68443f040309f25c13feba419a2~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=646&h=494&s=40222&e=png&b=fcfcfc

这是版本一的接口。

假设后面我们又开发了一版接口,但路由还是 aaa,怎么做呢?

这样:

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c250308ccd844d718012f31be1936210~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1046&h=1046&s=203587&e=png&b=1f1f1f

在 controller 上标记为 version 1,这样默认全部的接口都是 version 1。

然后单独用 @Version 把 version 2 的接口标识一下。

在 main.ts 里调用 enableVersioning 开启接口版本功能:

import { VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableVersioning({
    type: VersioningType.HEADER,
    header: 'version'
  })
  await app.listen(3000);
}
bootstrap();

开启接口版本功能,指定通过 version 这个 header 来携带版本号。

测试下:

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ceb66b0e2054af1b380f3941dabf294~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=834&h=744&s=71130&e=png&b=fbfbfb

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d4017c479d2462aaaa3be46395b33e3~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=780&h=738&s=68871&e=png&b=fafafa

可以看到,带上 version:1 的 header,访问的就是版本 1 的接口。

带上 version:2 的 header,访问的就是版本 2 的接口。

它们都是同一个路由。

但这时候有个问题:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0820e02311694cb99587de5e9bd47d50~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=844&h=834&s=86331&e=png&b=fbfbfb

如果不带版本号就 404 了。

这个也很正常,因为这就是版本一的接口嘛,只有显式声明版本才可以。

如果你想所有版本都能访问这个接口,可以用 VERSION_NEUTRAL 这个常量:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b8a0b312a954fcd9a06fa6ae61f3fc2~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=952&h=940&s=177909&e=png&b=1f1f1f

现在带不带版本号,不管版本号是几都可以访问这些接口:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74e38dbd860340ce92b733371d2a1e12~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=820&h=662&s=65007&e=png&b=fafafa

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ee57c76be2e144d09b5b5cf066443f04~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=824&h=688&s=65226&e=png&b=fafafa

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a46469a0d5854a0eaba556af9fb40b4a~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=860&h=682&s=67222&e=png&b=fbfbfb

但是现在因为从上到下匹配,版本 2 的接口不起作用了:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04e8fc6531764dcfbce0743dc3923d58~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=740&h=820&s=117414&e=png&b=1f1f1f

这时候或者可以把它移到上面去:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2e86f2604ee4129807e98954a972575~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=754&h=864&s=120678&e=png&b=1f1f1f

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e27d6c8fdbe74286a697fc4f9179708c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=782&h=690&s=64622&e=png&b=fbfbfb

或者单独建一个 version 2 的 controller

nest g controller aaa/aaa-v2 --no-spec --flat

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4e1a6c503ba1493b93aee2f5d2dcae8b~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=822&h=122&s=29419&e=png&b=191919

把 AaaController 里 version 2 的接口删掉,移到这里来:

import { Controller, Get,Version } from '@nestjs/common';
import { AaaService } from './aaa.service';

@Controller({
    path: 'aaa',
    version: '2'
})
export class AaaV2Controller {
    constructor(private readonly aaaService: AaaService) {}

    @Get()
    findAllV2() {
      return this.aaaService.findAll() + '222';
    }
}

现在版本 2 就走的 AaaV2Controller:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9be3257c23fc4f52956cc93a8190c64f~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=920&h=688&s=71303&e=png&b=fbfbfb

其他版本走 AaaController:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/473e09b4438e44a483171d7804b91ad8~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=748&h=650&s=61604&e=png&b=fafafa

一般我们就是这样做的,有一个 Controller 标记为 VERSION_NEUTRAL,其他版本的接口放在单独 Controller 里。

注意,controller 之间同样要注意顺序,前面的 controller 先生效:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/023021b745054ed99b8184fa4ebfc9a0~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1054&h=422&s=101609&e=png&b=1f1f1f

试一下:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/860fb89a3c8e43f8921e4fffe6ed80e9~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=842&h=538&s=56896&e=png&b=fafafa

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3edf83a92cf5454cb86fb9ec9820c931~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=758&h=572&s=54199&e=png&b=fafafa

除了用自定义 header 携带版本号,还有别的方式:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4ebe215f03746fc84767f688d362f77~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=908&h=680&s=124059&e=png&b=1f1f1f

app.enableVersioning({
    type: VersioningType.MEDIA_TYPE,
    key: 'vv='
})

MEDIA_TYPE 是在 accept 的 header 里携带版本号:

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a7ff07b6eff24d84b1f463a15159b35e~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=876&h=656&s=68634&e=png&b=fbfbfb

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/89e5cbf05f164f90b92ef10181241ce0~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=878&h=652&s=67990&e=png&b=fbfbfb

你也可以用 URI 的方式:

app.enableVersioning({
    type: VersioningType.URI
})

https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2862e5a2ccce4c9fb7b4033fa51aaea5~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=788&h=586&s=55320&e=png&b=fcfcfc

但是这种方式不支持 VERSION_NEUTRAL,你要指定明确的版本号才可以:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a74148c256fd4ee5a09cd17d5ea1c30c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=694&h=664&s=66147&e=png&b=fcfcfc

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5b1954e36140460689f19854d62c1927~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=838&h=528&s=113934&e=png&b=1f1f1f

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3ce8e4c765354d06a654225b02a8106c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=820&h=566&s=55477&e=png&b=fbfbfb

https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cf2075b1dca3440a9b7da62add45e185~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=706&h=564&s=51386&e=png&b=fafafa

此外,如果觉得这些指定版本号的方式都不满足需求,可以自己写:

import { VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Request } from 'express';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const extractor = (request: Request)=> {
    if(request.headers['disable-custom']) {
      return '';
    }
    return request.url.includes('guang') ? '2' : '1';
  }

  app.enableVersioning({
    type: VersioningType.CUSTOM,
    extractor
  })

  await app.listen(3000);
}

bootstrap();

我们自己实现了一个版本号的逻辑,如果 url 里包含 guang,就返回版本 2 的接口,否则返回版本 1 的。

此外,如果有 disable-custom 的 header 就返回 404。

试一下:

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c67fc216cb0945f995bdf277e1e320be~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=782&h=496&s=46961&e=png&b=fafafa

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/db6ff09759d545c5b4d6f1cb62d1c61c~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=718&h=482&s=42144&e=png&b=fbfbfb

https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f8f5eca724f4f4bb4102c5488829f28~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=830&h=604&s=66267&e=png&b=fbfbfb

这样,就能实现各种灵活的版本号规则。

案例代码在小册仓库

总结

今天我们学了如何开发一个接口的多个版本。

Nest 内置了这个功能,同一个路由,指定不同版本号就可以调用不同的接口。

只要在 main.ts 里调用 enableVersioning 即可。

有 URI、HEADER、MEDIA_TYPE、CUSTOM 四种指定版本号的方式。

HEADER 和 MEDIA_TYPE 都是在 header 里置顶,URI 是在 url 里置顶,而 CUSTOM 是自定义版本号规则。

可以在 @Controller 通过 version 指定版本号,或者在 handler 上通过 @Version 指定版本号。

如果指定为 VERSION_NEUTRAL 则是匹配任何版本号(URI 的方式不支持这个)。

这样,当你需要开发同一个接口的多个版本的时候,就可以用这些内置的功能。