series

  • [Koa source code] Koa
  • [Koa source code] Koa-router
  • Koa – BodyParser
  • Koa cookie
  • [Koa source code learning] KOa-session

preface

Koa is a Web framework based on Nodejs, it does not have any built-in middleware, but provides a set of elegant methods to handle the request execution flow, and through the use of async, await syntax, it is easy to achieve asynchronous middleware processing.

Each time it receives a request from a client, Koa creates a context object, proxies the native request and response, and then executes a series of middleware functions to process the request accordingly. So let’s take a look at how Koa is implemented internally.

Initialization phase

During the initialization phase, we can divide it into the following three steps:

  1. Initialization: InitializationKoaThe instance
  2. Use: Adds middleware to the instance
  3. Listen: creates a server and enables listening

Initialize the

Let’s start with the Koa constructor, which looks like this:

/* koa/lib/application.js */
const Emitter = require('events');
const context = require('./context');
const request = require('./request');
const response = require('./response');

// Emitter implements the event interface
module.exports = class Application extends Emitter {
  constructor(options) {
    // ...
    // It is used to store the middleware list
    this.middleware = [];
    // The context object of the Koa implementation that encapsulates the request and response
    this.context = Object.create(context);
    // Encapsulate the native request object
    this.request = Object.create(request);
    // Encapsulate the native response object
    this.response = Object.create(response);
    // ...}}Copy the code

As you can see, when creating a Koa instance, you add the Middleware, Context, Request, and Response instance properties to the current instance.

use

After the Koa instance is created, you can use the use method to add middleware to your application with code like this:

/* koa/lib/application.js */
module.exports = class Application extends Emitter {
  use(fn) {
    if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
    // ...
    // Add middleware to the middleware
    this.middleware.push(fn);
    return this; }}Copy the code

As you can see, the use method takes a function and adds it to middleware.

listen

After adding the middleware, you can call the Listen method to start the server as follows:

/* koa/lib/application.js */
module.exports = class Application extends Emitter { listen(... args) {// Use the native HTTP module to create a server and enable listening
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}
Copy the code

As you can see, listen uses the native HTTP module, first creating a server using the http.createServer method, then immediately calling listen to enable listening. The key here is to generate a callback function that matches the Request event by calling the callback method, as shown below:

/* koa/lib/application.js */
module.exports = class Application extends Emitter {
  callback() {
    // Process middleware
    const fn = compose(this.middleware);

    // Add application-level error handling
    if (!this.listenerCount('error')) this.on('error'.this.onerror);

    // Request handlers
    const handleRequest = (req, res) = > {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    returnhandleRequest; }}Copy the code

As you can see, the callback method first uses the compose method to build middleware into an executor, and then defines and returns the handleRequest method, which is the callback function added to the request event. When the server is turned on, This callback function is executed each time a request is received from the client. Let’s take a look at how Compose builds the actuator.

compose

The compose method is provided via the KOA-compose module with the following code:

/* koa-compose/index.js */
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
  for (const fn of middleware) {
    if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}// Middleware executor, executed when processing requests
  return function (context, next) {
    // last called middleware #
    let index = - 1
    // Start with the first middleware and return a Promise
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      // Execute the next middleware executor, which is used in koa-router
      if (i === middleware.length) fn = next
      if(! fn)return Promise.resolve()
      try {
        // Construct the next argument to execute the middleware
        // Use promise.resolve to ensure asynchronous execution of the middleware
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

List of middleware in nature is a set of functions, and our purpose, is to provide an implementation model, at every time of receipt of a request, begin from the first middleware execution, before each middleware, will first construct a next method, temporary current middleware is executing, it is used to transfer control to the next middleware, as a result, All middleware will execute according to the stack model in a first-in, last-out order. Meanwhile, to support asynchronous middleware, Koa uses promise.resolve to wrap the execution result of middleware, and calls await next() in upstream middleware must wait until downstream middleware completes execution. The code after Next is executed to implement a true asynchronous middleware system.

Runtime phase

As a result of the above analysis, we have started a server, tied to request events, and built middleware into an asynchronous executor that will enable all middleware previously registered to execute in the order they were added. Let’s take a look at what happens internally when Koa receives a request. The callback function for the request event looks like this:

/* koa/lib/application.js */
module.exports = class Application extends Emitter {
  callback() {
    // ...
    const handleRequest = (req, res) = > {
      // Create a context object corresponding to the current request
      const ctx = this.createContext(req, res);
      // Execute middleware
      return this.handleRequest(ctx, fn);
    };
    // ...}}Copy the code

As you can see, when a request is received, Koa uses the createContext method to create a context object corresponding to the request as follows:

/* koa/lib/application.js */
module.exports = class Application extends Emitter {
  createContext(req, res) {
    // Create instances of context, Request, and Response
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    // Establish relationships with native REq and RES
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    // Koa recommends a namespace for storing custom data
    context.state = {};
    returncontext; }}Copy the code

You create instances of context, request, and Response, and then associate them with the native REQ and RES. The advantage of this is that you can access all resources related to the request through the CTX object, and at the same time, Request and Response objects provide a series of common attributes and methods, masking the implementation details of the underlying HTTP module for easy use.

After CTX is created, the handleRequest method is called to execute the middleware with code like this:

/* koa/lib/application.js */
module.exports = class Application extends Emitter {
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    // Koa default status code
    res.statusCode = 404;
    // Handle an error related to this request
    const onerror = err= > ctx.onerror(err);
    // Process the final response
    const handleResponse = (a)= > respond(ctx);
    // Register error events on res and res.socket to ensure that the program will run properly if an error occurs
    onFinished(res, onerror);
    // The executor that executes middleware
    returnfnMiddleware(ctx).then(handleResponse).catch(onerror); }}Copy the code

As you can see, in the handleRequest method, you basically define onError and handleResponse, and then pass the context object to the executor to start executing the added middleware. Because the return result of the actuator is also a Promise, the handleResponse method will be called during the depressing, which processes the response logic, and the OnError method will be called during the Rejected, which performs error processing.

ctx.body

In the middleware, we can assign ctx.body and Koa will automatically process it correctly based on the data type. Let’s take a look at how this process is implemented inside Koa, as shown below:

/* koa/lib/response.js */
module.exports = {
  set body(val) {
    const original = this._body;
    // Save the data in _body
    this._body = val;

    // Does not carry a response body
    if (null == val) {
      if(! statuses.empty[this.status]) this.status = 204;
      if (val === null) this._explicitNullBody = true;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // Override the default 404
    if (!this._explicitStatus) this.status = 200;

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type');

    // string
    if ('string'= = =typeof val) {
      if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
      this.length = Buffer.byteLength(val);
      return;
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      if (setType) this.type = 'bin';
      this.length = val.length;
      return;
    }

    // stream
    if (val instanceof Stream) {
      onFinish(this.res, destroy.bind(null, val));
      if(original ! = val) { val.once('error', err => this.ctx.onerror(err));
        // overwriting
        if (null! = original)this.remove('Content-Length');
      }

      if (setType) this.type = 'bin';
      return;
    }

    // json
    this.remove('Content-Length');
    this.type = 'json'; }}Copy the code

As you can see, when assigning the body value, first assign the data to _body and then do the following:

  1. If null: If the response body is empty, run statuss. empty to check whether the current status code is 204,205,304. If it is not, set it to 204 by default. Then remove the content-Type, Content-Length, and Transfer-Encoding headers.

  2. If the value type is String: Set content-Type to text/ HTML if the Content’s first non-whitespace character is <, or text/plain if the Content’s first non-whitespace character is <. Then set content-Length to buffer.bytelength.

  3. If the value Type is Buffer: If content-type is not set, set it to application/octet-stream, and set content-length to buffer.length.

  4. If the value is of Type Stream: use the onFinish method to ensure that the resource can be cleaned up if the response Stream is closed or an error occurs. Finally, set it to application/ OCtet-stream, again if content-Type is not set.

  5. If none of the above conditions are met, Koa treats the Content as JSON and sets the content-type to Application/JSON.

The above logic is mainly used to set the content-type and content-Length, but it does not return the specific response Content at this time. It just temporarily stores the data in res._body. When all the middleware is finished executing, or the next middleware is not called to execute the next middleware, This time the executor completes and the previous handleResponse method is called, where Koa continues to call the respond method, which returns the response as follows:

/* koa/lib/application.js */
function respond(ctx) {
  // Bypass Koa's built-in response processing and need to handle the res yourself
  if (false === ctx.respond) return;

  if(! ctx.writable)return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

  // the status code is 204,205,304, and the response body is not required
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  // The HEAD request does not require a response body and ends the response directly
  if ('HEAD' === ctx.method) {
    if(! res.headersSent && ! ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // the status code is not 204,205,304, and the response content is null, return the cause phrase as the response body
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if(! res.headersSent) { ctx.type ='text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // Return the corresponding response content according to the data type
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string'= = =typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if(! res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }Copy the code

Koa calls the native End or PIPE methods, depending on the type of data, to return the corresponding response. By default, Koa returns the data in JSON format using json.stringify.

conclusion

Koa overall logic is very simple, it is just to provide a framework for use, first of all to Koa, middleware is added to the instance and then upon receipt of the request, Koa will be in accordance with the order, to perform, they, in turn, in addition, Koa internal offers a wide range of properties, methods, used to encapsulate the underlying HTTP implementation details, in the middleware, You can respond to a request by assigning a value directly to the body.