Deep Interpretation of Koa Source in Node.js

Time:2019-10-1

Preface

Node. JS has also been writing for two or three years. When I first started learning Node, Hello world created an HttpServer. Later, I also experienced Express, Koa1.x, Koa2.x and routing-controllers (still driven by Express and Koa) that combined TypeScript.

Koa version is the most used version, but also interested in its onion model, so recently take the time to read its source code, just in the near future may be a refactoring Express project, refactoring it into Koa2.x version, so reading its source code is also an effective help for refactoring.

How did Koa come from?

First, we need to determine what Koa is.

The emergence of any framework is to solve the problem, while Koa is to build HTTP services more easily.

It can be simply understood as a middleware framework for HTTP services.

Using HTTP module to create HTTP service

I believe that when you learn Node, you should have written code like this:

const http = require('http')
const serverHandler = (request, response) => {
Response.end ('Hello World') // Return data
}
http
.createServer(serverHandler)
.listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))

For the simplest example, you can see a Hello World string by visiting http://127.0.0.1:8888 after the script runs.
But this is only a simple example, because no matter what address we visit (or even modify the requested Method), we always get the string:


> curl http://127.0.0.1:8888
> curl http://127.0.0.1:8888/sub
> curl -X POST http://127.0.0.1:8888

So we may add logic to the callback and return the corresponding data to the user according to the path and method:

const serverHandler = (request, response) => {
// default
let responseData = '404'
if (request.url === '/') {
if (request.method === 'GET') {
responseData = 'Hello World'
} else if (request.method === 'POST') {
responseData = 'Hello World With POST'
}
} else if (request.url === '/sub') {
responseData = 'sub page'
}
Response. end (responseData) // Return data
}

Implementation of Express-like

But there is another problem with this way of writing. If it’s a big project, there are more than N interfaces.

It would be too difficult to maintain if they were all written in this handler.

The example simply assigns values to a variable, but real projects don’t have such simple logic.

So, we abstract handler once, so that we can easily manage the path:


class App {
constructor() {
this.handlers = {}
this.get = this.route.bind(this, 'GET')
this.post = this.route.bind(this, 'POST')
}
route(method, path, handler) {
let pathInfo = (this.handlers[path] = this.handlers[path] || {})
// register handler
pathInfo[method] = handler
}
callback() {
return (request, response) => {
let { url: path, method } = request
this.handlers[path] && this.handlers[path][method]
? this.handlers[path][method](request, response)
: response.end('404')
}
}
}

Then, by instantiating a Router object to register the corresponding path, the service is finally started:


const app = new App()
app.get('/', function (request, response) {
response.end('Hello World')
})
app.post('/', function (request, response) {
response.end('Hello World With POST')
})
app.get('/sub', function (request, response) {
response.end('sub page')
})
http
.createServer(app.callback())
.listen(8888, _ => console.log('Server run as http://127.0.0.1:8888'))

Middleware in Express

In this way, a clean HTTP Server is implemented, but the function is still very simple.

If we have a requirement now, we need to add some parameters to the front of some requests, such as the unique ID of a request.

Repetition of code in our handler is certainly not advisable.

So we need to optimize the processing of route to support passing in multiple handlers:


route(method, path, ...handler) {
let pathInfo = (this.handlers[path] = this.handlers[path] || {})
// register handler
pathInfo[method] = handler
}
callback() {
return (request, response) => {
let { url: path, method } = request
let handlers = this.handlers[path] && this.handlers[path][method]
if (handlers) {
let context = {}
function next(handlers, index = 0) {
handlers[index] &&
handlers[index].call(context, request, response, () =>
next(handlers, index + 1)
)
}
next(handlers)
} else {
response.end('404')
}
}
}

Then add another handler for the path monitoring above:


function generatorId(request, response, next) {
this.id = 123
next()
}
app.get('/', generatorId, function(request, response) {
response.end(`Hello World ${this.id}`)
})

So when you access the interface, you can see the words Hello World 123.

This can simply be thought of as a middleware implemented in Express.

Middleware is the core of Express and Koa. All dependencies are loaded through middleware.

More Flexible Middleware Solution-Onion Model

The above scheme can really make it easy to use some middleware, call next () in process control to enter the next link, and the whole process becomes very clear.

But there are still some limitations.

For example, if we need time-consuming statistics for some interfaces, there are several possible solutions in Express:

function beforeRequest(request, response, next) {
this.requestTime = new Date().valueOf()
next()
}
// Solution 1. Modify the original handler processing logic, make time-consuming statistics, and send data by end.
app.get('/a', beforeRequest, function(request, response) {
// Time-consuming statistics of requests
console.log(
`${request.url} duration: ${new Date().valueOf() - this.requestTime}`
)
response.end('XXX')
})
// Solution 2. Move the logic of output data into a postpositioned Middleware
function afterRequest(request, response, next) {
// Time-consuming statistics of requests
console.log(
`${request.url} duration: ${new Date().valueOf() - this.requestTime}`
)
response.end(this.body)
}
app.get(
'/b',
beforeRequest,
function(request, response, next) {
this.body = 'XXX'
Next ()// remember to call, otherwise the middleware will terminate here
},
afterRequest
)

Either way, the original code is a destructive modification, which is undesirable.

Because Express uses response. end () to return data to the interface requester, the subsequent code execution will be terminated after the call.

And because there was no good solution to wait for the execution of asynchronous functions in a middleware.


function a(_, _, next) {
console.log('before a')
let results = next()
console.log('after a')
}
function b(_, _, next) {
console.log('before b')
setTimeout(_ => {
this.body = 123456
next()
}, 1000)
}
function c(_, response) {
console.log('before c')
response.end(this.body)
}
app.get('/', a, b, c)

As in the example above, the output order of the log is actually:


before a
before b
after a
before c

This obviously doesn’t meet our expectations, so it’s meaningless to get the return value of next () in Express.

So there’s the onion model Koa brought in, just in time for Koa 1. x to appear, just in time for Node to support the new grammar, Generator functions and Promise definitions.
That’s why we have such amazing libraries as co, and when our middleware uses Promise, the former middleware can easily handle its own affairs after the subsequent code has been executed.

However, the role of Generator itself is not to help us use Promise to control asynchronous processes more easily.

So, with the release of Node 7.6, the async and await grammars were supported, and Koa2.x was introduced by the community, replacing the co+Generator with async grammar.

Koa also removes CO from dependencies (version 2.x uses koa-conversion to convert Generator functions to promise, and version 3.x will not support Generator directly)

Since there is no difference between the two versions of Koa in terms of function and usage, and at most some grammatical adjustments, I will skip some Koa 1. x related things and go straight to the topic.

In Koa, middleware can be defined and used in the following ways:


async function log(ctx, next) {
let requestTime = new Date().valueOf()
await next()
console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}
router.get('/', log, ctx => {
// do something...
})

Because the existence of some grammatical sugars obscures the actual running process of the code, we use Promise to restore the above code:


function log() {
return new Promise((resolve, reject) => {
let requestTime = new Date().valueOf()
next().then(_ => {
console.log(`${ctx.url} duration: ${new Date().valueOf() - requestTime}`)
}).then(resolve)
})
}

The code is roughly like this, that is to say, calling next returns us a Promise object, and when Promise resolves is what Koa does internally.
You can simply implement it (for the App class implemented above, you just need to modify the callback):

callback() {
return (request, response) => {
let { url: path, method } = request
let handlers = this.handlers[path] && this.handlers[path][method]
if (handlers) {
let context = { url: request.url }
function next(handlers, index = 0) {
return new Promise((resolve, reject) => {
if (!handlers[index]) return resolve()
handlers[index](context, () => next(handlers, index + 1)).then(
resolve,
reject
)
})
}
next(handlers).then(_ => {
// Closing the request
response.end(context.body || '404')
})
} else {
response.end('404')
}
}
}

Each time the middleware is called, then is listened on and the resolve and reject processing of the current Promise is passed into the callback of Promise.

That is to say, only when the resolve of the second middleware is called will the then callback of the first middleware be executed.

This implements an onion model.

Like the process that our log middleware executes:

  1. Get the current timestamp requestTime
  2. Call next () to execute subsequent middleware and listen for callbacks
  3. The third, fourth and fifth middleware may be called in the second middleware, but this is not what log cares about. Log only cares about when the second middleware resolves, while the resolve of the second middleware relies on the resolution of the middleware behind it.
  4. When the second middleware resolves, this means that no other middleware is executed (all resolves), then log will continue to execute the following code.

So like onions, layers of wrapping, the outermost layer is the largest, the first to execute, and the last to execute. (In a complete request, next is executed first, next is executed last)

The above is the whole content of this article. I hope it will be helpful to everyone’s study, and I hope you will support developpaer more.

Recommended Today

Details of multi-path and large capacity hard disk mount under CentOS

I. application environment and requirementsBlade servers connect HP storage through fiber switches, forming a 2×2 link The storage capacity of the operating system for CentOS 6.4 64 bit mount is 2.5t Based on this application environment, two problems need to be solved: In order to ensure the stability and transmission performance of the link, multi-path […]