Skip to content

路由参数

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

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

ts

➜  my_midway_app tree
.
├── src
│   ├── controller
│   │   ├── user.ts
│   │   └── home.ts
│   └── interface.ts
├── test
├── package.json
└── tsconfig.json
  • interface.ts
ts
// src/interface.ts
export interface IUser {
  id: number;
  name: string;
  age: number;
}

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

ts
// src/controller/user.ts

import { Controller } from "@midwayjs/core";

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

接下去,我们要针对不同的请求类型,调用不同的处理逻辑。

除了请求类型之外,请求的数据一般都是动态的,会在 HTTP 的不同位置来传递,比如常见的 Query,Body 等。

装饰器参数约定

MidWay 添加了常见的动态取值的装饰器,我们以@Query装饰器举例,@Query装饰器会获取到 URL 中的 Query 参数部分

并且将赋值给函数入参,下面的示例中

id 会从路由的Query参数上拿,如果 URL 为?id=1则 id 的值为 1,同时这个路由会返回User类型的对象

ts
// src/controller/user.ts

import { Controller, Get, Query } from "@midwayjs/core";
import { IUser } from "../interface";

@Controller("/api/user")
export class UserController {
  @Get("/")
  async getdata(@Query("id") id: string): Promise<IUser> {
    //xxxxx
    return {
      id: 2,
      name: "jsopy",
      age: 36,
    };
  }
}

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

ts
// URL = /?id=1
async getUser(@Query('id') id: string) // id = 1
async getUser(@Query() queryData) // {"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.request.body / ctx.request.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]

警告

注意 EGG 和其他框架不同,@Query@Queries是有区别的

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

Query

在 URL 中?后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数,例如

ts
GET /user?uid=1&sex=male

就是用户传递过来的参数

示例从装饰器获取

  • 第一种获取单个
ts
// src/controller/user.ts
import { Controller, Get, Query } from "@midwayjs/core";

@Controller("/user")
export class UserController {
  @Get("/")
  async getUser(@Query("uid") uid: string): Promise<User> {
    // xxxx
  }
}
  • 第二种获取全部
ts
import { Controller, Get, Query } from "@midwayjs/core";
import { IUser } from "../interface";

@Controller("/api/user")
export class UserController {
  @Get("/")
  async getdata(@Query() queryall): Promise<IUser> {
    //xxxxx
    return {
      id: Number(queryall.id),
      name: queryall.name,
      age: Number(queryall.age),
    };
  }
}

示例从上下文获取

ts
// src/controller/user.ts
import { Controller, Get, Inject } from "@midwayjs/core";
import { IUser } from "../interface";
// 引入上下文
import { Context } from "@midwayjs/koa";

@Controller("/api/user")
export class UserController {
  // 注入上下文
  @Inject()
  ctx: Context;

  // Get请求
  @Get("/")
  async getdata(): Promise<IUser> {
    //xxxxx
    console.log(this.ctx.query);
    return {
      id: Number(this.ctx.query.id),
      name: String(this.ctx.query.name),
      age: Number(this.ctx.query.age),
    };
  }
}

警告

注意 EggJS 和其他框架不同,在当 Query String 中的 key 重复的时候,只取 key 第一次出现时的值,后面再出现的都会被忽略. 比如GET/user?uid=12345&uid=2通过ctx.query拿到的值就是{uid:'1'}

Body

虽然我们可以通过 URL 传递参数,但是还是有诸多限制

  • 浏览器中会对 URL 的长度有所限制,如果需要传递的参数过多就会无法传递。

  • 服务端经常会将访问的完整 URL 记录到日志文件中,有一些敏感数据通过 URL 传递会不安全。

在前面的 HTTP 请求报文示例中,我们看到在 header 之后还有一个 body 部分,我们通常会在这个部分传递 POSTPUTDELETE 等方法的参数。一般请求中有 body 的时候,客户端(浏览器)会同时发送 Content-Type 告诉服务端这次请求的 body 是什么格式的。Web 开发中数据传递最常用的两类格式分别是 JSONForm

框架内置了 bodyParser 中间件来对这两类格式的请求 body 解析成 object 挂载到 ctx.request.body 上。HTTP 协议中并不建议在通过 GETHEAD 方法访问时传递 body,所以我们无法在 GETHEAD 方法中按照此方法获取到内容。

示例从装饰器获取

  • 单个 body 获取
ts
  // Post请求

  @Post('/post')
  async postdata(@Body('id') id: number): Promise<IUser> {
    return {
      id: id,
      name: '呵呵',
      age: 18,
    };
  }
  • 获取全部 body
ts
  // 获取POST全部参数
  @Post('/post1')
  async postdataall(@Body() bodyall: any): Promise<IUser> {
    return {
      id: bodyall.id,
      name: bodyall.name,
      age: bodyall.age,
    };
  }

示例从上下文获取

ts
  // 从上下文获取
  @Post('/post2')
  async postdataall2(): Promise<IUser> {
    const bodyall = this.ctx.request.body as IUser;
    console.log(bodyall);
    return {
      id: bodyall.id,
      name: bodyall.name,
      age: bodyall.age,
    };
  }

同时获取 body 和 query 参数

ts
  // 同时获取query和body
  @Post('/post3')
  async postdataall3(@Query() query: any): Promise<IUser> {
    const bodyall = this.ctx.request.body as IUser;
    return {
      id: bodyall.id,
      name: bodyall.name,
      age: bodyall.age,
      sex: query.sex,
    };
  }

