Koa2 introduction

The reason for this article is the elegant use and encapsulation of the KOA2 +Ts project that we’ve tried before

The koA2 source file is structured as follows:

  • application.jsThe Koa2 entry file, which encapsulates the context, request, response, and middleware processes, exports the real columns of the class, and inherits events, so the framework supports Event listening and firing capabilities, such as:
module.exports = class Application extends Emitter {}
Copy the code
  • context.jsIs the context CTX for processing the application. It encapsulates the methods of Request.js and Response.js.
  • request.jsIt encapsulates the request to process HTTP.
  • response.jsIt encapsulates the processing of HTTP responses.

Therefore, implementing the KOA2 framework requires the following four modules to be encapsulated and implemented:

  1. encapsulationnode http serveR. createKoa class constructor;
  2. structureThe request and response, andcontextObject;
  3. Compose Middleware mechanismThe implementation of;
  4. Error capture and error handling;

Create a koA class constructor

Implement a simple server using node’s native module and print Hello KOa, code:


const http = require('http');

const server = http.createServer((req, res) = > {
  res.writeHead(200);
  res.end('hello koa.... ');
});

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

So the first step to implementing KOA is to encapsulate the native module. We first create application.js to implement an Application object.

The basic code is encapsulated as follows (if we put the code inside application.js) :


const Emitter = require('events');
const http = require('http');

class Application extends Emitter { /* Constructor */ constructor() {
    super(a);this.callbackFunc = null;
  } // Start HTTP server and pass in the argument callback
 listen(. args) {
    const server = http.createServer(this.callback()); 
    returnserver.listen(... args); }use(fn) { 
    this.callbackFunc = fn;
  }
  callback() { 
    return (req, res) = > { 
      this.callbackFunc(req, res); }}}module.exports = Application;
Copy the code

We then create a new test.js file in that directory and initialize it with the following code:

const testKoa = require('./application');
const app = new testKoa();

app.use((req, res) = > {
  res.writeHead(200);
  res.end('hello koa.... ');
});

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

The above code has a disadvantage. The callback function parameters passed in by app.use are still req and RES, which are node’s native Request and response objects. It is not convenient to use this object, and it does not meet the usability of the framework design.

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

app.use(async (ctx, next) => {
  console.log(11111);
  await next();
  console.log(22222);
});
app.listen(3000.() = > {
  console.log('listening on 3000');
});
Copy the code

For that reason, we need to construct the Request, Response, and context objects.

Construct request, Response, and context objects.

1. request.js

The function of this module is to encapsulate the request object of the native HTTP module, and proxy some properties or methods of the Request object by overwriting getter/setter functions.

Therefore, we need to create a request.js file in the root directory of our project, which only has methods to get and set urls, and finally export the file, the code is as follows:

const request = {
  get url() { return this.req.url;
  },
  set url(val) { this.req.url = val; }};module.exports = request;
Copy the code

In short, you need to convert HTTP RES and REQ to KOA RES and REQ for easy use

2. response.js

Response.js also encapsulates the response object of the HTTP module, and projects some properties or methods of the response object through getter/setter functions.

Similarly, we need to create a response.js in the root directory of our project. The basic code looks like this:

const response = {
  get body() { 
    return this._body;
  },
  set body(data) { 
    this._body = data;
  },
  get status() { 
    return this.res.statusCode;
  },
  set status(statusCode) { 
    if (typeofstatusCode ! = ='number') { 
      throw new Error('statusCode must be a number ');
    } 
    this.res.statusCode = statusCode; }};module.exports = response;
Copy the code

The code is the same as the simple code above, and there are four methods in the file, the body read and the set method. Read a property named this._body. The status methods set or read this.res.statuscode, respectively. Similarly: this.res is the response object in Node native.

3. context.js

With simple Request.js and Response.js above, the core of the context is to delegate property methods on request and Response objects to the context object. That is, it will change this.res.statusCode to this.ctx.statuscode something like this. All methods and properties in request.js and Response. js can be found on the CTX object.

So we need to create context.js in the root directory of the project. The basic code is as follows:

const context = {
  get url() { return this.request.url;
  },
  set url(val) { this.request.url = val;
  },
  get body() { return this.response.body;
  },
  set body(data) { this.response.body = data;
  },
  get status() { return this.response.statusCode;
  },
  set status(statusCode) { if (typeofstatusCode ! = ='number') { throw new Error('statusCode must be a number ');
    } this.response.statusCode = statusCode; }};module.exports = context;
Copy the code

As you can see from the code above, context.js is a proxy for some common methods or properties,

For example, context.url is used to proxy context.request.url directly, and context.body is used to proxy context.response.body, Context. status proxies context.response.status.

But context.request and context.response will be mounted in application.js.

So our context.js code could look like this:

let proto = {};

function delegateSet(property, name) {
    proto.__defineSetter__(name, function (val) {
        this[property][name] = val;
    });
}

function delegateGet(property, name) {
    proto.__defineGetter__(name, function () {
        return this[property][name];
    });
}

let requestSet = [];
let requestGet = ['query'];

let responseSet = ['body', 'status'];
let responseGet = responseSet;

requestSet.forEach(ele => {
    delegateSet('request', ele);
});

