[ReadingKoa] Day Two — Read and Understand Koa Core

Introduction

We will read the structure and all source code from Koa — a new web framework with async middleware. If you don’t know how middleware of Koa works, you may take a look this post first:

We will cover the all files in Koa, which contains four files only (amazing):

  • application.js
  • context.js
  • request.js
  • response.js

File 1: Application File (application.js)

This is the entry point of Koa. This is how we init a koa server:

const Koa = require('koa');
const app = new Koa();
app.listen(3000);

new Koa() actually instantiate a new Application object, here is the constructor in application.js:

module.exports = class Application extends Emitter {
constructor() {
super();
    this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context); // from File 2: context.js
this.request = Object.create(request); // from File 3: request.js
this.response = Object.create(response); // from File 4: response.js
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}

About Emitter

new Koa() initiate an Application object, which extends Emitter. After extending Emitter class, it will expose an eventEmitterObject.on() function that allows one or more functions to be attached to named events emitted by the object. Meaning that we can attach a event to Koa like this:

const app = new Koa();
app.on('event', (data) => {
console.log('an event occurred! ' + data); // an event occurred! 123
});
app.emit('event', 123);

When the EventEmitter object emits an event, all of the functions attached to that specific event are called synchronously. Any values returned by the called listeners are ignored and will be discarded.

Events | Node.js v12.4.0 Documentation

About Object.create()

We can also see Object.create() in the constructor, it just creates a new object, using an existing object as the prototype of the newly created object. Here is some examples:

const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person);
me.name = "Matthew"; // "name" is a property set on "me", but not on "person"
me.isHuman = true; // inherited properties can be overwritten
me.printIntroduction(); // "My name is Matthew. Am I human? true"

Object.create()

Start Server

After talking about new Koa(), we can look into app.listen(300). If we start the server using app.listen(3000);, the following code will be execute:

listen(...args) {
debug('listen');
// Step 1: call callback(), create a http server
const server = http.createServer(this.callback());
// Step 5: http server created, start listen to port
return server.listen(...args);
}
callback() {
// Step 2: prepare middlewares
const fn = compose(this.middleware);
  if (!this.listenerCount('error')) this.on('error', this.onerror);
  const handleRequest = (req, res) => {
// Step 3: createContext, we will talk more about this
const ctx = this.createContext(req, res);
// Step 4: handleRequest, we will talk more about this
return this.handleRequest(ctx, fn);
};
  return handleRequest;
}

If you wonder how to start a http server without Koa, here is a normal way we create server by using http package directly:

const http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Hello World!');
res.end();
}).listen(8080);

About createContext (added comments to code)

createContext(req, res) {
// create new object by using this.context as
prototype
const context = Object.create(this.context);
// create new object, make sure request and response object can be access inside context object
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
// make sure context, request, response, app object can access each other
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;
// again make sure response object can be accessed inside request object
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
// return context object, this is the ctx object we can use in middleware
return context;
}

About handleRequest (added comments to code)

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
// when all middleware have been finish, call respond()
const handleResponse = () => respond(ctx);
// if res from http package throw error, call onerror function
onFinished(res, onerror);
// middleware part have been covered in the last post, we dont discuss here
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

respond (added comments to code)

// Just attach ctx.body to res, not really special here
function respond(ctx) {
if (false === ctx.respond) return;
  if (!ctx.writable) return;
  const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
  // ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
  if ('HEAD' == ctx.method) {
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}
  // if body does not exist, return 
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
  // If body is buffer, return body directly
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
  // JSON encrypt the body
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}

File 2: Context (context.js)

This file used a package called delegate to export methods in context.js, I have written a article for understanding how the package works:

Here is the bottom part of context.js:

delegate(proto, 'response')
.method('attachment')
.method('redirect')
.....
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.access('querystring')

This means when you access ctx.querystring it actually accessing ctx.request.querystring , and ctx.request is assigned when createContext is called.

So this delegate mainly let you access methods inside response and request easily by using ctx in middleware (because all the middleware have ctx as input). Here is a example of middleware mentioned in day one post:

// Here is the ctx
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'Hello World';
await next();
console.log(4);
});

File 3: Request (request.js)

This is the prototype of ctx.request. This file mainly let you access all data about the http request from this.req, such as header, ip, host, url etc…. here is some example:

get(field) {
const req = this.req;
switch (field = field.toLowerCase()) {
case 'referer':
case 'referrer':
return req.headers.referrer || req.headers.referer || '';
default:
return req.headers[field] || '';
}
},

File 4: Response (response.js)

This is the prototype of ctx.response. This file mainly let you access the data in this.res, such as response header and body, here is part of the source code:

set(field, val) {
if (this.headerSent) return;
    if (2 == arguments.length) {
if (Array.isArray(val)) val = val.map(v => typeof v === 'string' ? v : String(v));
else if (typeof val !== 'string') val = String(val);
this.res.setHeader(field, val);
} else {
for (const key in field) {
this.set(key, field[key]);
}
}
},

BTW

Thanks for reading! If this article can help you understand Koa, please give me some claps =] This is a great support for me.

Reference

Koa — next generation web framework for node.js