Built-in Basic Objects of the Framework

12003

In this chapter, we will get a preliminary understanding of some basic objects built into the framework. These objects include four objects (Application, Context, Request, Response) inherited from [Koa], as well as other objects extended by the framework (Controller, Service, Helper, Config, Logger). We will frequently encounter them in the subsequent documentation.

Application

The Application is the global application object. In an application, each process will instantiate only one Application instance. It inherits from Koa.Application, on which we can mount some global methods and objects. We can easily extend the Application object in plugins or applications.

Events

During the operation of the framework, some events will be triggered on the Application instance, which application developers or plugin developers can listen to for certain operations. As application developers, we generally listen in the custom startup script:

  • server: This event will be triggered only once in a worker process. After the HTTP service has completed startup, it will expose the HTTP server to developers through this event.
  • error: Whenever an exception is captured at runtime, the error event will be triggered, exposing the error object and associated context (if any), allowing developers to perform custom logging, reporting, etc.
  • request and response: When the application receives a request and responds to a request, the request and response events will be triggered respectively, exposing the context of the current request, allowing developers to listen to these two events for logging.
// app.js

module.exports = (app) => {
  app.once('server', (server) => {
    // websocket related operations
  });
  app.on('error', (err, ctx) => {
    // report error
  });
  app.on('request', (ctx) => {
    // log received request
  });
  app.on('response', (ctx) => {
    // ctx.starttime is set by the framework
    const used = Date.now() - ctx.starttime;
    // log total request time
  });
};

Access Methods

The Application object can be accessed in almost any scenario when writing applications. Here are a few common access methods:

Almost all files loaded by the framework's Loader (Controller, Service, Schedule, etc.) can obtain the Application instance by exporting a function that will be called by Loader, passing app as a parameter:

  • Custom Startup Script

    // app.js
    module.exports = (app) => {
      app.cache = new Cache();
    };
  • Controller File

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

Similar to Koa, the Application object can be accessed via ctx.app on the Context object. For example, in a Controller file:

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

In instances that inherit from the Controller and Service base classes, the Application object can be accessed via this.app.

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

Context

The Context is a request-level object that inherits from [Koa.Context]. Each time a user request is received, the framework will instantiate a Context object. This object encapsulates the information of the user request and provides many convenient methods to obtain request parameters or set response information. The framework will mount all [Service] instances onto the Context instance. Some plugins will also mount other methods and objects onto it (e.g., [egg-sequelize] will mount all models onto the Context).

Access Methods

The most common way to access the Context instance is within [Middleware], [Controller], and [Service]. We have already seen the corresponding access method in the Controller example. The method of obtaining the Context in Service is the same as in Controller, while the method of obtaining the Context instance in Middleware is consistent with the usage in [Koa] framework middleware.

The framework's [Middleware] supports both Koa v1 and Koa v2 middleware syntax. Depending on the syntax used, the way to obtain the Context instance varies slightly:

// Koa v1
function* middleware(next) {
  // this is the instance of Context
  console.log(this.query);
  yield next;
}

// Koa v2
async function middleware(ctx, next) {
  // ctx is the instance of Context
  console.log(ctx.query);
}

In addition to obtaining the Context instance when handling requests, there are also scenarios outside of user requests where we need to access objects on the Context instance, such as service/model. In this case, we can create an anonymous Context instance using the Application.createAnonymousContext() method:

// app.js
module.exports = (app) => {
  app.beforeStart(async () => {
    const ctx = app.createAnonymousContext();
    // Preload before application starts
    await ctx.service.posts.load();
  });
};

In scheduled tasks, each task receives a Context instance as a parameter, allowing us to conveniently execute some scheduled business logic:

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

Request & Response

Request is a request-level object that inherits from [Koa.Request]. It encapsulates the native HTTP Request object of Node.js and provides a series of helper methods to obtain commonly used HTTP request parameters.

Response is a request-level object that inherits from [Koa.Response]. It encapsulates the native HTTP Response object of Node.js and provides a series of helper methods to set HTTP responses.

Access Methods

You can access the current request's Request (ctx.request) and Response (ctx.response) instances on the Context instance.

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    // Get the `id` parameter from the request
    const id = ctx.request.query.id;
    // Set the response body
    ctx.response.body = app.cache.get(id);
  }
}
  • [Koa] will proxy some methods and properties from Request and Response on the Context, see [Koa.Context].
  • As in the example above, ctx.request.query.id and ctx.query.id are equivalent, and ctx.response.body = and ctx.body = are also equivalent.
  • It is important to note that to obtain the POST body, you should use ctx.request.body, not ctx.body.

