框架内置基础对象

12003

在本章中,我们将初步了解框架内部内置的一些基础对象。这些对象包括从 [Koa] 继承而来的 4 个对象(ApplicationContextRequestResponse)以及框架扩展的其他一些对象(ControllerServiceHelperConfigLogger)。在后续的文档中,我们会经常遇到它们。

Application

Application 是全局应用对象。在一个应用中,每个进程只会实例化一个 Application 实例。它继承自 Koa.Application,在其上我们可以挂载一些全局的方法和对象。我们可以轻易地在插件或应用中扩展 Application 对象

事件

在框架运行时,会在 Application 实例上触发一些事件,应用开发者或插件开发者可以监听这些事件做一些操作。作为应用开发者,我们一般会在启动自定义脚本中进行监听:

  • server:该事件在一个 worker 进程中只会触发一次,在 HTTP 服务完成启动后,会通过这个事件将 HTTP server 暴露出来给开发者。
  • error:运行时捕获到任何异常后,都会触发 error 事件,将错误对象和关联的上下文(如果有)暴露出来,开发者可以对其进行自定义的日志记录、上报等处理。
  • requestresponse:应用收到请求和响应请求时,分别会触发 requestresponse 事件,并将当前请求的上下文暴露出来,开发者可以监听这两个事件进行日志记录。
  1. // app.js
  2. module.exports = (app) => {
  3. app.once('server', (server) => {
  4. // websocket 相关操作
  5. });
  6. app.on('error', (err, ctx) => {
  7. // 上报错误
  8. });
  9. app.on('request', (ctx) => {
  10. // 记录收到的请求
  11. });
  12. app.on('response', (ctx) => {
  13. // ctx.starttime 是由框架设置的
  14. const used = Date.now() - ctx.starttime;
  15. // 记录请求总耗时
  16. });
  17. };

获取方式

Application 对象在编写应用时几乎任何场合都能获取到,下面介绍几个常用的获取方式:

几乎所有由框架 Loader 加载的文件(Controller、Service、Schedule 等),都可以通过导出一个函数来获取 Application 实例,该函数会被 Loader 调用,并将 app 作为参数:

  • 启动自定义脚本

    1. // app.js
    2. module.exports = (app) => {
    3. app.cache = new Cache();
    4. };
  • Controller 文件

    1. // app/controller/user.js
    2. class UserController extends Controller {
    3. async fetch() {
    4. this.ctx.body = this.app.cache.get(this.ctx.query.id);
    5. }
    6. }

Koa 一样,在 Context 对象上,可以通过 ctx.app 访问到 Application 对象。以 Controller 文件为例:

  1. // app/controller/user.js
  2. class UserController extends Controller {
  3. async fetch() {
  4. this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id);
  5. }
  6. }

在继承自 Controller、Service 基类的实例中,可以通过 this.app 访问到 Application 对象。

  1. // app/controller/user.js
  2. class UserController extends Controller {
  3. async fetch() {
  4. this.ctx.body = this.app.cache.get(this.ctx.query.id);
  5. }
  6. }

Context

Context 是一个请求级别的对象,继承自 [Koa.Context]。在每次收到用户请求时,框架都会实例化一个 Context 对象。这个对象封装了这次用户请求的信息,并提供了许多便捷的方法来获取请求参数或者设置响应信息。框架会将所有的 [Service] 挂载到 Context 实例上。部分插件也会将其他方法和对象挂载至其上(如 [egg-sequelize] 会将所有的 model 挂到 Context 上)。

获取方式

获取 Context 实例的最常见方式是在 [Middleware]、[Controller] 以及 [Service] 中。我们已经在 Controller 的示例中看到了相应的获取方式。在 Service 中获取 Context 的方法与 Controller 中相同,在 Middleware 中获取 Context 实例的方法则与 [Koa] 框架的中间件中使用方法一致。

