The Koa source code is minimal, with less than 2k lines of code, and consists of four module files, making it a good place to learn.

Reference code: learn-koa2

Let’s look at some native Node implementation Server code:

const http = require('http');

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

server.listen(3000, () = > {console.log('server start at 3000');
});
Copy the code

Very simple a few lines of code, to achieve a Server Server. The createServer method receives a callback that performs various operations on each requested REq RES object and returns a result. However, it is also obvious that the callback function can easily become bloated with the complexity of the business logic. Even if the callback function is divided into small functions, it will gradually lose control of the whole process in the complex asynchronous callback.

In addition, Node provides some natively provided apis that sometimes confuse developers:

res.statusCode = 200;
res.writeHead(200);
Copy the code

Changing an RES property or calling an RES method can change the HTTP status code, which can easily lead to different code styles in a multi-person project.

Let’s look at the Koa implementation Server:

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

app.use(async (ctx, next) => {
  console.log('1-start');
  await next();
  console.log('1-end');
});


app.use(async (ctx, next) => {
  console.log('2-start');
  ctx.status = 200;
  ctx.body = 'Hello World';
  console.log('2-end');
});


app.listen(3000);

// Final output:
// 1-start
// 2-start
// 2-end
// 1-end
Copy the code

Koa uses middleware concepts to handle an HTTP request, and Koa uses async and await syntax to make asynchronous processes more manageable. The CTX execution context proxies the native RES and REQ, allowing developers to avoid the underlying layer and access and set properties through the proxy.

After reading the comparison, we should have a few questions:

  • ctx.statusWhy can you directly set the status code, is not not seeresThe object?
  • MiddlewarenextWhat is it? Why executenextIs it the next piece of middleware?
  • After all middleware execution is complete, why can I go back to the original middleware (onion model)?

Now let’s take a look at the source code with some confusion and implement a simple version of Koa ourselves!

Encapsulate the HTTP Server

Reference code: step-1

// How to use Koa
const Koa = require('koa');
const app = new Koa();

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

app.listen(3000);
Copy the code

We started by mimicking the use of KOA and building a simple skeleton:

Create new kao/application.js.

const http = require('http');

class Application {
  constructor() {
    this.callbackFn = null;
  }

  use(fn) {
    this.callbackFn = fn;
  }

