Analysis and implementation of KOA2 framework principle

  html, html5, javascript, node.js

What is the koa framework?

Koa is a new web framework based on node implementation, which was built by the original team of express framework. It is characterized by elegance, conciseness, strong expression and high degree of freedom. Compared with express, it is a lighter node framework because all its functions are implemented through plug-ins. This plug-in architecture design mode is very consistent with unix philosophy.

The koa framework has now been updated to version 2. X. This article starts from scratch, explains the framework source code structure and implementation principle of koa2 step by step, shows and explains some of the most important concepts in the framework source code of koa2, and then hands-on teaches everyone to implement a simple koa2 framework in person to help everyone learn and understand koa2 at a deeper level. After reading this article, check against the source code of koa2. I believe your thinking will be very smooth.

The framework used in this article is koa2, which is different from koa1. koa1 uses generator+co.js, while koa2 uses async/await. therefore, the code and demo in this article need to be run on node 8 and above. if the reader’s version of node is lower, it is recommended to upgrade or install babel-cli and use babel-node to run the code involved in this article.

The complete code gitlab address of the lightweight koa implemented in this article is:article_koa2

Koa source code structure

图片描述

The above picture shows the lib folder of koa2’s source code directory structure. under the lib folder are four koa2 core files: application.js, context.js, request.js, response.js

application.js

Application.js is koa’s entry file. it exports the constructor to create class instances. it inherits events, thus giving the framework the ability to monitor and trigger events. Application also exposes some commonly used apis, such as toJSON, listen, use, etc.

The implementation principle of listen is actually to encapsulate http.createServer. The focus is the callback passed in this function, which includes the merging of middleware, the processing of context and the special processing of res.

Use is to collect middleware, put multiple middleware into a cache queue, and then recursively combine and call these columns of middleware through koa-compose plug-in.

context.js

This part is koa’s application context ctx. In fact, it is a simple object exposure. The focus is on delegate. This is the agent. This is designed for the convenience of developers. For example, we want to access CTX. Repponse. Status. However, we can directly access ctx.status through delegate.

request.js、response.js

These two parts are some operations on the native res and req, a large number of uses of some syntax of get and set of es6, fetching headers or setting headers, and setting body, etc. These are not introduced in detail, and interested readers can read the source code by themselves.

Four modules to realize koa2

The above outlines the general framework of koa2 source code. Next, we will implement a koa2 framework. The author believes that understanding and implementing a koa framework requires the implementation of four major modules, namely:

  • Encapsulate nodetserver, create Koa class constructor
  • Construct request, response, context objects
  • Middleware Mechanism and Implementation of Onion Peeling Model
  • Error capture and error handling
  • Let’s analyze and implement them one by one.

Module 1: Encapsulate node http server and Create Koa Class Constructor

Reading the source code of koa2, we know that the implementation of koa’s server application and port monitor is actually based on the native code of node, and the code in the following figure is the server monitoring implemented by the native code of node.

let http = require('http');
let server = http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world');
});
server.listen(3000, () => {
console.log('listenning on 3000');
});

We need to encapsulate the above node native code into koa mode:

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

The first step to implement koa is to encapsulate the above process, so we need to create Application.js to implement a constructor of application class

let http = require('http');
class Application {
constructor() {
this.callbackFunc;
}
listen(port) {
let server = http.createServer(this.callback());
server.listen(port);
}
use(fn) {
this.callbackFunc = fn;
}
callback() {
return (req, res) => {
this.callbackFunc(req, res);
};
}
}
module.exports = Application;

Then create example.js, introduce application.js, and run the server instance to start the listening code:

let Koa = require('./application');
 let app = new Koa();
 app.use((req, res) => {
 res.writeHead(200);
 res.end('hello world');
 });
 app.listen(3000, () => {
 console.log('listening on 3000');
 });

Now enter localhost:3000 in the browser to see “hello world” displayed in the browser. Now we have completed the first step. We have simply encapsulated http server and created a class that can generate koa instances. In this class, app.use is also used to register middleware and callback functions, app.listen is used to open server instances and pass in callback functions. The first module is mainly to realize typical koa style and build a simple framework for koa. Next we will begin to write and explain the second module.

Module 2: Construct request, response, context Objects

According to the source code of koa2, the three files of context.js, request.js and response.js are the code files of request, response and context respectively. Context is ctx when we write koa code at ordinary times. It is equivalent to a global koa instance context this. It connects two functional modules, request and response, and is exposed to the parameters of koa instance and callback functions such as middleware, thus serving as a link between the preceding and the following.

The request and response function modules encapsulate the node’s native request and response respectively, using getter and setter attributes, and node-based object req/res object encapsulates koa’s request/response object. Based on this principle, we simply implement request.js and response.js, first create the request.js file, and then write the following code:

let url = require('url');
module.exports = {
get query() {
return url.parse(this.req.url, true).query;
}
};

