在常见的 MVC 架构中,C 即代表控制器,控制器用于负责解析用户的输入,处理后返回相应的结果。

image.png

常见的有:

  • RESTful (opens new window) 接口中,控制器接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
  • 在 HTML 页面请求中,控制器根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
  • 在代理服务器中,控制器将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。


一般来说,控制器常用于对用户的请求参数做一些校验,转换,调用复杂的业务逻辑,拿到相应的业务结果后进行数据组装,然后返回。

在 Midway 中,控制器也承载了路由的能力,每个控制器可以提供多个路由,不同的路由可以执行不同的操作。

在接下去的示例中,我们将演示如何在控制器中创建路由。

# 路由


控制器文件一般来说在 src/controller  目录中,我们可以在其中创建控制器文件。Midway 使用 @Controller() 装饰器标注控制器,其中装饰器有一个可选参数,用于进行路由前缀(分组),这样这个控制器下面的所有路由都会带上这个前缀。

同时,Midway 提供了方法装饰器用于标注请求的类型。

比如,我们创建一个首页控制器,用于返回一个默认的 / 路由的页面。

➜  my_midway_app tree
.
├── src
│   └── controller
│       └── home.ts
├── test  
├── package.json  
└── tsconfig.json

// src/controller/home.ts

import { Controller, Get, Provide } from '@midwayjs/decorator';

@Provide()
@Controller('/')
export class HomeController {

  @Get('/')
  async home() {
    return "Hello Midwayjs!";
  }
}


@Controller 装饰器用户告诉框架,这是一个 Web 控制器类型的类,而 @Get 装饰器告诉框架,被修饰的 home 方法,将被暴露为 / 这个路由,可以由 GET 请求来访问。

整个方法返回了一个字符串,在浏览器中你会收到 text/plain 的响应类型,以及一个 200 的状态码。

# 路由方法


上面的示例,我们已经创建了一个 GET 路由。一般情况下,我们会有其他的 HTTP Method,Midway 提供了更多的路由方法装饰器。

// src/controller/home.ts

import { Controller, Get, Provide } from '@midwayjs/decorator';

@Provide()
@Controller('/')
export class HomeController {

  @Get('/')
  async home() {
    return 'Hello Midwayjs!';
  }
  
  @Post('/update')
  async updateData() {
    return 'This is a post method'
  }
}


Midway 还提供了其他的装饰器, @Get 、 @Post 、 @Put() 、 @Del() 、 @Patch() 、 @Options() 、 @Head()  和 @All() ,表示各自的 HTTP 请求方法。

@All 装饰器比较特殊,表示能接受以上所有类型的 HTTP Method。

你可以将多个路由绑定到同一个方法上。

@Get('/')
@Get('/main')
async home() {
  return 'Hello Midwayjs!';
}

# 请求参数


接下去,我们将创建一个关于用户的 HTTP API,同样的,创建一个 src/controller/user.ts  文件,这次我们会增加一个路由前缀,以及增加更多的请求类型。

我们以用户类型举例,先增加一个用户类型,我们一般会将定义的内容放在 src/interface.ts 文件中。

➜  my_midway_app tree
.
├── src
│   ├── controller
│   │   ├── user.ts
│   │   └── home.ts
│   └── interface.ts
├── test  
├── package.json  
└── tsconfig.json

// src/interface.ts
export interface User {
	id: number;
  name: string;
  age: number;
}

再添加一个路由前缀以及对应的控制器。

// src/controller/user.ts

import { Controller, Provide } from "@midwayjs/decorator";

@Provide()
@Controller('/api/user')
export class UserController {
	// xxxx
}



接下去,我们要针对不同的请求类型,调用不同的处理逻辑。除了请求类型之外,请求的数据一般都是动态的,会在 HTTP 的不同位置来传递,比如常见的 Query,Body 等。

Midway 添加了常见的动态取值的装饰器,我们以 @Query 装饰器举例, @Query 装饰器会获取到 URL 中的 Query 参数部分,并将它赋值给函数入参。下面的示例,id 会从路由的 Query 参数上拿,如果 URL 为 /?id=1 ,则 id 的值为 1,同时,这个路由将会返回 User 类型的对象。

// src/controller/user.ts

import { Controller, Provide, Get, Query } from "@midwayjs/decorator";

@Provide()
@Controller('/api/user')
export class UserController {
	@Get('/')
  async getUser(@Query() id: string): Promise<User> {
    // xxxx
  }
}


@Query  装饰器的有参数,可以传入一个指定的字符串 key,获取对应的值,赋值给入参,如果不传入,则默认的字符串 key 为参数名。

// 下面两种写法相同
async getUser(@Query() id: string)
async getUser(@Query('id') id: string)

// 可以修改参数名,这个时候为了取到值就必须修改装饰器的参数
// URL = /?id=1
async getUser(@Query('id') uid: string)  // uid = 1

有时候,我们希望拿到整个 Query 对象的值,Midway 提供了一个特殊的 key,用于指定获取整个对象。

import { ALL } from "@midwayjs/decorator";

async getUser(@Query(ALL) queryObject: object)  // queryObject = {"id": 1}

Midway 提供了更多从 Query、Body 、Header 等位置获取值的装饰器,这些都是开箱即用,并且适配于不同的上层 Web 框架。

下面是这些装饰器,以及对应的等价框架取值方式。

装饰器 Express Koa/EggJS
@Session(key?: string) req.session / req.session[key] ctx.session / ctx.session[key]
@Param(key?: string) req.params / req.params[key] ctx.params / ctx.params[key]
@Body(key?: string) req.body / req.body[key] ctx.body / ctx.body[key]
@Query(key?: string) req.query / req.query[key] ctx.query / ctx.query[key]
@Queries(key?: string) 无 / ctx.queries[key]
@Headers(name?: string) req.headers / req.headers[name] ctx.headers / ctx.headers[name]