框架的 [Middleware] 支持 Koa v1 和 Koa v2 两种中间件的写法。根据不同的写法,获取 Context 实例的方式略有区别:

  1. // Koa v1
  2. function* middleware(next) {
  3. // this 为 Context 的实例
  4. console.log(this.query);
  5. yield next;
  6. }
  7. // Koa v2
  8. async function middleware(ctx, next) {
  9. // ctx 为 Context 的实例
  10. console.log(ctx.query);
  11. }

除了在处理请求时可以获得 Context 实例外,还有一些非用户请求的场景下需要访问 service / model 等 Context 实例上的对象。这时我们可以通过 Application.createAnonymousContext() 方法创建一个匿名 Context 实例:

  1. // app.js
  2. module.exports = (app) => {
  3. app.beforeStart(async () => {
  4. const ctx = app.createAnonymousContext();
  5. // 应用启动前预加载
  6. await ctx.service.posts.load();
  7. });
  8. };

定时任务中,每个 task 都接收一个 Context 实例作为参数,这样我们可以更便利地执行一些定时的业务逻辑:

  1. // app/schedule/refresh.js
  2. exports.task = async (ctx) => {
  3. await ctx.service.posts.refresh();
  4. };

Request & Response

Request 是一个请求级别的对象,继承自 [Koa.Request]。封装了 Node.js 原生的 HTTP Request 对象,提供了一系列辅助方法获取 HTTP 请求常用参数。

Response 是一个请求级别的对象,继承自 [Koa.Response]。封装了 Node.js 原生的 HTTP Response 对象,提供了一系列辅助方法设置 HTTP 响应。

获取方式

可以在 Context 的实例上获取到当前请求的 Request(ctx.request) 和 Response(ctx.response) 实例。

  1. // app/controller/user.js
  2. class UserController extends Controller {
  3. async fetch() {
  4. const { app, ctx } = this;
  5. // 获取请求中的 `id` 参数
  6. const id = ctx.request.query.id;
  7. // 设置响应体
  8. ctx.response.body = app.cache.get(id);
  9. }
  10. }
  • [Koa] 会在 Context 上代理一部分 Request 和 Response 上的方法和属性,参见 [Koa.Context]
  • 如上面例子中的 ctx.request.query.idctx.query.id 是等价的,ctx.response.body =ctx.body = 也是等价的。
  • 需要注意的是,获取 POST 的 body 应该使用 ctx.request.body,而不是 ctx.body

    Controller

框架提供了一个 Controller 基类,并推荐所有的 Controller 都继承于该基类实现。这个 Controller 基类有下列属性:

  • ctx - 当前请求的 Context 实例。
  • app - 应用的 Application 实例。
  • config - 应用的配置
  • service - 应用的所有 service
  • logger - 为当前 Controller 封装的 logger 对象。

Controller 文件中,可以通过两种方式来引用 Controller 基类:

  1. // app/controller/user.js
  2. // 从 egg 上获取(推荐)
  3. const Controller = require('egg').Controller;
  4. class UserController extends Controller {
  5. // 实现
  6. }
  7. module.exports = UserController;
  8. // 从 app 实例上获取
  9. module.exports = (app) => {
  10. return class UserController extends app.Controller {
  11. // 实现
  12. };
  13. };

Service

框架提供了一个 Service 基类,并推荐所有的 Service 都继承于该基类实现。

Service 基类的属性和 Controller 基类属性一致,访问方式也类似:

  1. // app/service/user.js
  2. // 从 egg 上获取(推荐)
  3. const Service = require('egg').Service;
  4. class UserService extends Service {
  5. // implement
  6. }
  7. module.exports = UserService;
  8. // 从 app 实例上获取
  9. module.exports = (app) => {
  10. return class UserService extends app.Service {
  11. // implement
  12. };
  13. };

Helper

Helper 用来提供一些实用的 utility 函数。它的作用在于我们可以将一些常用的动作抽离在 helper.js 里面成为一个独立的函数。这样可以利用 JavaScript 编写复杂的逻辑,避免逻辑分散于各个地方,同时便于更好地编写测试用例。