requestGet.forEach(ele => {
    delegateGet('request', ele);
});

responseSet.forEach(ele => {
    delegateSet('response', ele);
});

responseGet.forEach(ele => {
    delegateGet('response', ele);
});

module.exports = proto;

Copy the code

Delegates can also, instead of defining delegateGet ourselves, delegateSet delegates directly into the delegates library

Finally, we need to modify the application.js code to introduce the Request, Response, and context objects. The following code:

const Emitter = require('events')
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

class Application extends Emitter {
  constructor() {
    super(a)this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
    this.middleWares = []
  }
  listen(port, callback) {
    const server = http.createServer(this.callback())
    server.listen(port)
    callback()
  }
  use(fn) {
    this.middleWares.push(fn)
    // keep the chain call of use
    return this
  }
  callback() {
    return (req, res) = > {
      let ctx = this.createContext(req, res)
      // Create the response content
      let response = () = > this.responseBody(ctx)
      // Call the compose function to combine all functions. Middleware time to parse
      const fn = this.compose()
      return fn(ctx).then(response)
    }
  }
  createContext(req, res) {
    let ctx = Object.create(this.context)
    ctx.request = Object.create(this.request)
    ctx.response = Object.create(this.response)
    ctx.req = ctx.request.req = req
    ctx.res = ctx.response.res = res
    return ctx
  }
  responseBody(ctx) {
    const content = ctx.body
    if (typeof content === 'string') {
      ctx.res.end(content)
    } else if (typeof content === 'object') {
      ctx.res.end(JSON.stringify(content))
    }
  }
}

module.exports =  Application

Copy the code

We added the createContext method, which is key. It creates CTX through Object.create and mounts request and Response to CTX, and native REq and RES to CTX child properties.

Realization of middleware mechanism

It is very clear how a request passes through the middleware to generate a response. Is it very convenient to develop and use the middleware in this mode

Now that we want to implement a mechanism similar to koA2 middleware, how do we do it?

The peeling onion model of KOA is implemented with generator + co.js in KOA1, while KOA2 is implemented with async/await + Promise. Next, we implement the middleware mechanism in KOA2 based on async/await + Promise. First, assume that when koA’s middleware mechanism is in place, it can successfully run the following code:

We all know that koA2 uses async/await. Let’s say we now have three simple async functions:

// Suppose there are three test functions that want to implement the middleware mechanism in KOA
async function fun1(next) {
  console.log(1111);
  await next();
  console.log('aaaaaa');
}

async function fun2(next) {
  console.log(22222);
  await next();
  console.log('bbbbb');
}

async function fun3() {
  console.log(3333);
}
Copy the code

For the three simple functions above, I want to construct a function and execute them in sequence, first execute fun1, print 1111, then execute the next function fun2, print 22222 when I get await next(), then execute fun3 when I get await next(). Print 3333, then go ahead and print BBBBB, then print AAAAA.

Next, let’s modify the application.js code:

const Emitter = require('events') const http = require('http') const context = require('./context') const request = require('./request') const response = require('./response') class Application extends Emitter { constructor() { super() this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response) this.middleWares = [] } listen(port, callback) { const server = http.createServer(this.callback()) server.listen(port) callback() } use(fn) { This.middlewares.push (fn) // Hold the chain call of use return this} callback() {return (req, Let response = () => this.responseBody(CTX) // call compose, Const fn = this.pose () return fn(CTX).then(response)}} createContext(req, res) { let ctx = Object.create(this.context) ctx.request = Object.create(this.request) ctx.response = Object.create(this.response) ctx.req = ctx.request.req = req ctx.res = ctx.response.res = res return ctx } compose() { return async ctx => { function createNext(middleware, oldNext) { return async () => { await middleware(ctx, oldNext) } } let len = this.middleWares.length let next = async () => { return Promise.resolve() } for (let i = len - 1;  i >= 0; i--) { let currentMiddleware = this.middleWares[i] next = createNext(currentMiddleware, next) } await next() } } responseBody(ctx) { const content = ctx.body if (typeof content === 'string') { ctx.res.end(content) } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)) } } } module.exports  = ApplicationCopy the code

The core of the compose function is the compose function, which each time wraps its execution function into next as the next parameter of the previous middleware, so that when the loop reaches the first middleware, it only needs to execute next() once, and can chain recursively call all middleware

Fourth, error handling

Here, we directly inherit events to implement:

let EventEmitter = require('events'); 
    class Application extends EventEmitter {
      callback() {
       return (req, res) = > {
         let ctx = this.createContext(req, res)
         // Create the response content
         let response = () = > this.responseBody(ctx)
         // Create an exception catch
         let onerror = (err) = > this.onerror(err, ctx)
         // Call the compose function to combine all functions
         const fn = this.compose()
         return fn(ctx).then(response).catch(onerror)
       }
     }
      onerror(err) {
       if(! (errinstanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err))

       if (404 == err.status || err.expose) return

       if (this.silent) return

       const msg = err.stack || err.toString()
       console.error()
       console.error(msg.replace(/^/gm.' '))
       console.error()
     }}
Copy the code

The demo address

Github.com/kkxiaojun/m…