框架对 bodyParser 设置了一些默认参数,配置好之后拥有以下特性:

当请求的 Content-Type 为 application/jsonapplication/json-patch+jsonapplication/vnd.api+jsonapplication/csp-report 时,会按照 json 格式对请求 body 进行解析,并限制 body 最大长度为 1mb。 当请求的 Content-Typeapplication/x-www-form-urlencoded 时,会按照 form 格式对请求 body 进行解析,并限制 body 最大长度为 1mb。 如果解析成功,body 一定会是一个 Object(可能是一个数组)。

警告

常见错误: ctx.request.bodyctx.body 混淆,后者其实是 ctx.response.body 的简写。

Router Params

如果路由上使用 :xxx 的格式来声明路由,那么参数可以通过 ctx.params 获取到。

示例 从装饰器获取

ts
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Param } from "@midwayjs/core";

@Controller("/user")
export class UserController {
  @Get("/:uid")
  async getUser(@Param("uid") uid: string): Promise<User> {
    // xxxx
  }
}

从上下文获取

ts
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Inject } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";

@Controller("/user")
export class UserController {
  @Inject()
  ctx: Context;

  @Get("/:uid")
  async getUser(): Promise<User> {
    const params = this.ctx.params;
    // {
    //   uid: '1',
    // }
  }
}

除了从 URL 和请求 body 上获取参数之外,还有许多参数是通过请求 header 传递的。框架提供了一些辅助属性和方法来获取。

  • ctx.headers,ctx.header,ctx.request.headers,ctx.request.header:这几个方法是等价的,都是获取整个 header 对象。

  • ctx.get(name),ctx.request.get(name):获取请求 header 中的一个字段的值,如果这个字段不存在,会返回空字符串。

  • 我们建议用 ctx.get(name) 而不是 ctx.headers['name'],因为前者会自动处理大小写。

从装饰器获取

ts
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Headers } from "@midwayjs/core";

@Controller("/user")
export class UserController {
  @Get("/:uid")
  async getUser(@Headers("cache-control") cacheSetting: string): Promise<User> {
    // no-cache
    // ...
  }
}

从上下文获取

ts
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Inject } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";

@Controller("/user")
export class UserController {
  @Inject()
  ctx: Context;

  @Get("/:uid")
  async getUser(): Promise<User> {
    const cacheSetting = this.ctx.get("cache-control");
    // no-cache
  }
}

HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。

通过 ctx.cookies,我们可以在 Controller 中便捷、安全的设置和读取 Cookie

ts
import { Inject, Controller, Get, Provide } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";

@Controller("/")
export class HomeController {
  @Inject()
  ctx: Context;

  @Get("/")
  async home() {
    // set cookie
    this.ctx.cookies.set("foo", "bar", { encrypt: true });
    // get cookie
    this.ctx.cookies.get("foo", { encrypt: true });
  }
}

Session

通过 Cookie,我们可以给每一个用户设置一个 Session,用来存储用户身份相关的信息,这份信息会加密后存储在 Cookie 中,实现跨请求的用户身份保持。

框架内置了 Session 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session 。

ts
import { Inject, Controller, Get, Provide } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";

@Controller("/")
export class HomeController {
  @Inject()
  ctx: Context;

  @Get("/")
  async home() {
    // 获取 Session 上的内容
    const userId = this.ctx.session.userId;
    const posts = await this.ctx.service.post.fetch(userId);
    // 修改 Session 的值
    this.ctx.session.visited = ctx.session.visited
      ? ctx.session.visited + 1
      : 1;
    // ...
  }
}

Session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 null:

ts
ctx.session = null;

上传的文件

上传的文件一般使用 multipart/form-data 协议头,由 @Files 装饰器获取,由于上传功能由 upload 组件提供,具体可以参考 upload 组件。

其他的参数

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

装饰器Express 对应的方法Koa/EggJS 对应的方法
@RequestPathreq.baseurlctx.path
@RequestIPreq.ipctx.ip

示例:获取 body 、path 和 ip

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

}

自定义请求参数装饰器

你可以快速通过 createRequestParamDecorator 创建自定义请求参数装饰器。

ts
import { createRequestParamDecorator } from "@midwayjs/core";

// 实现装饰器
export const Token = () => {
  return createRequestParamDecorator((ctx) => {
    return ctx.headers.token;
  });
};

// 使用装饰器
export class UserController {
  async invoke(@Token() token: string) {
    console.log(token);
  }
}

请求类型转换

如果是简单类型,Midway 会自动将参数转换为用户声明的类型。

比如:

数字类型

ts
@Get('/')
async getUser(@Query('id') id: number): Promise<User> {
  console.log(typeof id)  // number
}

布尔类型

  • 当值为 0,"0", "false" 则转为 false,其余返回 Boolean(value) 的值
ts
@Get('/')
async getUser(@Query('id') id: boolean): Promise<User> {
  console.log(typeof id)  // boolean
}

Class

如果是复杂类型,如果指定的类型是 Class,将会自动转换为该类的实例。

ts
// class
class UserDTO {
  name: string;

  getName() {
    return this.name;
  }
}

@Get('/')
async getUser(@Query() query: UserDTO): Promise<User> {
  // query.getName()
}

如果不希望被转换,可以使用 Interface。

ts
interface User {
  name: string;
}

@Get('/')
async getUser(@Query() query: User): Promise<User> {
  // ...
}

参数校验

后续会说