TIP

注意:ALL 这个 key 这些装饰器都可用, ALL  和 All  是不同的, ALL 用来获取到所有的属性,是一个变量,而 All 是一个装饰器,用于匹配所有 method 的请求。

WARNING

**注意 **@Queries 装饰器和 @Query 有所区别

Queries 会将相同的 key 聚合到一起,变为数组。当用户访问的接口参数为 /?name=a&name=b 时,@Queries 会返回 {name: [a, b]},而 Query 只会返回 {name: b}



**示例:获取单个 body**
@Post('/')
async updateUser(@Body() id: string): Promise<User> {
  // id 等价于 ctx.request.body.id
}

示例:所有 body 参数

@Post('/')
async updateUser(@Body(ALL) user: User): Promise<User> {
  // user 等价于 ctx.request.body 整个 body 对象
}

示例:获取 query 和 body 参数

装饰器可以组合使用。

@Post('/')
async updateUser(@Body(ALL) user: User, @Query() pageIdx: number): Promise<User> {
  // user 从 body 获取
  // pageIdx 从 query 获取
}

示例:获取 param 参数

@Get('/api/user/:uid')
async findUser(@Param() uid: string): Promise<User> {
  // uid 从路由参数中获取
}


还有一些比较常见的参数装饰器,以及它们的对应方法。

装饰器 Express Koa/EggJS
@RequestPath req.baseurl ctx.path
@RequestIP req.ip ctx.ip


示例:获取 body 、path 和 ip

@Post('/')
async updateUser(
  @Body() id: string,
  @RequestPath() p: string, 
  @RequestIP() ip: string): Promise<User> {

}

# 状态码


默认情况下,响应的状态码总是200,我们可以通过在处理程序层添加 @HttpCode 装饰器来轻松更改此行为。

import { Controller, Get, Provide, HttpCode } from "@midwayjs/decorator";

@Provide()
@Controller('/')
export class HomeController {

  @Get('/')
  @HttpCode(201)
  async home() {
    return "Hello Midwayjs!";
  }
}


TIP

状态码装饰器不能在响应流关闭后(response.end之后)修改。

# 响应头


Midway 提供 @SetHeader 装饰器来简单的设置自定义响应头。

import { Controller, Get, Provide, SetHeader } from "@midwayjs/decorator";

@Provide()
@Controller('/')
export class HomeController {

  @Get('/')
  @SetHeader('x-bbb', '123')
  async home() {
    return "Hello Midwayjs!";
  }
}


当有多个响应头需要修改的时候,你可以直接传入对象。

import { Controller, Get, Provide, SetHeader } from "@midwayjs/decorator";

@Provide()
@Controller('/')
export class HomeController {

  @Get('/')
  @SetHeader({
  	'x-bbb': '123',
    'x-ccc': '234'
  })
  async home() {
    return "Hello Midwayjs!";
  }
}


TIP

响应头装饰器不能在响应流关闭后(response.end之后)修改。

# 重定向


如果需要简单的将某个路由重定向到另一个路由,可以使用 @Redirect 装饰器。 @Redirect 装饰器的参数为一个跳转的 URL,以及一个可选的状态码,默认跳转的状态码为 302 。

import { Controller, Get, Provide, Redirect } from "@midwayjs/decorator";

@Provide()
@Controller('/')
export class LoginController {
  
  @Get('/login_check')
  async check() {
    // TODO
  }

  @Get('/login')
  @Redirect('/login_check')
  async login() {
    // TODO
  }
  
  @Get('/login_another')
  @Redirect('/login_check', 302)
  async loginAnother() {
    // TODO
  }
}

TIP

重定向装饰器不能在响应流关闭后(response.end之后)修改。


# 响应类型


虽然浏览器会自动根据内容判断最佳的响应内容,但是我们经常会碰到需要手动设置的情况。我们也提供了 @ContentType 装饰器用于设置响应类型。

import { Controller, Get, Provide, ContentType } from "@midwayjs/decorator";

@Provide()
@Controller('/')
export class HomeController {
  
  @Get('/')
  @ContentType('html')
  async login() {
    return '<body>hello world</body>';
  }
}

TIP

响应类型装饰器不能在响应流关闭后(response.end之后)修改。

# 优先级


midway 已经统一对路由做排序,通配的路径将自动降低优先级,在最后被加载。

规则如下:

  • 1、绝对路径规则优先级最高如 /ab/cb/e
  • 2、星号只能出现最后且必须在/后面,如 /ab/cb/**
  • 3、如果绝对路径和通配都能匹配一个路径时,绝对规则优先级高,比如 /abc/*/abc/d,那么请求 /abc/d 时,会匹配到后一个绝对的路由
  • 4、有多个通配能匹配一个路径时,最长的规则匹配,如 /ab/**/ab/cd/** 在匹配 /ab/cd/f 时命中 /ab/cd/**
  • 5、如果 //* 都能匹配 / ,但 / 的优先级高于 /*
  • 6、如果都为通配,但是其余权重都一样,比如 /:page/page/page/:page ,那么两者权重等价,以编码加载顺序为准


此规则也与 Serverless 下函数的路由规则保持一致。

简单理解为,“明确的路由优先级最高,长的路由优先级高,通配的优先级最低”。

比如:

@Controller('/api')
export class APIController {
	@Get('/invoke/*')
  async invokeAll() {
  }
  
  @Get('/invoke/abc')
  async invokeABC() {
  }
}

这种情况下,会先注册 /invoke/abc ,保证优先级更高。

不同的 Controller 的优先级,我们会以长度进行排序, / 根 Controller 我们将会最后加载。