The original address

On Friday, the students in the group discussed some fun things. Someone mentioned some ideas like “koA in 5 minutes” and “React in 100 lines”. After careful thought, it was not impossible to achieve KOA in 5 minutes, so this blog was created.

To prepare

Just go to the KOA official website and find a demo that represents the core functions of KOA

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

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time'.`${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
Copy the code

The final effect is to implement a 5min-koa module, directly replace the first line of code with const koa = require(‘./5min-koa’); , the program can be executed normally.

The core of Koa

According to the KOA website, the app.listen method is actually shorthand for the following code

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
Copy the code

So we can implement app.listen first

class Koa {
  constructor() {}
  callback() {
    return (req, res) = > {
      // TODO
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port); }}Copy the code

The core of KOA is divided into four parts

  • The context context
  • Middleware middleware
  • Request the request
  • Responce response

Context

Let’s implement a very simplified version of context, as follows

class Context {
  constructor(app, req, res) {
    this.app = app
    this.req = req
    this.res = res
    // In order to minimize the implementation time, we used the native res and req directly and did not implement ctx.request ctx.response on CTX
    // ctx.request ctx.response just wraps one layer over the native RES and REq
  }
  // Implement some of the CTX proxy methods used in demo
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}
Copy the code

So you’ve got a basic Context, which is pretty small, but enough. Each time a new request is made, a new CTX object is created.

Middleware

Koa’s middleware is an asynchronous function that takes two parameters, CTX and Next, where CTX is the current request context and next is the next middleware (also asynchronous function). To think of it this way, we need an array to maintain the middleware, and each call to app.use pushes one function into the array. So the use method is implemented as follows

use(middleware) {
  this.middlewares.push(middleware)
}
Copy the code

Each time a new request is made, we need to inject the context of that request into each middleware in the array. It is not enough to just flood CTX, but to make each middleware call to the next middleware through the next function. When we call the next function, we usually do not need to pass the parameters, and the called middleware must receive CTX and next parameters.

Callers don’t need to pass arguments, but callers can receive arguments, which immediately reminded me of the bind method. If only CTX and next were pre-bound for each middleware, the problem would be solved. The following code converts the list of Middleware passed in by the user into a list of next functions using the bind method

let bindedMiddleware = []

for (let i = middlewares.length - 1; i >= 0; i--) {
  if (middlewares.length == i + 1) {
    // Finally, the next method is set to promise.resolve
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
  } else {
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0))}}Copy the code

We end up with an array of next functions, the bindedMiddleware variable.

Request

The callback function in http.createServer is called each time a request is received, so we write code to handle the request in the TODO location of the callback method above, and put the middleware list above into the next list of functions.

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = []
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0))}}return bindedMiddleware[0]()
  } else {
    return Promise.resolve()
  }
}
Copy the code

Responce

Let’s just make it simple and send ctx.body directly to the client.

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { 'Content-Type': 'text/plain'}); ctx.res.end(ctx.body); }}Copy the code

Complete the Koa class implementation

Koa app instance with on, EMIT and other methods, which is the node Events module implementation of good things. Let the Koa class inherit directly from the Events module. We then put the above handleRequest and handleResponse methods into the callback method of the KOA class to obtain the final KOA, a total of 58 lines of code, as shown below

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

class Context {
  constructor(app, req, res) {
    this.app = app;
    this.req = req;
    this.res = res;
  }
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

class Koa extends Emitter{
  constructor(options) {
    super(a);this.options = options
    this.middlewares = [];
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  callback() {
    return (req, res) = > {
      let ctx = new Context(this, req, res);
      handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port); }}function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = [];
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0])); }}return bindedMiddleware[0] (); }else {
    return Promise.resolve(); }}function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { 'Content-Type': 'text/plain'}); ctx.res.end(ctx.body); }}module.exports = Koa;
Copy the code

Try running the first Demo, no problem.

conclusion

Simple implementation, rough code is not rough, shows the core of KOA, but with less error handling, and no consideration for performance, there are many, many areas to improve.

I wrote this 5 minutes after koA to see koA source, found that the implementation of the idea is basically like this, I believe that after the baptism of this 5 minutes KOA, you go to see KOA source as a piece of cake.

Done!