This way, when you use ctx.query in the koa instance, you will return the value of URL. parse (this. req. URL, true). According to the source code, based on getter and setter, header, url, origin, path and other methods are also encapsulated in request.js, which are all encapsulated on the original request with getter and setter. the author will not implement them here.

Next, we implement the response.js file code module, which, like the request principle, is also based on getter and setter to encapsulate the native response. then we will briefly describe the response.js file if we implement the koa response module by taking the commonly used ctx.body and ctx.status statements as examples, and then enter the following code:

module.exports = {
 get body() {
 return this._body;
 },
 set body(data) {
 this._body = data;
 },
 get status() {
 return this.res.statusCode;
 },
 set status(statusCode) {
 if (typeof statusCode !  == 'number') {
 throw new Error('something wrong!'  );
 }
 this.res.statusCode = statusCode;
 }
 };

The above code realizes the reading and setting of koa’s status. When reading, it returns the statusCode attribute based on the native response object, while the reading of body reads and writes this._body. The native this.res.end is not used for body operation here, because when we write koa code, body will be read and modified many times, so the real operation of returning browser information is to package and operate in application.js

Now we have implemented request.js, response.js, obtained the request, response object and their encapsulation methods, and then we started to implement context.js the role of context is to mount the request and response object on top of ctx, so that koa instances and codes can be easily used in the request and response objects. Now let’s create a context.js file and enter the following code:

let proto = {};

function delegateSet(property, name) {
proto.__defineSetter__(name, function (val) {
this[property][name] = val;
});
}

function delegateGet(property, name) {
proto.__defineGetter__(name, function () {
return this[property][name];
});
}

let requestSet = [];
let requestGet = ['query'];

let responseSet = ['body', 'status'];
let responseGet = responseSet;

requestSet.forEach(ele => {
delegateSet('request', ele);
});

requestGet.forEach(ele => {
delegateGet('request', ele);
});

responseSet.forEach(ele => {
delegateSet('response', ele);
});

responseGet.forEach(ele => {
delegateGet('response', ele);
});

module.exports = proto;

The context.js file mainly mounts and proxies the common request and response methods, and directly proxies the context.request.query through context.query, and the context.body and context.status proxy the context.response.body and context.response.status However, context.request, context.response will be mounted in application.js

You could have used simple setters and getters to set each method, but since the context object definition method is relatively simple and standard, it can be seen in the koa source code that the koa source code uses __defineSetter__ and __defineSetter__ to replace the reading setting of each property of setter/getter. In this way, it is mainly convenient to expand and simplify the writing method. when we need to proxy more res and req methods, we can add the corresponding method name and attribute name to the array object in the context.js file.

So far, we have obtained three module objects: request, response and context. next, we will mount all the methods of request and response under the context, let the context realize its connecting function, modify the application.js file and add the following code:

let http = require('http');
 let context = require('./context');
 let request = require('./request');
 let response = require('./response');
 
 createContext(req, res) {
 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;
 return ctx;
 }

As you can see, we have added the createContext method, which is the key. It creates ctx through Object.create, mounts req and response on ctx, mounts the native REQ and res on the child attributes of ctx, and looks back at the context/REQ/RESPONSE.js file. We can know where the this.res or this.response and the like used at that time came from. It was originally mounted on the corresponding instance in this createContext method. After the runtime context ctx is constructed, our app.use callback function parameters are all based on ctx.

Module 3: Implementation of Middleware Mechanism and Onion Peeling Model

So far, we have successfully implemented the context object, request object and response object modules. The most important module is koa’s middleware module. koa’s middleware mechanism is an onion peeling model. Multiple middleware are put into an array queue by use and then executed from the outer layer. When they meet next, they enter the next middleware in the queue. After all middleware are executed, they begin to frame back and execute the unexecuted code part of the middleware in the queue. This is the onion peeling model, koa’s middleware mechanism.

Koa’s onion peeling model is implemented in koa1 by generator+co.js, koa2 by async/away+promise, and then we implement the middleware mechanism in koa2 based on async/away+promise. First, suppose koa’s middleware mechanism is ready, then it can successfully run the following code:

let Koa = require('../src/application');

let app = new Koa();

app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});

app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});

app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});

app.listen(3000, () => {
console.log('listenning on 3000');
});

After successful operation, 123456 will be output at the terminal, which can verify that our koa onion peeling model is correct. Next we will begin to implement, modify the application.js file and add the following code:

compose() {
return async ctx => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
let len = this.middlewares.length;
let next = async () => {
return Promise.resolve();
};
for (let i = len - 1;   i >= 0;  i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}

callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let onerror = (err) => this.onerror(err, ctx);
let fn = this.compose();
return fn(ctx);
};
}

