Koa is a Web development framework based on NodeJS, which is characterized by small and refined, compared with large and comprehensive Express. Although the two are developed by the same team, each has its own more suitable application scenarios: Express is suitable for larger enterprise applications, and KOA is committed to being a cornerstone of Web development. For example, egg.js is based on KOA.

I’ll write a post on Express source code parsing about the differences and connections between the two frameworks, but I won’t go into that here. The main objectives of this paper are as follows:

The KOA website says, “KOA provides an elegant way to write server-side applications quickly and happily.” What is this elegant approach? How does it work? Let’s take a look and write the source code by hand.

I didn’t know about the sun. I lived in winter

Fool usage

The use of KOa can be said to be silly, but let’s go through it quickly:

The first thing that catches your eye is not a rockery, but Hello World

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

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

app.listen(3000); 
Copy the code

How to write without a frame

let http = require('http')

let server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(4000)
Copy the code

It is found that koA has more use and Listen methods on two instances and CTX and next parameters in the use callback. These four differences, almost all of koA, are what make KOA so powerful.

listen

Simple! The HTTP syntax sugar actually uses http.createserver () and listens on a port.

ctx

Simpler! Using the context mechanism, the original REQ and RES objects are combined into one, and a lot of expansion is carried out, so that developers can easily use more attributes and methods, greatly reduce the time of processing strings, extracting information, and avoid many third-party package introduction process. (for example, ctx.query, ctx.path, etc.)

use

The point! Middleware is the core of KOA. Solve the problem of callback hell in asynchronous programming, based on Promise, using the onion model idea, make the nested, entangled code become clear, clear, and extensible, customizable, with the help of many third party middleware, can make the streamlined KOA more versatile (such as KOA-Router, the implementation of routing). The main idea is a very neat compose function. When in use, skip from one middleware to the next using the next() method.

Note: The above parts in bold are described in detail below.

The source code

How simple is KOA? It’s as simple as four files, with lots of blank lines and comments, adding up to less than 1,800 lines of code (or a few hundred useful lines).

Github.com/koajs/koa/t…

So, learning koA source code is not a painful process. Howe is not exaggerating to say that with these four files and over 100 lines of code handwritten, you can fully understand KOA. I’m going to go into a lot of detail to prevent big chunks of code.

The preparatory work

To imitate the official, we create a KOA folder and create four files: application.js, context.js, request.js, and Response.js. By looking at package.json, you can see that application.js is the entry file.

Js is related to the context object, request.js is related to the request object, and response.js is related to the response object.

  • First of all, to sort out the idea, the principle is nothing more than use when get a callback function, listen when the execution of this function.

  • In addition, the use callback parameter CTX extends a lot of functionality to the original req and res.

  • Use (); next(); next(); next();

  • So, it seems very simple, but there are only two difficulties: how to process native REQ and RES into CTX, and how to implement middleware.

  • First of all, CTX is really just a context object, request and Response are two files that extend the properties, the context file implements the proxy, and we’ll write the source code for that by hand.

  • Second, the middleware in the source code is implemented by a middleware implementation module, KOA-compose, which we’ll write by hand here.

application.js

Koa is a class with two main methods on it: use and Listen.

As mentioned above, LISTEN is the syntactic sugar of HTTP, so the HTTP module is included.

Koa has a set of error handling mechanisms that listen for instance error events. So introduce the Events module to inherit EventEmitter. Introduce three more custom modules.

let http = require('http')
let EventEmitter = require('events')
let context = require('./context')
let request = require('./request')
let response = require('./response')

class Koa extends EventEmitter {
  constructor () {
    super()
  }
  use () {

  }
  listen () {

  }
}

module.exports = Koa
Copy the code

These three modules, in fact, are an object, in order to code can run through, here first a simple export.

context.js

Module. Exports = proto. // exports = protoCopy the code

request.js

let request = {}
module.exports = request
Copy the code

response.js

let response = {}
module.exports = response
Copy the code

1. The listen method creates an HTTP service and listens to a port. 2, the use method passes in the callback.

