Router 将 Request 基于 HTTP Method 和 URL 分发到不同的 Controller 上,Controller 的职责就是:接收请求,返回响应。
Controller 的主要职责
Controller 一般有三种使用场景:
- 在 RESTful 接口中,负责返回或者存储 数据
- 在页面请求中,返回 模板 给浏览器直接渲染
- 作为代理服务器,将请求转发给其它服务,并将结果返回给客户端
在 Egg.js 的设计思想中,Controller 只负责接收请求,调用其它 API 计算后返回响应,复杂的业务逻辑应该放到 Service 中。
因此 Controller 要做的就是:
- 获取请求参数
- 检验、组装参数
- 调用 Service 处理业务
- 转换 Service 返回的结果
- 返回响应给客户端
下面是一个典型的响应请求的过程:
import { Controller } from 'egg'
export default class HomeController extends Controller {
public async index() {
const { ctx, service } = this
// 获取参数
const userInfo = ctx.request.body;
// 校验参数
ctx.assert(userInfo && userInfo.name, 422, 'user name is required.')
// 调用 Service 进行业务处理
const result = await service.user.create(userInfo)
// 响应内容和响应码
ctx.body = result
ctx.status = 201
}
}
Controller 的实例化
在配置路由时,我们通过controller.home.index
这种形式来指定处理请求的方法。
这里虽然访问了HomeController
的实例home
,但实际上只有请求真实发生时才会真正实例化控制器。
并且这个实例化还是延迟的:只有当请求真正需要用到控制器器时才会实例化。
这意味着,在执行外层中间件时控制器并没有被实例化?。
虽然每一个请求都需要实例化控制器,但是官方说这个性能损耗可以忽略不计。
文件系统与挂载规则
Controller 默认都存放在 app/controller 目录下,并挂载到app.controller
对象上。
例如 app/controller/home.ts 中定义的控制器是HomeController
,它的实例的挂载位置是app.controller.home
。
文件名会自动转换为 camelCase 格式:
- app/controller/chinese_user.ts =>
app.controller.chineseUser
- app/controller/ChineseUser.ts =>
app.controller.chineseUser
文件可以分级存放,对应的实例挂载位置将会自动分级:
- app/controller/chinese/user.ts =>
app.controller.chinese.user
基本属性
ctx
(Context), app
(Application), service
(Service), config
(Config), logger
(Logger).
请求相关的参数
请求实例挂载到了ctx.request
上,响应实例挂载到了ctx.response
上。
Context 也提供了许多便捷方法来访问请求相关的参数。
查询参数
查询参数对象挂载到ctx.query
上,它是一个Record<string, string>
类型,只能传递字符串。
当参数的 key 重复时,ctx.query
只会取第一次匹配上的 value。
此外还有一个ctx.queries
用来解析重复的 key,它的类型总是Record<string, string[]>
,即使 key 只出现了一次。
路由参数
路径上的参数,挂载到ctx.params
上,value 的类型总是string
。
请求体
GET 请求只能通过查询参数或者路由参数上传数据,这不适合用来传递大数据、结构复杂的数据、二进制数据或者敏感数据。
上面这些数据应该用 POST 请求来传输,因为它提供了一个 请求体 用来存储这些复杂的数据。
此外,PUT, DELETE 方法也可以使用请求体,而 GET HEAD 方法没有请求体。
使用请求体还需要在请求头中附带一个Content-Type
字段告诉服务器请求体的数据类型,例如 JSON, Form。
Egg.js 提供了 bodyParser 中间件来解析请求体,并挂载到了ctx.request.body
上。
请求体会被解析为一个 Object (对象或者数组),框架插件会根据Content-Type
自动解析:
application/json
,application/json-patch+json
,application/vnd.api+json
,application/csp-report
类型的请求体会按照 JSON 格式解析,且请求体最大长度为 100Kbapplication/x-www-form-urlencoded
类型的请求体会按照 Form 格式解析,且请求体最大长度为 100Kb
修改请求体的最大长度:
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
config.bodyParser = {
jsonLimit: '1mb',
formLimit: '1mb'
}
}
如果请求体超过设置大小,会抛出 413 异常:
HTTP 413 (Payload Too Large) 表示请求主体的大小超过了服务器愿意或有能力处理的限度,服务器可能会关闭连接以防止客户端继续发送该请求。
如果请求体解析失败,会抛出 400 异常:
HTTP 400 (Bad Request) 响应状态码表示由于语法无效,服务器无法理解该请求。 客户端不应该在未经修改的情况下重复此请求。
上述只是修改 Egg 框架本身的请求体大小限制,在生成环境下,服务可能部署在例如 Nginx 服务器下,还需要修改代理服务器的限制。
请求体的挂载位置是ctx.request.body
,而ctx.body
指的是响应体。
上传文件 *
当客户端发起上传文件的请求时,Egg.js 的 Multipart 插件用来解析 请求体 中的文件。
上传文件的请求的Content-Type
一般是Multipart/form-data
。
没有细读,可参考这里。
请求 Headers
业务数据一般通过参数和请求体传递,此外,请求头 中也可以传递一些参数。
由于这些参数的 key 基本上都是固定的,因此框架提供了它们的快捷访问方式。
请求头的挂载位置:ctx.header
, ctx.headers
, ctx.request.header
or ctx.request.headers
。
获取一个字段的值,不存在时会返回空字符串:ctx.get(key)
or ctx.request.get(key)
。
使用ctx.get
或者ctx.request.get
方法获取参数,而不是通过索引的方式。
ctx.host
发起请求的域名。
先读取config.hostHeaders
配置的值,该配置将默认读取x-forwarded-host
头。
读取失败则读取host
头。
还是失败则返回空字符串。
ctx.protocol
如果当前连接是加密连接,则直接返回 https。
如果不是加密连接则根据config.protocolHeaders
配置读取,该配置默认读取x-forwarded-proto
头。
如果读取失败,则读取config.protocol
的值,该值默认为 http。
ctx.ips
请求经过的所有中间设备的 IP 地址列表。
设置config.proxy = true
启用这个 Getter,因为只有在请求经过代理时才有访问这个参数的必要。
这个参数通过config.ipsHeaders
配置读取,该配置默认读取x-forwarded-for
头。
如果读取失败,则返回空数组。
ctx.ip
请求发起方的 IP 地址,可能是客户端,也可能是代理。
优先从ctx.ips
中读取,所以安全不能保证。
ctx.ips
为空数组时,直接读取请求发起方的地址(remoteAddress)。
Cookie
一个原始的 HTTP 请求是无状态的:每次请求都需要带上用户的身份信息,虽然之前可能发起过一次登录请求,然而服务器处理之后就忘记了。
每次请求都带上用户的敏感信息无疑是愚蠢的,于是 HTTP 协议就设计了一个请求头 Cookie。
在用户进行登录验证时,服务器会将少量数据附带在响应中返回给浏览器,这部分数据由浏览器负责管理,在用户下次请求时自动附带上,然后由服务器验证,以达到校验用户身份的目的。
ctx.cookies
对象用来管理 Cookie 数据,它暴露了get
和set
方法用来获取和设置 Cookie 数据。
Cookie 头本身只是一个字符串,但是框架提供了对象式的访问和设置方法。
在 config/config.default.ts 中通过config.cookies
来配置 Cookie:
// config.cookie =
{
httpOnly: boolean;
sameSite: 'none' | 'lax' | 'strict';
}
Session
Cookie 是保存在浏览器中,用于跨请求用户身份鉴定的数据。
Session 是保存在服务器中,配合 Cookie 实现跨请求用户身份鉴定的工具,它的安全性更高。
框架内置了 Session 插件,使用者可直接通过ctx.session
来访问或者修改当前用户的 Session。
在 config/config.default.ts 中配置 Session:
config.key
: 使用指定键将 Session 信息存入 Cookie 中config.maxAge
: Session 的最大有效时间
参数校验
框架提供了许多工具对请求中的参数进行校验,校验功能由 Validate 插件提供。
启用插件:
const plugin = {
validate: {
enable: true,
package: 'egg-validate'
}
}
使用校验:
ctx.validate(rule, body?)
rule 配置对字段的校验规则;body 指定校验的参数对象,默认为ctx.request.body
。
例如ctx.validate({name: {type: 'string'}, age: {type: 'number'}}, ctx.query)
表示检验 查询参数 对象的 name 字段和 age 字段的数据类型。
调用 Service
Service 负责具体的业务逻辑,在任何 Controller 上都能调用任意一个 Service 的方法,并且调用是惰性的,只有调用时才会实例化 Service 对象。
例如DemoService
类的实例的挂载位置默认为ctx.service.demo
。
响应
设置状态码
ctx.status
(number)
设置响应体
业务数据都是通过 Response Body 返回给浏览器的。
设置响应体数据时也需要设置 Content-Type 响应头,告诉浏览器怎样解析响应体。
框架中通过ctx.body
或者ctx.response.body
设置响应体数据。
除了返回对象或者字符串外,响应体还可以设置为 stream,用来返回文件。
使用ctx.render
方法返回 HTML 模板。
支持 JSONP 设置。
设置 Header
ctx.set(key: string, value: string)
设置一个响应头ctx.set(headers: Record<string, string>)
设置多个响应头
重定向
ctx.redirect(url)
只有在白名单中才能成功重定向ctx.unsafeRedirect(url)
不检查目标地址直接重定向
报名单配置方法:
config.security = {
domainWhiteList: ['.domain.com'] // 以 . 开头
}
ctx.redirect
只有在配置了白名单时才会执行检查,如果没有配置白名单或者白名单为空数组,则不进行检查,此时相当于crx.unsafeRedirect
。
参考资料
http://www.wangchonghaha.cn/bookstact/JsServer/Eggjs/controller.html
评论区