Koa is introduced

On Koa’s official website, the slogan is: The next Generation Web development framework based on the Node.js platform. As we all know, Koa and Express both came from the same team. How do I parse this slogan?

All of the words in this slogan that contain the meaning “compare” can be understood as relative to Express. The so-called “next generation”, “smaller”, “faster”, all have their own meanings. Let’s take a look at Express vs. Koa downloads at this point:

As you can see, Express’s market share and downloads (and growth) are much higher than Koa’s. So it’s safe to call yourself “the next generation” (though it’s debatable whether this statistical comparison is fair).

Also, unlike Express, which includes a basic business processing framework internally, Koa contains only the middleware core processing logic known as the onion Ring model. Koa expects more business modules to be refined by the community. In this way, it is more appropriate to compare Koa to the Connect that was built in before Express (Connect has been removed from Express 4 internally). However, the “smaller” Koa is more clearly defined and has some outstanding features that make developers happy.

Next, let’s take a closer look at how Koa differs from Express. Only by understanding the differences can we better understand the characteristics of Koa.

self-image

On Koa’s official website, there is a description of Koa’s position:

Philosophically, Koa aims to “fix and replace node”, whereas Express “augments node”. Koa uses promises and async functions to rid apps of callback hell and simplify error handling. It exposes its own ctx.request and ctx.response objects instead of node’s req and res objects.

Express, on the other hand, augments node’s req and res objects with additional properties and methods and includes many other “framework” features, such as routing and templating, which Koa does not.

An excerpt from an online translation reads as follows:

Conceptually, Koa is about “fixing and replacing Node,” while Express is about “enhancing Node.” Koa uses promise and async functions to get rid of callback hell and simplify exception-handling logic. It exposes its ctx.request and ctx. Response objects instead of Node’s REq and RES objects.

Express, on the other hand, enhances Node’s REq and RES objects by adding additional properties and methods, and introduces many framework features, such as routing and templates, that Koa does not.

Whether its implementation conforms to the positioning, you can see by yourself.

Middleware processing logic

In essence, Koa differs from Express 4 in the following two points:

  • Koa is to usePromise + async/awaitTo handle middleware delivery logic; Express 4 uses callback functions to handle middleware delivery logic.
  • Koa goes through all the middleware processing logic before finally returning the request; Express 4 is encounteredres.send()Return the request.

With Promise support for async/await writing in the new ES specification, Koa is perfectly capable of supporting asynchronous processing and getting rid of callback hell. In the following simulation implementation, you can see the difference between the two by looking at the code.

// Express and Koa deal with the underlying simulation of middleware logic

// express: callback function
app.use(function middleware1(req, res, next) {
  console.log('middleware1 start')
      // next()
      (function (req, res, next) {
          console.log('middleware2 start')
              // next()
              (function (req, res, next) {
                  console.log('middleware3 start')
                      // next()
                      (function handler(req, res, next) {
                          res.send("end")
                          console.log('123456')
                      })()
                  console.log('middleware3 end')
              })()
          console.log('middleware2 end')
      })()
  console.log('middleware1 end')})// Koa: Based on Promise + async/await
function compose() {
  return function () {
    const ctx = {}
    // Each call to next() wraps a promise.resolve layer
    Promise.resolve(function fn1(context){
      console.log("Middleware1 start");
      yield Promise.resolve(function fn2(context){
        console.log("Middleware2 start");
        yield Promise.resolve(function fn3(context){
          console.log("Middleware3 start");
          yield Promise.resolve(function fn4(context){
            / /... more
          });
          console.log("Middleware3 end");
        });
        console.log("Middleware2 end");
      })
      console.log("Middleware1 end"); }); }}Copy the code

Error handling logic

Within Koa, due to the Onion circle model, normal middleware processing can not handle error logic, just set the first middleware in the middleware array as error function middleware. In Express 4, error handling logic needs to be included in every normal piece of middleware, and it needs to be thrown out via the next() function, otherwise the error will disappear.

Smaller core code

Unlike Express 4, which includes neat Web tools (routing, templates, and so on), Koa focuses only on core code. So it has a smaller volume.

Koa parsing

Simple descriptions do not give us a deeper understanding of how Koa works. Let’s try to look at the source code. The simplest demo code:

const Koa = require('koa');
const app = new Koa();

// response
app.use(ctx= > {
  ctx.body = 'Hello Koa';
});
console.log("start the test1 server !");
app.listen(3000);
Copy the code

Here, we want to understand a few things about the source code:

  1. When you initialize, what do you do?
  2. app.use()What did you do?
  3. app.listen()What did you do?

Koa Initializes the application instance

In the Koa application, we build an instance application with const app = new Koa(). Core code (deleted) :

  constructor(options) {
    super(a);this.middleware = [];
    // Every app instance has instances of the following three objects
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect; }}Copy the code

To initialize an app instance, add context, Request, Response, middleware, and other properties to the app instance. During this initialization, Koa will mount any methods that Koa officially provides into their respective locations for easy invocation in code.

App.use () adds middleware

  use(fn) {
    if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || The '-');
    // Store it directly into the Middleware array for further processing
    this.middleware.push(fn);
    return this;
  }
Copy the code

This checks the type of the incoming function, converts it if it’s an old Generator function type, and puts it directly into the Middleware array. The middleware in the array executes each request one by one.

App.listen () listening — core logic

The server is created when the listen function executes:

  listen(. args) {
    debug('listen');
    const server = http.createServer(this.callback());
    returnserver.listen(... args); }Copy the code

In Node’s HTTP module, for each request, it goes to the callback function. So this callback is used to handle the actual request. Let’s see what callback does:

  callback() {
    // Wrap all middleware, return an executable function. Koa-compose implements the onion ring model
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error'.this.onerror);
    const handleRequest = (req, res) = > {
      // req res is a node native request parameter
      const ctx = this.createContext(req, res);
      // Return the created CTX to all middleware as the context for the entire request
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
Copy the code

It’s not simple here, it involves a few points:

  1. createContextWhat did you do
  2. composeHow is the Onion model implemented
  3. this.handleRequest(ctx, fn)What did

Parsing createContext

  createContext(req, res) {
    // Each request corresponds to a CTX, request, response, req, or RES
    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;
    // Mount the node native request parameter req res to context, request, and Response
    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;
    context.state = {};
    return context;
  }
Copy the code

The main thing here is to specify what is mounted by the context, which is generated on every request. From the code:

  1. Per application instanceappThere will be corresponding onescontext,request,responseInstance (i.e.this.xxx). Each request creates its own instance based on these instances. The goal is not to contaminate global variables.
  2. willnodeThe nativereq,resAs well asthisMount to thecontext,request,responseOn.
  3. Will create thecontextReturns, passing in the first argument of any middleware as the context for this request.

Focus on point 2, why these properties are mounted. In addition to the convenience, we can also know by looking at the source resquest.js response.js file: all these accesses are proxies, and ultimately access node’s native REq and RES.

Koa-compose implements the onion ring model

Next, let’s look at how the koa-compose plug-in implements the onion ring model in the source code:

function compose (middleware) {
  // Type check
  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! ')}return function (context, next) {
    // The middleware that was last executed
    let index = -1
    return dispatch(0)
    function dispatch (i /* indicates which middleware */ is expected to be executed) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      // Without fn, return a reolved Promise object
      if(! fn)return Promise.resolve()
      try {
        /* Const next = dispatch. Bind (null, I + 1); /* Const next = dispatch. Bind (null, I + 1); const fnResult = fn(context, next); return Promise.resolve(fnResult); * /
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

In a nutshell, incomposeAccording to the variableiExecute the middleware one by one, in a recursive fashion. The process is not difficult to understand and the implementation is clever.It’s worth noting that each dispatch() returns a Promise. See the GIF below to see how the above code is implemented.

Parsing handleRequest

Now that we have the context information for each request and the middleware logic that flows through compose, let’s see how the request is ultimately handled:

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err= > ctx.onerror(err);
    const handleResponse = () = > respond(ctx);
    onFinished(res, onerror);
    // Execute the middleware function after compose and respond
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
Copy the code

Implement the middleware function behind compose and finally implement the respond method. As you can see, the response is wrapped and assigned to context.res.end(body).

conclusion

To sum up, we briefly introduced Koa; At the same time, through the analysis of the Demo file execution process, we see some of the Koa related principles from the source level. So far, we have covered the introduction and analysis of Koa. I write this article in the process, will be more understanding of the whole process, also hope to help you!