Class Koa extends EventEmitter {constructor () {super() this.fn} use (fn) {this.fn = fn; Callback to this.fn} listen (... Args) {let server = http.createserver (this.fn) // Put the callback server.listen(... }}}}}}}}}Copy the code

This will enable you to start a service.

Let app = new Koa() app.use((req, res)) => {// do not write middleware, Next res.end('hello world')}) app.Listen (3000)Copy the code

CTX is a context object that binds many requests and related data and methods, such as cxx.path, cxx.query, cxx.body (), etc., which greatly facilitates development.

The thinking goes like this: When the user calls the use method, it saves the fn callback, creates a createContext function to create the context, creates a handleRequest function to handle the request, and when the user listens, it puts the handleRequest into the createServer callback. Call fn inside the function and pass in the context object, and the user gets CTX.

Class Koa extends EventEmitter {constructor () {super() this.fn this.context = context Response = response} use (fn) {this.fn = fn} createContext(req, res){// This is the core, Create () const CTX = object.create () const request = const CTX = object.create () const request = const CTX = Object ctx.request = Object.create(this.request) const response = ctx.response = Object.create(this.response) // Read through the dizzying operations below, Req = request.req = Response.req = req ctx.res = request.res = Response.res = res request.ctx = Response.ctx Response = response response.request = request return CTX} handleRequest(req,res){// create a function to handleRequest let CTX = this.createcontext (req, res) // Create CTX this.fn(CTX) // call the callback given by the user to return the CTX to the user. Res.end (cxx.body) // cxx.body is used to output to the page, and then it will say how to bind data to cxx.body} Listen (... Args) {let server = http.createserver (this.handlerequest.bind (this))// Bind is called here in case this loses server.Listen (... args) } }Copy the code

If you don’t understand object.create, look at this example:

let o1 = {a: 'hello'} let o2 = object.create (o1) o2.b = 'world' console.log('o1:', o1.b) O2.a) // The created object inherits the properties of the original objectCopy the code

o1: undefined

o2: hello


After the above operation, the user can use various postures to get the desired value on CTX.

For example, the URL can be obtained by using ctx.req. Url, ctx.request.req. Url, or ctx.response.req.

app.use((ctx) => {
  console.log(ctx.req.url)
  console.log(ctx.request.req.url)
  console.log(ctx.response.req.url)
  console.log(ctx.request.url)
  console.log(ctx.request.path)
  console.log(ctx.url)
  console.log(ctx.path)
})
Copy the code

Access localhost: 3000 / ABC

/abc

/abc

/abc

/undefined

/undefined

/undefined

/undefined

Posture, not necessarily cool, to be cool, we hope to achieve the following two points:

  • Customize request values and extend attributes other than native attributes, such as Query Path, etc.
  • Can be directly through the ctx.url method value, above is not convenient.

1 change request

request.js

Let url = require('url') let request = {get url() {// Instead of using the native req return this.req.url}, get path() {return url.parse(this.req.url).pathname}, Get query() {return url.parse(this.req.url).query} //... } module.exports = requestCopy the code

It’s very simple. You can bind the data to the request by using the object get accessor to return processed data. The problem here is how to get the data. Use some third-party modules to reQ processing can be, the source code on the expansion of a very much, here is only a few examples, understand the principle can be.

Access localhost: 3000 / ABC? id=1

/abc? id=1 /abc? id=1 /abc? id=1 /abc? id=1 /abc undefined undefined

2 The next step is to implement the direct value of CTX, which is achieved through an agent

context.js

Let defineGetter = {} function defineGetter(prop, name){// create defineGetter, Proto.__definegetter__ (name, function(){// each object has a __defineGetter__ method. You can use this method to implement the proxy. Return this[prop][name] return this[prop][name] Url})} defineGetter('request', 'url') defineGetter('request', 'path') //....... module.exports = protoCopy the code

Access localhost: 3000 / ABC? id=1

/abc? id=1 /abc? id=1 /abc? id=1 /abc? id=1 /abc /abc? id=1 /abc

The __defineGetter__ method binds a function to a specified property of the current object. When the value of that property is read, your bound function is called. The first argument is the property and the second is the function. The __defineGetter__ method is triggered, so this is CTX. This way, when the defineGetter method is called, the argument 2 property of argument 1 can be proxied to CTX.

Question, how many times do I have to call defineGetter for how many properties to proxy? Yes, if you want to be elegant, you can imitate the official source code, propose a delegates module, bulk agent (actually not elegant to where), here for the convenience of display, or understand it.

3 Modify Response. According to the KOA API, the output to the page is not res.end(‘xx’) or res.send(‘xx’), but ctx.body = ‘xx’. We need to implement setting cctX.body, and we need to implement getting cCTX.body.

response.js

Let response = {get body(){return this._body ()}, set body(value){this.res.statusCode = 200 }} module. Exports = response.}} module. Exports = responseCopy the code

This gets ctx.Response.body, not ctx.body, again, using context proxy

Modify the context

let proto = { } function defineGetter (prop, name) { proto.__defineGetter__(name, function(){ return this[prop][name] }) } function defineSetter (prop, name) { proto.__defineSetter__(name, Function (val){this[prop][name] = val})} defineGetter('request', 'url') defineGetter('request', 'path') defineGetter('response', 'body') Module. Exports = protoCopy the code

Test the

app.use((ctx) => {
  ctx.body = 'hello world'
  console.log(ctx.body)
})
Copy the code

Access localhost: 3000

Output from the node console:

hello world

The page displays hello World

Next, let’s fix the body problem. It says that once you set the body value, the status code is changed to 200, so if you don’t set the body value, it should be 404. Also, the user will not only output strings, but also files, pages, JSON, etc., so change the handleRequest function:

Let Stream = require(' Stream ') // Introduce Stream handleRequest(req,res){res.statusCode = 404 // default 404 let CTX = This.createcontext (req, res) this.fn(CTX) if(typeof ctx.body == 'object'){// If it is an object, Res.setheader (' content-type ', 'application/json; Charset =utf8') res.end(json.stringify (cxx.body))} else if (cxx.body instanceof Stream){// if (cxx.body. Pipe (res)} Else if (typeof CTX. Body = = = 'string' | | Buffer. IsBuffer (CTX. Body)) {/ / if it is a string or Buffer res. SetHeader (' the content-type ', 'text/htmlcharset=utf8') res.end(ctx.body) } else { res.end('Not found') } }Copy the code

Now that context is in place, let’s look at the most important thing: middleware

Now that we can only use once, we need to implement use multiple times, and we can use the next method in the use callback to jump to the next middleware. Before we do that, let’s look at a concept: the “onion model.”

When we use “use” many times

    app.use((crx, next) => {
        console.log(1)
        next()
        console.log(2)
    })
    app.use((crx, next) => {
        console.log(3)
        next()
        console.log(4)
    })
    app.use((crx, next) => {
        console.log(5)
        next()
        console.log(6)
    })
Copy the code

It is executed in this order:

One, three, five, six, four, two

The next method calls the next use, and the code below next executes after the next use. We can think of the above code as:

app.use((ctx, Next) => {console.log(1) // next() is replaced with the code console.log(3) // next() is replaced with the code console.log(5) // next() Log (6) console.log(4) console.log(2)})Copy the code

In this case, the output should be 135642

This is the Onion model, with next passing execution to the next middleware.

In this way, the request data in the hands of the developer will act like a guard of honor, passing through each layer of middleware and finally responding to the user.

It manages complex operations without messy nesting.

In addition, KOA’s middleware supports async/await

app.use(async (ctx, next) => { console.log(1) await next() console.log(2) }) app.use(async (ctx, Next) => {console.log(3) let p = new Promise((resolve, roject) => {setTimeout(() => {console.log('3.5') resolve()}, 1000) }) await p.then() await next() console.log(4) ctx.body = 'hello world' })Copy the code

1 3 // after one second 3.5 4 2

Async returns a promise, and when the next of the previous use is preceded by the await keyword, it waits for the next use callback to resolve before continuing.

All that needs to be done now is in two steps:

The first step is to string multiple use callbacks in order.

Arrays and recursion are used here. Each use stores the current function in an array and executes it in order. To do this, we use a function called compose, which is the most important function.

Constructor () {super() // this.fn This.middlewares = [] this.context = context this.request = request this.response = Response} Use (fn) {// this.fn = fn } compose(middlewares, CTX){// compose(middlewares, CTX) If (index === middlewares. Length) return if(index === middlewares The last time next cannot be executed, Let middleware = middlewares[index] // Select the current function that should be called middleware(CTX, () => Dispatch (index + 1)) // call and pass CTX and the next function to be called, } handleRequest(req,res){res.statusCode = 404 let CTX = this.createcontext (req,res) Res) // this.fn(CTX) to: This.pose (this.middlewares, CTX) If (typeof cx. Body == 'object'){res.setheader (' content-type ', 'application/json; charset=utf8') res.end(JSON.stringify(ctx.body)) } else if (ctx.body instanceof Stream){ ctx.body.pipe(res) } else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) { res.setHeader('Content-Type', 'text/htmlcharset=utf8') res.end(ctx.body) } else { res.end('Not found') } }Copy the code

Re-test the example printed above with 123456, you can correctly get 135642

Step two, wrap each callback intoPromiseTo achieve asynchrony.

Finally, use promise.resolve to wrap each callback as a Promise and then call it. If you don’t know the Promise, see my next post [juejin. Im /post/684490…

compose(middlewares, CTX){function dispatch(index){if(index === middlewares. Length) return promise.resolve () // If the last middleware, Let middleware = middlewares[index] return promise.resolve (middleware(CTX, () => dispatch(index + 1))) // Wrap the middleware with promise.resolve} Return dispatch(0)} handleRequest(req,res){res.statusCode = 404 let ctx = this.createContext(req, res) let fn = this.compose(this.middlewares, If (typeof cx. Body == 'object'){res.setheader (' content-type ', 'application/json '; charset=utf8') res.end(JSON.stringify(ctx.body)) } else if (ctx.body instanceof Stream){ ctx.body.pipe(res) } else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) { res.setHeader('Content-Type', 'text/htmlcharset=utf8') res.end(cxx.body)} else {res.end('Not found')}}).catch(err => {// monitor error emitted error, For app.on('error', (err) =>{}) this.emit('error', err) res.statuscode = 500 res.end('server error')})}Copy the code

Complete Application code

let http = require('http') let EventEmitter = require('events') let context = require('./context') let request = require('./request') let response = require('./response') let Stream = require('stream') class Koa extends EventEmitter { constructor () { super() this.middlewares = [] this.context = context this.request = request this.response = response } use (fn) { this.middlewares.push(fn) } createContext(req, res){ const ctx = Object.create(this.context) const request = ctx.request = Object.create(this.request) const response =  ctx.response = Object.create(this.response) ctx.req = request.req = response.req = req ctx.res = request.res = response.res = res request.ctx = response.ctx = ctx request.response = response response.request = request return ctx } compose(middlewares, ctx){ function dispatch (index) { if (index === middlewares.length) return Promise.resolve() let middleware = middlewares[index] return Promise.resolve(middleware(ctx, () => dispatch(index + 1))) } return dispatch(0) } handleRequest(req,res){ res.statusCode = 404 let ctx = this.createContext(req, res) let fn = this.compose(this.middlewares, ctx) fn.then(() => { if (typeof ctx.body == 'object') { res.setHeader('Content-Type', 'application/json; charset=utf8') res.end(JSON.stringify(ctx.body)) } else if (ctx.body instanceof Stream) { ctx.body.pipe(res) } else if (typeof ctx.body === 'string' || Buffer.isBuffer(ctx.body)) { res.setHeader('Content-Type', 'text/htmlcharset=utf8') res.end(ctx.body) } else { res.end('Not found') } }).catch(err => { this.emit('error', err) res.statusCode = 500 res.end('server error') }) } listen (... args) { let server = http.createServer(this.handleRequest.bind(this)) server.listen(... args) } } module.exports = KoaCopy the code

conclusion

With all the core features written, you should know enough about KOA from this article to give it a thumbs-up if it helped you.