Helper 本身是一个类,具有和 Controller 基类相同的属性,它也会在每次请求时进行实例化。因此,Helper 上的所有函数也能获取到当前请求相关的上下文信息。

获取方式

可以在 Context 的实例上获取到当前请求的 Helper(ctx.helper)实例。

  1. // app/controller/user.js
  2. class UserController extends Controller {
  3. async fetch() {
  4. const { app, ctx } = this;
  5. const id = ctx.query.id;
  6. const user = app.cache.get(id);
  7. ctx.body = ctx.helper.formatUser(user);
  8. }
  9. }

除此之外,Helper 的实例还可以在模板中获取。例如,可以在模板中使用 security 插件提供的 shtml 方法。

  1. <!-- app/view/home.nj -->
  2. {{ helper.shtml(value) }}

自定义 Helper 方法

在应用开发中,我们可能经常需要自定义一些 Helper 方法。例如上面例子中的 formatUser,我们可以通过 框架扩展 的形式来自定义 Helper 方法。

  1. // app/extend/helper.js
  2. module.exports = {
  3. formatUser(user) {
  4. return only(user, ['name', 'phone']);
  5. },
  6. };

Config

我们推荐应用开发遵循配置和代码分离的原则,将一些需要硬编码的业务配置都放到配置文件中。同时,配置文件支持各个不同的运行环境使用不同的配置,使用起来也非常方便。所有框架、插件和应用级别的配置都可以通过 Config 对象获取到。关于框架的配置,可以详细阅读Config 配置章节。

获取方式

我们可以通过 app.configApplication 实例上获取到 config 对象,也可以在 Controller、Service、Helper 的实例上通过 this.config 获取到 config 对象。

Logger

框架内置了功能强大的日志功能,可以非常方便地打印各种级别的日志到对应的日志文件中,每一个 logger 对象都提供了 4 个级别的方法:

  • logger.debug()
  • logger.info()
  • logger.warn()
  • logger.error()

在框架中提供了多个 Logger 对象,下面我们简单地介绍一下各个 Logger 对象的获取方式和使用场景。

App Logger

我们可以通过 app.logger 来获取它。如果我们想做一些应用级别的日志记录,如记录启动阶段的一些数据信息,记录一些与请求无关的业务信息,都可以通过 App Logger 来完成。

App CoreLogger

我们可以通过 app.coreLogger 来获取它。一般在开发应用时,我们不应该通过 CoreLogger 打印日志。而框架和插件则需要通过它来打印应用级别的日志,这样可以更清晰地区分应用和框架打印的日志。通过 CoreLogger 打印的日志会被放到与 Logger 不同的文件中。

Context Logger

我们可以从 Context 实例上,通过 ctx.logger 获取它。从访问方式上我们可以看出,Context Logger 一定是与请求相关的。它打印的日志都会在前面带上一些当前请求相关的信息(如 [$userId/$ip/$traceId/${cost}ms $method $url])。通过这些信息,我们可以从日志快速定位请求,并串联一次请求中的所有日志。

Context CoreLogger

我们可以通过 ctx.coreLogger 获取它。它与 Context Logger 的区别在于,一般只有插件和框架会通过它来记录日志。

Controller Logger 和 Service Logger

我们可以在 Controller 和 Service 实例上,通过 this.logger 获取它们。它们实质上就是一个 Context Logger,不过在打印日志时,还会额外地加上文件路径,以方便定位日志的打印位置。

订阅模型

订阅模型是一种比较常见的开发模式,例如消息中间件的消费者或调度任务。因此,我们提供了 Subscription 基类来规范化这个模式。

你可以通过以下方式来引用 Subscription 基类:

  1. const Subscription = require('egg').Subscription;
  2. class Schedule extends Subscription {
  3. // 需要实现此方法
  4. // subscribe 可以为 async function 或 generator function
  5. async subscribe() {}
  6. }

插件开发者可以根据自己的需求,基于它定制订阅规范,例如定时任务就是使用这种规范实现的。

相关链接: