Koa framework introduction

Koa was built by the original Express team to be a smaller, more expressive, and more robust Web framework. Composing web applications using KOA eliminates repetitive and tedious nesting of callback functions and greatly improves error handling efficiency by combining different generators. Koa doesn’t bundle any middleware into kernel methods, it just provides a lightweight and elegant library of functions that makes writing Web applications a breeze.

The characteristics of

  • Light weight, no binding
  • Middleware architecture
  • Callbacks are replaced with different generator and await/async
  • Enhanced error handling
  • Easy to use API

Simple to use

Koa encapsulates node services and provides an easy-to-use API. Suppose we want to return Hello, node! When requesting port 3000. Data, using the native Node implementation code as follows:

const http = require('http');

const server = http.createServer((req, res) = > {
  res.end('hello, node! ');
});

server.listen(3000.() = > {
  console.log('server is running on 3000... ');
});
Copy the code

Using Koa, the implementation is as follows:

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

app.use((ctx, next) = > {
  ctx.body = 'hello, node! ';
});

app.listen(3000.() = > {
  console.log('server is running on 3000... ');
});
Copy the code

By comparison, the koA implementation uses new koa () to create an instance of koA with the use method, whose callback takes CTX and next. These simple points are basically what koA is all about.

Middleware and onion ring models

Middleware is at the heart of Koa, which uses use() to invoke a set of middleware and next() to pass the context to the next middleware for processing. When there is no next next() to execute, the logic after next in each use() callback is executed in reverse order.

Here’s koA’s onion ring model:

The node console prints the localhost:3000 port in 1, 3, 5, 6, 4, 2 order:

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

app.use((ctx, next) = > {
  console.log(1);
  next();
  console.log(2);
});

app.use((ctx, next) = > {
  console.log(3);
  next();
  console.log(4);
});

app.use((ctx, next) = > {
  console.log(5);
  ctx.body = 'hello, node! ';
  console.log(6);
});

app.listen(3000.() = > {
  console.log('server is running on 3000... ');
});

Copy the code

Koa source code structure

Koa source

There are four core documents of Koa:application.js,context.js,request.js,response.js. All of the code adds up to less than 2000 lines, which is very lightweight, and a lot of code is concentrated inrequest.jsresponse.jsThere are only a few hundred lines of core code for processing request and response headers.

application

Application.js is a koA entry file that exports koA’s constructors, which contain koA’s main functionality implementations.

listen

The Application constructor first implements Listen via the HTTP module in Node:

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

use

The use method adds all the middleware functions it receives to this.middleware so that the middleware can be called sequentially later. In order to be compatible with use in KOA1, middleware functions of generator type will be converted through the KOA-convert library to be compatible with recursive calls of KOA in KOA2.

use(fn) {
    if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
    if (isGeneratorFunction(fn)) { // Compatible with koA1 use
      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 '-');
    this.middleware.push(fn);
    return this;
}
Copy the code

callback

When the listen function is started, the createServer function returns the result of the callback function.

When the service is started, the callback function does middleware merging, listens for error requests from the framework layer, and so on.

It then returns the handleRequest method, which takes req and RES and creates a new KOA context CTX based on node HTTP native REQ and RES each time the server receives the request.

 callback() {
    const fn = compose(this.middleware); // Merge middleware

    if (!this.listenerCount('error')) this.on('error'.this.onerror); // Catch frame layer errors

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

    return handleRequest;
  }
Copy the code

createContext

Let’s look at the createContext function, which is a bunch of assignment operations.

  1. Create () creates new objects imported from context.js, request.js, response.js to prevent contamination of the original imported objects.

  2. Request = object.create (this.request) and context.response = object.create (this.response) Object is mounted to the context object. This corresponds to the delegate part of context.js (see the koA core library section below for an explanation of delegates), Enable CTX to access ctx.request. XXX and ctx.response. XXX directly through ctx.xxx

  3. Through a series of assignment operations, the res and REq of the original HTTP request, the Koa instance app, and so on are mounted to the context, Request, and Response objects respectively. Context. Js, request.js, and Response. js can be used to access the original request and corresponding parameters

createContext(req, res) {
    / / Object. The create () to create
    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;
    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 final diagram of this code after execution is as follows:

handleRequest

After createContext is executed in the callback, the created CTX and the sequentially executed function generated after merging the middleware are passed to handleRequest and executed.

HandleRequest listens for the RES through the onFinished method, and when the RES completes, closes, or fails, the onError callback is executed. After that, it returns the result of the middleware execution. When all the middleware execution is complete, it executes respond and returns the data.

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err= > ctx.onerror(err);
    const handleResponse = () = > respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code

context

cookies

Context. Js uses the get and set methods to set and read cookies.

delegate

Context.js has a number of delegate operations, which allow CTX to directly access properties and methods in response and request. You can obtain ctx.request. XXX or ctx.response. XXX through ctx. XXX.

Delegates are implemented through the delegates library, delegates delegate the properties and methods of the node below the object via proto.__defineGetter__ and proto.__defineSetter__. (Proto.__definegetter__ and proto.__definesetter__ are now deprecated by MDN and object.defineProperty () is used instead)

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');
  // ...

delegate(proto, 'request')
  .method('acceptsLanguages')
  .getter('ip');
  // ...
Copy the code

Context. Js exports a context object, which is mainly used to transmit information between middleware and other components. Meanwhile, two objects, Request and Response, are mounted on the context object.

In addition, it performs cookie processing and delegates the event and method on request and Response object using the delegates library to make it convenient for users to use.

request

Request.js exports the request object, and uses the get() and set() methods to query the parameters of the request header, such as header, URL, href, method, path, query… Do the processing, mounted to the request object, convenient user access and Settings.

response

As with request.js, response parameters are handled by get() and set().

koa-compose

In application.js, middleware is incorporated through Compose, which is also a core implementation of KOA.

The source code for Koa-compose is very simple, with only a few dozen lines:

function compose (middleware) { // An array of middleware functions
  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) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if(! fn)return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

Compose receives an array of middleware functions and returns a closure that maintains an index of the currently invoked middleware.

Dispatch (I) returns the result of the i-th function in middleware via promise.resolve (), the i-th + 1 function passed in app.use(). The second argument to the app.use() callback is next, so when the code in app.use() is executed to next(), dispatch.bind(null, I + 1)) is executed, the next app.use() callback.

By analogy, the app.use() callbacks are strung together until there is no next, and the edge returns the logic after next() that executes each app.use() in sequence. The result of the first app.use() is finally returned via promise.resolve ().

Implement a simple Koa

Let’s try to implement a simplified version of KOA:

Encapsulates the HTTP module of node

To implement the listen function, create an internal MyKoa class and a Node-based HTTP module as shown in the simplest example at the beginning of this article:

// application.js
const http = require('http');

class MyKoa {
  listen(. args) {
    const server = http.createServer((req, res) = > {
      res.end('mykoa')}); server.listen(... args); }}module.exports = MyKoa;
Copy the code

Implement the use method and simple createContext

Then to implement the app.use() method, we see that app.use() has ctx.body inside, so we also need to implement a simple CTX object.

  1. To create acontext.js, internal exportctxObject, which can be obtained and set via get and set, respectivelyctx.bodyValue:
// context.js
module.exports = {
  get body() {
    return this._body;
  },

  set body(value) {
    this._body = value; }};Copy the code
  1. inapplication.jsAdd the use and createContext methods to the MyKoa classres.endreturnctx.body:
const http = require('http');
const _context = require('./context');

class MyKoa {

  listen(. args) {
    const server = http.createServer((req, res) = > {
      const ctx = this.createContext(req, res);
      this.callback(); res.end(ctx.body); }); server.listen(... args); }use(callback) {
    this.callback = callback;
  }

  createContext(req, res) {
    const ctx = Object.assign(_context);
    returnctx; }}module.exports = MyKoa;
Copy the code

Perfect createContext

To access the request header through CTX and set the relevant information such as ctx.query, ctx.message and so on, we need to create response.js and request.js to handle the request header and response header. Mount the Request and Response objects to the CTX object, and implement a delegate function that gives CTX access to properties and methods on the Request and Response objects.

  1. To achieve a simple request and response, request through get method, can be resolvedreq.urlTo convert it to an object. In response, get and setmessage, can get and setres.statusMessageValue:
// request.js
module.exports = {
  get query() {
    const arr = this.req.url.split('? ');
    if (arr[1]) {
      const obj = {};
      arr[1].split('&').forEach((str) = > {
        const param = str.split('=');
        obj[param[0]] = param[1];
      });
      return obj;
    }
    return{}; }};Copy the code
// response.js
module.exports = {
  get message() {
    return this.res.statusMessage || ' ';
  },

  set message(msg) {
    this.res.statusMessage = msg; }};Copy the code
  1. Create a new utils.js, export the delegate method, and internally pass object.defineProperty to the passed Object obj to listen for property changes in real time. For example, delegate(CTX, ‘request’) When the value of the request object changes, the CTX on the Request proxy also gets the latest value.

    We then implement simple getters and setters with a listen function that adds the corresponding key to setters and getters when we use getters or setters. Allow obj to access the corresponding key by proxy to the corresponding key of Proterty:

// utils.js
module.exports.delegate = function Delegate(obj, property) {
  let setters = [];
  let getters = [];
  let listens = [];

  function listen(key) {
    Object.defineProperty(obj, key, {
      get() {
        return getters.includes(key) ? obj[property][key] : obj[key]; // Returns the corresponding value of obj[property][key] if the getter is passed, otherwise returns the value of obj[key]
      },
      set(val) {
        if(setters.includes(key)) { obj[property][key] = val; Obj [key] = obj[key] = obj[key] = obj[key]else{ obj[key] = val; }}}); }this.getter = function (key) {
    getters.push(key);
    if(! listens.includes(key)) {// prevent repeated calls to listen
      listen(key);
      listens.push(key);
    }
    return this;
  };

  this.setter = function (key) {
    setters.push(key);
    if(! listens.includes(key)) {// Prevent listenf from being called repeatedly
      listen(key);
      listens.push(key);
    }
    return this;
  };
  return this;
};
Copy the code
  1. Use a delegate to delegate request and response in context:
// context.js
const { delegate } = require('./utils');
const context = (module.exports = {
  get body() {
    return this._body;
  },

  set body(value) {
    this._body = value; }}); delegate(context,'request').getter('query');
delegate(context, 'response').getter('message').setter('message');
Copy the code
  1. Improve the createContext function:
// application.js
const http = require('http');
const _context = require('./context');
const _request = require('./request');
const _response = require('./response');

class MyKoa {
  // ...
  createContext(req, res) {
    const ctx = Object.assign(_context);
    const request = Object.assign(_request);
    const response = Object.assign(_response);
    ctx.request = request;
    ctx.response = response;
    ctx.req = request.req = req;
    ctx.res = response.res = res;
    returnctx; }}module.exports = MyKoa;
Copy the code

Implement middleware and onion models

So far, only the functionality to implement the app.use() middleware remains.

  1. Following the previous koa-compose analysis,In utils. Js, implement compose:
// utils.js
module.exports.compose = (middleware) = > {
  return (ctx, next) = > {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('error'));
      index = i;
      const cb = middleware[i] || next;
      if(! cb)return Promise.resolve();
      try {
        return Promise.resolve(
          cb(ctx, function next() {
            return dispatch(i + 1); })); }catch (error) {
        return Promise.reject(error); }}}; };Copy the code
  1. In app.js, initializethis.middlewareThe array,use()The callback function adds the callback to the array:
// ...
class MyKoa {
  constructor() {
    this.middleware = [];
  }
  // ...

  use(callback) {
    this.middleware.push(callback);
  }
  // ...
}

module.exports = MyKoa;
Copy the code
  1. In the Listen method createServer, middleware is merged when the request is encountered, and the middleware returns the RES result after execution:
// ...
const { compose } = require('./utils');

class MyKoa {
  // ...
  listen(. args) {
    const server = http.createServer((req, res) = > {
      const ctx = this.createContext(req, res);
      //
      const fn = compose(this.middleware);
      fn(ctx)
        .then(() = > { // After all middleware execution is complete, corresponding information is returned
          res.end(ctx.body);
        })
        .catch((err) = > {
          throwerr; }); }); server.listen(... args); }// ...
}
module.exports = MyKoa;
Copy the code

test

Now that we’re done, let’s introduce Mykoa and test it with the following service:

const Koa = require('.. /my-koa/application');
const app = new Koa();

app.use((ctx, next) = > {
  ctx.message = 'ok';
  console.log(1);
  next();
  console.log(2);
});

app.use((ctx, next) = > {
  console.log(3);
  next();
  console.log(4);
});

app.use((ctx, next) = > {
  console.log(5);
  next();
  console.log(6);
});

app.use((ctx, next) = > {
  console.log(ctx.message);
  console.log(ctx.query);
  ctx.body = 'hello, myKoa';
});

app.listen(3000.() = > {
  console.log('server is running on 3000... ');
});
Copy the code

Go to http://localhost:3000/api? Name = ZLX interface, return data hello, myKoa.

The node server console displays the following information:

1
3
5
ok
{ name: 'zlx' }
6
4
2
Copy the code

There is no problem with our implementation!

The source code

Finally attached source code implementation address: github.com/zh-lx/study…