Koa pushes all middleware into an internal array queue this.middlewares through the use function. The onion peeling model enables all middleware to execute in turn. After executing one middleware at a time, it will transfer control to the next middleware when encountering next (). The next parameter of the next middleware, the key code of the onion peeling model is the compose function:

compose() {
return async ctx => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
let len = this.middlewares.length;
let next = async () => {
return Promise.resolve();
};
for (let i = len - 1;   i >= 0;  i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}

The createNext function is used to pass the next of the previous middleware to the next middleware as a parameter, and bind the context ctx to the current middleware. When the middleware is finished executing, calling next () is actually to execute the next middleware.

for (let i = len - 1;   i >= 0;  i--) {
 let currentMiddleware = this.middlewares[i];
 next = createNext(currentMiddleware, next);
 }

The above code is actually the implementation of a chain reverse recursion model. I start the loop from the maximum number, encapsulate the middleware from the last one, and encapsulate its execution function into next every time as the next parameter of the previous middleware. Thus, when looping to the first middleware, only one next () need to be executed, and all middleware can be recursively called in chain. This is koa’s core code mechanism for peeling onions.

Here we summarize all the processes of peeling onion model code above. The middleware passed in through use is a callback function whose parameters are ctx context and next. next is actually the transfer bar of control right. next’s function is to stop running the current middleware, hand over control right to the next middleware, and execute the code before NEXT () of the next middleware. When the code run by the next middleware encounters NEXT (), it will give the code execution right to the next middleware. When the code is executed to the last middleware, the control right will be reversed, and it will start to go back to execute the unexecuted code left in all the middleware before. This whole process is a bit like a pseudo recursion. When all the middleware are finished, it will return a Promise object. Because our compose function returns an async function, the async function will return a Promise after execution, so that we can synchronize the asynchronous execution of all middleware, and then we can execute the response function and error handling function.

After the middleware mechanism code has been written, running our above example can already output 123456. So far, our koa’s basic framework has been basically completed. However, a framework cannot only implement functions. In order to make the framework and server instances robust, an error handling mechanism needs to be added.

Module 4: Error Capture and Error Handling

To achieve a basic framework, error handling and capture are essential. A robust framework must ensure that when errors occur, errors and thrown exceptions can be captured and fed back, and error information can be sent to the monitoring system for feedback. At present, the simple koa framework we have implemented has not yet been able to achieve this. We then add error handling and capture mechanisms.

throw new Error('oooops');

Based on the current framework, if the above error exception is thrown in the middleware code, no error can be caught. At this time, let’s look at the return return return code of the callback function in application.js, as follows:

return fn(ctx).then(respond);

As can be seen, fn is the execution function of middleware, and every middleware code is wrapped by async, and the execution function compose of middleware also returns an async function. According to the es7 specification, async returns an object instance of promise. If we want to capture the error of promise, we only need to use promise’s catch method to capture all middleware exceptions. The modified callback return code is as follows:

return fn(ctx).then(respond).catch(onerror);

Now we have realized the error exception capture of middleware, but we still lack the capture mechanism for errors in the framework layer. We hope that our server instance can have a monitoring mechanism for error events. We can subscribe to and monitor errors in the framework layer through on’s monitoring function. It is not difficult to realize this mechanism, just use nodejs native Events module. The events module provides us with an event monitor on function and an event trigger emit behavior function, one for transmitting events and one for receiving events. All we need to do is inherit koa’s constructor from the Events module. The constructed pseudo code is as follows:

let EventEmitter = require('events');
 class Application extends EventEmitter {}

After inheriting the events module, when we created the koa instance, we added the on listening function and the code was as follows:

let app = new Koa();
 
 app.on('error', err => {
 console.log('error happends: ', err.stack);
 });

In this way, we have realized the error capture and monitoring mechanism at the framework level. To sum up, error handling and capture are divided into middleware error handling capture and frame level error handling capture, middleware error handling uses promise catch, and frame level error handling uses nodejs native module events, so that we can capture all error exceptions on a server instance. At this point, we have completely realized a lightweight koa framework.

ending

So far, we have implemented a lightweight version of the koa framework. We have implemented four major modules: encapsulating node http server, creating Koa class constructor, constructing request, response, context object, middleware mechanism and onion peeling model, error capture and error handling. We have understood the implementation principle of this lightweight version of koa, and then look at the source code of koa2. You will find that everything is suddenly enlightened. the source code of koa2 is nothing more than adding a lot of tool functions and detailed processing on the basis of this lightweight version. the author will not introduce them one by one due to space limitation.

The complete code gitlab address of the lightweight koa implemented in this article is:article_koa2 

Author: No.1 Tadpole

Github: The article will be shared in the first placeThe mental process of the front-end diaosiWelcome star or watch, thank you

Finally,TNFE teamFor front-end developers, the latest high-quality content in small programs and web front-end technologies has been compiled. It is updated weekly. Welcome to star, github address:https://github.com/Tnfe/TNFE- …

图片描述