Controller

The framework provides a Controller base class and recommends that all Controllers inherit from this base class. This Controller base class has the following properties:

  • ctx - The current request's Context instance.
  • app - The application's Application instance.
  • config - The application's configuration.
  • service - All services of the application.
  • logger - The logger object encapsulated for the current Controller.

In the Controller file, you can reference the Controller base class in two ways:

// app/controller/user.js

// Get from egg (recommended)
const Controller = require('egg').Controller;
class UserController extends Controller {
  // implementation
}
module.exports = UserController;

// Get from app instance
module.exports = (app) => {
  return class UserController extends app.Controller {
    // implementation
  };
};

Service

The framework provides a Service base class and recommends that all Services inherit from this base class.

The properties of the Service base class are consistent with those of the Controller base class, and the access method is similar:

// app/service/user.js

// Get from egg (recommended)
const Service = require('egg').Service;
class UserService extends Service {
  // implement
}
module.exports = UserService;

// Get from app instance
module.exports = (app) => {
  return class UserService extends app.Service {
    // implement
  };
};

Helper

Helper is used to provide some utility functions. Its purpose is to allow us to extract some common actions into an independent function in helper.js. This way, we can write complex logic using JavaScript, avoiding logic being scattered in various places, and making it easier to write test cases.

Helper itself is a class with the same properties as the Controller base class, and it will also be instantiated on each request. Therefore, all functions on Helper can also access the context information related to the current request.

Access Methods

You can access the current request's Helper (ctx.helper) instance on the Context instance.

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.query.id;
    const user = app.cache.get(id);
    ctx.body = ctx.helper.formatUser(user);
  }
}

In addition, the Helper instance can also be accessed in templates. For example, you can use the shtml method provided by the security plugin in templates.

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

Custom Helper Methods

In application development, we may often need to customize some Helper methods. For example, in the above example, formatUser can be customized through framework extension.

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

Config

We recommend that application development follow the principle of separating configuration and code, placing some business configurations that need hard coding into configuration files. At the same time, configuration files support different configurations for various runtime environments, making them very convenient to use. All framework, plugin, and application-level configurations can be accessed through the Config object. For details on framework configuration, you can read the Config Configuration section.

Access Methods

We can access the config object from the Application instance via app.config, and we can also access the config object via this.config on the instances of Controller, Service, and Helper.

Logger

The framework has a powerful built-in logging feature, which makes it very convenient to print various levels of logs to the corresponding log files. Each logger object provides methods for four levels:

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

The framework provides multiple Logger objects, and below we briefly introduce the access methods and usage scenarios for each Logger object.

App Logger

We can access it via app.logger. If we want to do some application-level logging, such as recording some data information during the startup phase or logging some business information unrelated to requests, we can accomplish this through the App Logger.

App CoreLogger

We can access it via app.coreLogger. Generally, when developing applications, we should not print logs through CoreLogger. However, the framework and plugins need to use it to print application-level logs, which helps to clearly distinguish between application and framework logs. Logs printed through CoreLogger will be placed in a different file than those from Logger.

Context Logger

We can access it from the Context instance via ctx.logger. From the access method, we can see that the Context Logger is certainly related to requests. The logs it prints will be prefixed with some information related to the current request (e.g., [$userId/$ip/$traceId/${cost}ms $method $url]). With this information, we can quickly locate requests from the logs and link all logs within a single request.

Context CoreLogger

We can access it via ctx.coreLogger. The difference from Context Logger is that generally only plugins and the framework will use it to log.

Controller Logger and Service Logger

We can access them via this.logger on Controller and Service instances. They are essentially a Context Logger, but when printing logs, they will additionally include the file path to facilitate locating the log printing position.

Subscription Model

The subscription model is a relatively common development pattern, such as consumers of message middleware or scheduled tasks. Therefore, we provide a Subscription base class to standardize this pattern.

You can reference the Subscription base class as follows:

const Subscription = require('egg').Subscription;

class Schedule extends Subscription {
  // This method needs to be implemented
  // subscribe can be an async function or a generator function
  async subscribe() {}
}

Plugin developers can customize subscription specifications based on their needs, such as scheduled tasks implemented using this specification.

Related Links: