侧边栏壁纸
  • 累计撰写 218 篇文章
  • 累计创建 59 个标签
  • 累计收到 5 条评论

Egg09 - Controller

barwe
2022-04-20 / 0 评论 / 0 点赞 / 876 阅读 / 5,019 字
温馨提示:
本文最后更新于 2022-04-20,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Router 将 Request 基于 HTTP Method 和 URL 分发到不同的 Controller 上,Controller 的职责就是:接收请求,返回响应。

Controller 的主要职责

Controller 一般有三种使用场景:

  1. 在 RESTful 接口中,负责返回或者存储 数据
  2. 在页面请求中,返回 模板 给浏览器直接渲染
  3. 作为代理服务器,将请求转发给其它服务,并将结果返回给客户端

在 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 格式解析,且请求体最大长度为 100Kb
  • application/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 数据,它暴露了getset方法用来获取和设置 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

0

评论区