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