Home > Article > Web Front-end > What is the KOA framework? Detailed explanation of the four major modules for implementing koa2
koa is a new web framework based on node. This article will introduce you to the koa2 framework and help you understand and implement four major modules required to implement a koa framework.
koa is a new web framework based on node. It was created by the original team of express framework. It is characterized by elegance, simplicity, strong expressiveness 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 pattern is very consistent with the Unix philosophy. [Recommended learning: "nodejs Tutorial"]
koa framework has now been updated to version 2.x. This article starts from scratch and explains the framework source code structure and implementation principles of koa2 step by step, showing and Detailed explanation of several of the most important concepts in the koa2 framework source code, and then teach you step by step how to implement a simple koa2 framework to help you learn and understand koa2 on a deeper level. After reading this article, check it against the koa2 source code. I believe Your thoughts will be very smooth.
The framework used in this article is koa2, which is different from koa1. koa1 uses the execution method of generator co.js, while koa2 uses async/await, so the code and demo of this article need to run on node 8. version and above. If the reader's node version is lower, it is recommended to upgrade or install babel-cli, and use babel-node to run the code involved in this article.
The above picture is the lib folder of the source code directory structure of koa2, lib There are four core files of koa2 under the folder: application.js, context.js, request.js, and response.js.
application.js is the entry file of koa. It exports the constructor that creates class instances. It inherits events, so that Give the framework the ability to listen to events and trigger events. The application also exposes some commonly used APIs, such as toJSON, listen, use, etc.
The implementation principle of listen is actually an encapsulation of http.createServer. The focus is on the callback passed in this function, which includes the merging of middleware, context processing, and special processing of res.
Use is to collect middleware, put multiple middleware into a cache queue, and then use the koa-compose plug-in to recursively combine and call these middleware series.
This part is the application context ctx of koa. In fact, it is a simple object exposure. The focus is on delegate. This is the proxy. This is It is designed for the convenience of developers. For example, if we want to access ctx.repsponse.status, we can access it directly by accessing ctx.status through delegate.
These two parts are some operations on the native res and req, and use a lot of the get and set syntax of es6 , to get headers or set headers, and set body, etc. These will not be introduced in detail. Interested readers can read the source code by themselves.
The above briefly describes the general framework structure of the koa2 source code. Next, let’s implement a koa2 Framework, the author believes that understanding and implementing a koa framework requires the implementation of four major modules, which are:
Encapsulating node http server and creating Koa class constructor
Constructing request, response, and context objects
Implementation of middleware mechanism and onion peeling model
Error capturing and error handling
Let’s analyze and implement them one by one.
Module 1: Encapsulating the node http server and creating the Koa class constructor
Reading the source code of koa2, we learned that the server application and port monitoring of koa are actually based on The native code of node is encapsulated. The code in the figure below is the server monitoring implemented through 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, for which we need to create an application .js implements the constructor of an 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'); });
现在在浏览器输入localhost:3000即可看到浏览器里显示“hello world”。现在第一步我们已经完成了,对http server进行了简单的封装和创建了一个可以生成koa实例的类class,这个类里还实现了app.use用来注册中间件和注册回调函数,app.listen用来开启服务器实例并传入callback回调函数,第一模块主要是实现典型的koa风格和搭好了一个koa的简单的架子。接下来我们开始编写和讲解第二模块。
模块二:构造request、response、context对象
阅读koa2的源码得知,其中context.js、request.js、response.js三个文件分别是request、response、context三个模块的代码文件。context就是我们平时写koa代码时的ctx,它相当于一个全局的koa实例上下文this,它连接了request、response两个功能模块,并且暴露给koa的实例和中间件等回调函数的参数中,起到承上启下的作用。
request、response两个功能模块分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象。我们基于这个原理简单实现一下request.js、response.js,首先创建request.js文件,然后写入以下代码:
let url = require('url'); module.exports = { get query() { return url.parse(this.req.url, true).query; } };
这样当你在koa实例里使用ctx.query的时候,就会返回url.parse(this.req.url, true).query的值。看源码可知,基于getter和setter,在request.js里还封装了header、url、origin、path等方法,都是对原生的request上用getter和setter进行了封装,笔者不再这里一一实现。
接下来我们实现response.js文件代码模块,它和request原理一样,也是基于getter和setter对原生response进行了封装,那我们接下来通过对常用的ctx.body和ctx.status这个两个语句当做例子简述一下如果实现koa的response的模块,我们首先创建好response.js文件,然后输入下面的代码:
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; } };
以上代码实现了对koa的status的读取和设置,读取的时候返回的是基于原生的response对象的statusCode属性,而body的读取则是对this._body进行读写和操作。这里对body进行操作并没有使用原生的this.res.end,因为在我们编写koa代码的时候,会对body进行多次的读取和修改,所以真正返回浏览器信息的操作是在application.js里进行封装和操作。
现在我们已经实现了request.js、response.js,获取到了request、response对象和他们的封装的方法,然后我们开始实现context.js,context的作用就是将request、response对象挂载到ctx的上面,让koa实例和代码能方便的使用到request、response对象中的方法。现在我们创建context.js文件,输入如下代码:
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;
context.js文件主要是对常用的request和response方法进行挂载和代理,通过context.query直接代理了context.request.query,context.body和context.status代理了context.response.body与context.response.status。而context.request,context.response则会在application.js中挂载
本来可以用简单的setter和getter去设置每一个方法,但是由于context对象定义方法比较简单和规范,在koa源码里可以看到,koa源码用的是__defineSetter__和__defineSetter__来代替setter/getter每一个属性的读取设置,这样做主要是方便拓展和精简了写法,当我们需要代理更多的res和req的方法的时候,可以向context.js文件里面的数组对象里面添加对应的方法名和属性名即可。
目前为止,我们已经得到了request、response、context三个模块对象了,接下来就是将request、response所有方法挂载到context下,让context实现它的承上启下的作用,修改application.js文件,添加如下代码:
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; }
可以看到,我们添加了createContext这个方法,这个方法是关键,它通过Object.create创建了ctx,并将request和response挂载到了ctx上面,将原生的req和res挂载到了ctx的子属性上,往回看一下context/request/response.js文件,就能知道当时使用的this.res或者this.response之类的是从哪里来的了,原来是在这个createContext方法中挂载到了对应的实例上,构建了运行时上下文ctx之后,我们的app.use回调函数参数就都基于ctx了。
模块三:中间件机制和剥洋葱模型的实现
目前为止我们已经成功实现了上下文context对象、 请求request对象和响应response对象模块,还差一个最重要的模块,就是koa的中间件模块,koa的中间件机制是一个剥洋葱式的模型,多个中间件通过use放进一个数组队列然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分,这就是剥洋葱模型,koa的中间件机制。
koa的剥洋葱模型在koa1中使用的是generator + co.js去实现的,koa2则使用了async/await + Promise去实现的,接下来我们基于async/await + Promise去实现koa2中的中间件机制。首先,假设当koa的中间件机制已经做好了,那么它是能成功运行下面代码的:
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'); });
运行成功后会在终端输出123456,那就能验证我们的koa的剥洋葱模型是正确的。接下来我们开始实现,修改application.js文件,添加如下代码:
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通过use函数,把所有的中间件push到一个内部数组队列this.middlewares中,剥洋葱模型能让所有的中间件依次执行,每次执行完一个中间件,遇到next()就会将控制权传递到下一个中间件,下一个中间件的next参数,剥洋葱模型的最关键代码是compose这个函数:
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(); }; }
createNext函数的作用就是将上一个中间件的next当做参数传给下一个中间件,并且将上下文ctx绑定当前中间件,当中间件执行完,调用next()的时候,其实就是去执行下一个中间件。
for (let i = len - 1; i >= 0; i--) { let currentMiddleware = this.middlewares[i]; next = createNext(currentMiddleware, next); }
上面这段代码其实就是一个链式反向递归模型的实现,i是从最大数开始循环的,将中间件从最后一个开始封装,每一次都是将自己的执行函数封装成next当做上一个中间件的next参数,这样当循环到第一个中间件的时候,只需要执行一次next(),就能链式的递归调用所有中间件,这个就是koa剥洋葱的核心代码机制。
到这里我们总结一下上面所有剥洋葱模型代码的流程,通过use传进来的中间件是一个回调函数,回调函数的参数是ctx上下文和next,next其实就是控制权的交接棒,next的作用是停止运行当前中间件,将控制权交给下一个中间件,执行下一个中间件的next()之前的代码,当下一个中间件运行的代码遇到了next(),又会将代码执行权交给下下个中间件,当执行到最后一个中间件的时候,控制权发生反转,开始回头去执行之前所有中间件中剩下未执行的代码,这整个流程有点像一个伪递归,当最终所有中间件全部执行完后,会返回一个Promise对象,因为我们的compose函数返回的是一个async的函数,async函数执行完后会返回一个Promise,这样我们就能将所有的中间件异步执行同步化,通过then就可以执行响应函数和错误处理函数。
当中间件机制代码写好了以后,运行我们的上面的例子,已经能输出123456了,至此,我们的koa的基本框架已经基本做好了,不过一个框架不能只实现功能,为了框架和服务器实例的健壮,还需要加上错误处理机制。
要实现一个基础框架,错误处理和捕获必不可少,一个健壮的框架,必须保证在发生错误的时候,能够捕获到错误和抛出的异常,并反馈出来,将错误信息发送到监控系统上进行反馈,目前我们实现的简易koa框架还没有能实现这一点,我们接下加上错误处理和捕获的机制。
throw new Error('oooops');
基于现在的框架,如果中间件代码中出现如上错误异常抛出,是捕获不到错误的,这时候我们看一下application.js中的callback函数的return返回代码,如下:
return fn(ctx).then(respond);
可以看到,fn是中间件的执行函数,每一个中间件代码都是由async包裹着的,而且中间件的执行函数compose返回的也是一个async函数,我们根据es7的规范知道,async返回的是一个promise的对象实例,我们如果想要捕获promise的错误,只需要使用promise的catch方法,就可以把所有的中间件的异常全部捕获到,修改后callback的返回代码如下:
return fn(ctx).then(respond).catch(onerror);
现在我们已经实现了中间件的错误异常捕获,但是我们还缺少框架层发生错误的捕获机制,我们希望我们的服务器实例能有错误事件的监听机制,通过on的监听函数就能订阅和监听框架层面上的错误,实现这个机制不难,使用nodejs原生events模块即可,events模块给我们提供了事件监听on函数和事件触发emit行为函数,一个发射事件,一个负责接收事件,我们只需要将koa的构造函数继承events模块即可,构造后的伪代码如下:
let EventEmitter = require('events');class Application extends EventEmitter {}
继承了events模块后,当我们创建koa实例的时候,加上on监听函数,代码如下:
let app = new Koa(); app.on('error', err => { console.log('error happends: ', err.stack); });
这样我们就实现了框架层面上的错误的捕获和监听机制了。总结一下,错误处理和捕获,分中间件的错误处理捕获和框架层的错误处理捕获,中间件的错误处理用promise的catch,框架层面的错误处理用nodejs的原生模块events,这样我们就可以把一个服务器实例上的所有的错误异常全部捕获到了。至此,我们就完整实现了一个轻量版的koa框架了。
前为止,我们已经实现了一个轻量版的koa框架了,我们实现了封装node http server、创建Koa类构造函数、构造request、response、context对象、中间件机制和剥洋葱模型的实现、错误捕获和错误处理这四个大模块,理解了这个轻量版koa的实现原理,再去看koa2的源码,你就会发现一切都豁然开朗,koa2的源码无非就是在这个轻量版基础上加了很多工具函数和细节的处理,限于篇幅笔者就不再一一介绍了。
更多编程相关知识,请访问:编程视频!!
The above is the detailed content of What is the KOA framework? Detailed explanation of the four major modules for implementing koa2. For more information, please follow other related articles on the PHP Chinese website!