  callback() {
    return (req, res) = > this.callbackFn(req, res) } listen(... args) {const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

module.exports = Application;
Copy the code

Create a new test file kao/index.js

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

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

app.listen(3001, () = > {console.log('server start at 3001');
});
Copy the code

We’ve encapsulated the HTTP Server preliminaries: instantiate an object with new, register the callback function with use, and start the server with Listen and pass in the callback.

Note that when new is called, the server server is not actually turned on. It is actually turned on when listen is called.

This code, however, has significant drawbacks:

  • Use passes the callback function and receives the arguments that are still nativereqandres
  • Multiple calls to use override the previous middleware, rather than executing multiple middleware in sequence

Let’s tackle the first problem first

Encapsulate the REq and RES objects and construct the context

Reference code: step-2

Let’s start with the GET and set references in ES6

Get and set based on ordinary objects

const demo = {
  _name: ' ',
  get name() {
    return this._name;
  },

  set name(val) {
    this._name = val; }}; demo.name ='deepred';
console.log(demo.name);
Copy the code

Class-based get and set

class Demo {
  constructor() {
    this._name = ' ';
  }

  get name() {
    return this._name;
  }

  set name(val) {
    this._name = val; }}const demo = new Demo();
demo.name = 'deepred';
console.log(demo.name);
Copy the code

Get and set based on Object.defineProperty

const demo = {
  _name: ' '
};

Object.defineProperty(demo, 'name', {
  get: function() {
    return this._name
  },

  set: function(val) {
    this._name = val; }});Copy the code

Proxy-based GET and set

const demo = {
  _name: ' '
};

const proxy = new Proxy(demo, {
  get: function(target, name) {
    return name === 'name' ? target['_name'] : undefined;
  },

  set: function(target, name, val) {
    name === 'name' && (target['_name'] = val)
  }
});
Copy the code

There were also implementations of __defineSetter__ and __defineGetter__, which are now deprecated.

const demo = {
  _name: ' '
};

demo.__defineGetter__('name'.function() {
  return this._name;
});

demo.__defineSetter__('name'.function(val) {
  this._name = val;
});
Copy the code

The main difference is that object.defineProperty __defineSetter__ Proxy can set attributes dynamically, whereas otherwise they can only be set at definition time.

Request. js and Response. js in Koa use a lot of get and set proxies

New kao/request. Js

module.exports = {
  get header() {
    return this.req.headers;
  },

  set header(val) {
    this.req.headers = val;
  },

  get url() {
    return this.req.url;
  },

  set url(val) {
    this.req.url = val; }},Copy the code

When you visit request.url, you are actually visiting the native req.url. Note that the this.req native object is not injected at this time!

Create kao/response.js in the same way

module.exports = {
  get status() {
    return this.res.statusCode;
  },

  set status(code) {
    this.res.statusCode = code;
  },

  get body() {
    return this._body;
  },

  set body(val) {
    // The source code contains various judgments about the type val, which are omitted here
    /* Possible types 1. string 2. Buffer 3. Stream 4. Object */
    this._body = val; }}Copy the code

We don’t use the native this.res.end for the body, because the body will be read and modified multiple times when we write koA code, so the actual action that returns the browser information is wrapped and manipulated in application.js

Also note that the this.res native object is not injected at this time!

New kao/context. Js

const delegate = require('delegates');

const proto = module.exports = {
  // Context's own method
  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>'.res: '<original node res>'.socket: '<original node socket>'}; }},// The delegates principle is __defineGetter__ and __defineSetter__

// Method is a delegate method, getter delegates getter,access delegates getter and setter.

// proto.status => proto.response.status
delegate(proto, 'response')
  .access('status')
  .access('body')


// proto.url = proto.request.url
delegate(proto, 'request')
  .access('url')
  .getter('header')
Copy the code

Context.js proxies request and response. Ctx. body points to ctx.response.body. But ctx.response ctx.request has not been injected yet!

You might wonder why Response. js and request.js use get set proxies, while context.js uses delegate proxies. The main reason is that the set and GET methods can also add some logic of their own. The delegate, on the other hand, is pure and only proxies properties.

{
  get length() {
    // Own logic
    const len = this.get('Content-Length');
    if (len == ' ') return;
    return~~len; }},// Only proxy attributes
delegate(proto, 'response')
  .access('length')
Copy the code

So context.js is a good place to use delegates, which are simply properties and methods that delegate request and response.

The actual injection of native objects is done in the createContext method of application.js.

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

class Application {
  constructor() {
    this.callbackFn = null;
    // Context Request respones for each Kao instance
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  use(fn) {
    this.callbackFn = fn;
  }

  callback() {
    const handleRequest = (req, res) = > {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx)
    };

    return handleRequest;
  }

  handleRequest(ctx) {
    const handleResponse = (a)= > respond(ctx);
    // callbackFn is an async function that returns a Promise object
    return this.callbackFn(ctx).then(handleResponse);
  }

  createContext(req, res) {
    // For each request, a CTX object is created
    // CTX request response for each request
    // CTX proxy this is where the native REq res is proxy
    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;
    ctx.app = ctx.request.app = ctx.response.app = this;
    returnctx; } listen(... args) {const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

module.exports = Application;

function respond(ctx) {
  // Return the last data according to the type of ctx.body
  String 2.buffer 3.stream 4.object */
  let content = ctx.body;
  if (typeof content === 'string') {
    ctx.res.end(content);
  }
  else if (typeof content === 'object') {
    ctx.res.end(JSON.stringify(content)); }}Copy the code

The code uses the object.create method to create a brand new Object that inherits the original properties through the prototype chain. This can effectively prevent contamination of the original object.

CreateContext is called on each HTTP request, and each call generates a new CTX object and proxies the native object for the HTTP request.

Respond is the last method to return the HTTP response. Res.end terminates the HTTP request based on the type of ctx.body after all middleware is executed.

Now let’s test it again: kao/index.js

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

// Use CTX to modify the status code and response content
app.use(async (ctx) => {
  ctx.status = 200;
  ctx.body = {
    code: 1.message: 'ok'.url: ctx.url
  };
});

app.listen(3001, () = > {console.log('server start at 3001');
});
Copy the code

Middleware mechanisms

Reference code: step-3

const greeting = (firstName, lastName) = > firstName + ' ' + lastName
const toUpper = str= > str.toUpperCase()

const fn = compose([toUpper, greeting]);

const result = fn('jack'.'smith');

console.log(result);
Copy the code

Functional programming has the concept of compose. For example, combine greeting and toUpper into a composite function. Calling this composite function calls the greeting first, then passes the return value to toUpper to continue execution.

Implementation method:

// Imperative programming (procedural oriented)
function compose(fns) {
  let length = fns.length;
  let count = length - 1;
  let result = null;

  return function fn1(. args) {
    result = fns[count].apply(null, args);
    if (count <= 0) {
      return result
    }

    count--;
    returnfn1(result); }}// Declarative programming (functional)
function compose(funcs) {
  return funcs.reduce((a, b) = >(... args) => a(b(... args))) }Copy the code

Koa’s middleware mechanism is similar to compose, again packaging multiple functions into one, but Koa’s middleware is similar to the Onion model, where execution from A middleware to B middleware is completed, and the latter middleware can return to A again.

Koa uses KoA-compose to implement the middleware mechanism, the source code is very concise, but a little difficult to understand. It is recommended to understand recursion first


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! ')}/** * @param {Object} context * @return {Promise} * @api public */

  return function (context, next) {
    // last called middleware #
    let index = - 1
    return dispatch(0)
    function dispatch (i) {
      // Next is called multiple times in one middleware
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // fn is the current middleware
      let fn = middleware[i]
      if (i === middleware.length) fn = next // If the last middleware is also next (usually the last middleware is directly ctx.body, there is no need for next)
      if(! fn)return Promise.resolve() // No middleware, return success
      try {
        
        Resolve (fn(context, function next () {return dispatch(I + 1)})) */
        // dispatch.bind(null, I + 1) is the next argument in the middleware, which is called to access the next middleware

        // if fn returns a Promise object, promise. resolve returns the Promise object directly
        // if fn returns a normal object, promise. resovle promises it
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // Middleware is an async function, so an error doesn't go there and is caught directly in the catch of fnMiddleware
        // Catch middleware is a normal function that promises to get to the catch method of fnMiddleware
        return Promise.reject(err)
      }
    }
  }
}
Copy the code
const context = {};

const sleep = (time) = > new Promise(resolve= > setTimeout(resolve, time));

const test1 = async (context, next) => {
  console.log('1-start');
  context.age = 11;
  await next();
  console.log('1-end');
};

const test2 = async (context, next) => {
  console.log('2-start');
  context.name = 'deepred';
  await sleep(2000);
  console.log('2-end');
};

const fn = compose([test1, test2]);

fn(context).then((a)= > {
  console.log('end');
  console.log(context);
});
Copy the code

Recursive call stack execution:

Knowing the middleware mechanism, we should be able to answer the previous question:

What is next? How is the Onion model implemented?

Next is a function wrapped around Dispatch

Executing next on the NTH middleware executes dispatch(n+1), which goes to the NTH +1 middleware

Since dispatch returns all promises, the NTH middleware await next(); Go to the NTH +1 middleware. When the NTH +1 middleware execution is complete, the NTH middleware can be returned

If next is not called again in one middleware, then all middleware after it is not called again

Modify the kao/application. Js

class Application {
  constructor() {
    this.middleware = []; // Storage middleware
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  use(fn) {
    this.middleware.push(fn); // Storage middleware
  }

  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! ')}/** * @param {Object} context * @return {Promise} * @api public */
  
    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)
        }
      }
    }
  }
  

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

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

    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const handleResponse = (a)= > respond(ctx);
    // Execute the middleware and give the final result to Respond
    return fnMiddleware(ctx).then(handleResponse);
  }

  createContext(req, res) {
    // For each request, a CTX object is created
    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;
    ctx.app = ctx.request.app = ctx.response.app = this;
    returnctx; } listen(... args) {const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

module.exports = Application;

function respond(ctx) {
  let content = ctx.body;
  if (typeof content === 'string') {
    ctx.res.end(content);
  }
  else if (typeof content === 'object') {
    ctx.res.end(JSON.stringify(content)); }}Copy the code

Test the

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

app.use(async (ctx, next) => {
  console.log('1-start');
  await next();
  console.log('1-end');
})

app.use(async (ctx) => {
  console.log('2-start');
  ctx.body = 'hello tc';
  console.log('2-end');
});

app.listen(3001, () = > {console.log('server start at 3001');
});

// 1-start 2-start 2-end 1-end

Copy the code

Error handling mechanism

Reference code: step-4

Because the compose function still returns a Promise object, we can catch exceptions in a catch

kao/application.js

handleRequest(ctx, fnMiddleware) {
  const handleResponse = (a)= > respond(ctx);
  const onerror = err= > ctx.onerror(err);
  // catch the onerror method of CTX
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code

kao/context.js

const proto = module.exports = {
  // Context's own method
  onerror(err) {
    // The middleware reported an error
    const { res } = this;

    if ('ENOENT' == err.code) {
      err.status = 404;
    } else {
      err.status = 500;
    }
    this.status = err.status;
    res.end(err.message || 'Internal error'); }}Copy the code
const Kao = require('./application');
const app = new Kao();

app.use(async (ctx) => {
  // Errors can be caught
  a.b.c = 1;
  ctx.body = 'hello tc';
});

app.listen(3001, () = > {console.log('server start at 3001');
});
Copy the code

Now we have implemented error exception catching in the middleware, but we still lack a mechanism to catch errors in the framework layer. We can make the Application inherit from the native Emitter to implement error listeners

kao/application.js

const Emitter = require('events');

/ / Emitter inheritance
class Application extends Emitter {
  constructor() {
    / / call the super
    super(a);this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response); }}Copy the code

kao/context.js

const proto = module.exports = {
  onerror(err) {
    const { res } = this;

    if ('ENOENT' == err.code) {
      err.status = 404;
    } else {
      err.status = 500;
    }

    this.status = err.status;

    // Raises an error event
    this.app.emit('error', err, this);

    res.end(err.message || 'Internal error'); }}Copy the code
const Kao = require('./application');
const app = new Kao();

app.use(async (ctx) => {
  // Errors can be caught
  a.b.c = 1;
  ctx.body = 'hello tc';
});

app.listen(3001, () = > {console.log('server start at 3001');
});

// Listen for error events
app.on('error', (err) => {
  console.log(err.stack);
});
Copy the code

So far we can see two ways of catching Koa exceptions:

  • Middleware Catch (Promise Catch)
  • Frame capture (Emitter Error)
// Middleware that catches global exceptions
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (error) {
    return ctx.body = 'error'}})Copy the code
// Event listener
app.on('error', err => {
  console.log('error happends: ', err.stack);
});
Copy the code

conclusion

The Koa process can be divided into three steps:

Initialization phase:

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

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

app.listen(3000);
Copy the code

New initializes an instance, use collects middleware into the middleware array, listen synthesizes middleware fnMiddleware, returns a callback function to HTTP. createServer, starts the server, and waits for HTTP requests.

Request stage:

On each request, createContext generates a new CTX, which is passed to fnMiddleware, triggering the entire flow of the middleware

Response stage:

When the entire middleware is complete, the respond method is called, the last processing is done to the request, and the response is returned to the client.

Refer to the flow chart below: