Koa源码分析
Koa 是一个类似于 Express 的Web开发框架,创始人也都是TJ。Koa 的主要特点是,使用了 ES6 的 Generator 函数,进行了架构的重新设计。Koa 的原理和内部结构很像 Express,但是语法和内部结构进行了升级。
创建Koa应用
创建一个 koa 非常简单:
var koa = require(‘koa’);
var app = koa();
app.listen(3000); 或者可以酱紫:
var koa = require(‘koa’); var http = require(‘http’);
var app = koa();
http.createServer(app.callback()).listen(4000); 这两种方式在 koa 内部是等价的,在 Application 模块中, listen 就会调用自身的 callback:
//listen的实现 app.listen = function(){ debug(‘listen’); var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); }; callback 返回的函数会作为 server 的回调:
app.callback = function(){
/**
- 省略的代码 **/ return function(req, res){ res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror); fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror); } }; callback 也会将多个中间件转成了一个 fn,在构建服务器函数时方便调用。状态码默认是 404,即没有任何中间件修改过就是 404。
每个请求都会通过 createContext 创建一个上下文对象,其参数则分别是 Node 的 request 对象和 response 对象:
app.createContext = function(req, res){ var context = Object.create(this.context); var request = context.request = Object.create(this.request); var response = context.response = Object.create(this.response); 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; request.response = response; response.request = request; context.onerror = context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); context.accept = request.accept = accepts(req); context.state = {}; return context; }; 对于接收的参数,在返回上下文 context 之前,koa 会将参数注入到自身的 request 对象和 response 对象上,ctx.request 和 ctx.response 返回的是 koa 的对应对象,ctx.req 和 ctx.res 返回的是 Node 的对应对象;同时也会将 app 注册到 context/respose/request 对象上,方便在各自的模块中调用:
var app = Application.prototype;
module.exports = Application;
function Application() { if (!(this instanceof Application)) return new Application; this.env = process.env.NODE_ENV || ‘development’; //环境变量 this.subdomainOffset = 2; //子域偏移量 this.middleware = []; //中间件数组 this.proxy = false; //是否信任头字段 proxy this.context = Object.create(context); // koa的上下文(this) this.request = Object.create(request); //koa的request对象 this.response = Object.create(response); //koa 的reponse对象 } 上下文:context
context 对象是 Koa context 模块扩展出来的,添加了诸如 state、cookie、req、res 等属性。
onFinished 是一个第三方函数,用于监听 http response 的结束事件,执行回调。如果找到 context.onerror 方法,这是 koa默认的错误处理函数,它处理的是错误导致的异常结束。错误的处理是在 callback 中监听的:
// callback if (!this.listeners(‘error’).length) this.on(‘error’, this.onerror); koa 本身是没有定义事件处理机制的,其事件处理机制继承自 Node 的 events:
var Emitter = require(‘events’).EventEmitter; Object.setPrototypeOf(Application.prototype, Emitter.prototype); 默认的错误分发是在 Context 模块中:
onerror : function(err){ //some code this.app.emit(‘error’, err, this); //some code } 此外,在 Context 模块中,还将 request 对象和 response 对象的一些方法和属性委托给了 context 对象:
//response委托 delegate(proto, ‘response’) .method(‘attachment’) .method(‘append’) .access(‘status’) .access(‘body’) .getter(‘headerSent’) .getter(‘writable’); …..
//request委托 delegate(proto, ‘request’) .method(‘acceptsLanguages’) .method(‘get’) .method(‘is’) .access(‘querystring’) .access(‘url’) .getter(‘origin’) .getter(‘href’) .getter(‘subdomains’) .getter(‘protocol’) .getter(‘host’) …. 通过第三方模块 delegate 将 koa 在 Response 模块和 Request 模块中定义的方法委托到了 context 对象上,所以以下的一些写法是等价的:
//在每次请求中,this 用于指代此次请求创建的上下文 context(ctx) this.body ==> this.response.body this.status ==> this.response.status this.href ==> this.request.href this.host ==> this.request.host ….. 在 createContext 方法中,还给 context 定义了重要属性 state
context.state = {} 这个属性可以被各个中间件共享,用于在中间件之间传递数据,这也是 koa 推荐的方式:
this.state.user = yield User.find(id); 中间件
中间件是对 HTTP 请求进行处理的函数,对于每一个请求,都会通过中间件进行处理。在 koa 中,中间件通过 use 进行注册,且必须是一个 Generator 函数(未开启 this.experimental):
app.use(function* f1(next) { console.log(‘f1: pre next’); yield next; this.body = ‘hello koa’; console.log(‘f1: post next’); });
app.use(function* f2(next) { console.log(‘ f2: pre next’); console.log(‘ f2: post next’); }); 输出如下:
f1: pre next f2: pre next f2: post next f1: post next 与 Express 的中间件顺序执行不同,在koa中,中间件是所谓的“洋葱模型”或级联式(Cascading)的结构,也就是说,属于是层层调用,第一个中间件调用第二个中间件,第二个调用第三个,以此类推。上游的中间件必须等到下游的中间件返回结果,才会继续执行。
koa 对中间件的数量并没有限制,可以随意注册多个中间件。但如果有多个中间件,只要有一个中间件缺少 yield next 语句,后面的中间件都不会执行:
app.use(function *(next){ console.log(‘>> one’); yield next; console.log(‘<< one’); });
app.use(function *(next){ console.log(‘>> two’); this.body = ‘two’; console.log(‘<< two’); });
app.use(function *(next){ console.log(‘>> three’); yield next; console.log(‘<< three’); }); 上面代码中,因为第二个中间件少了yield next语句,第三个中间件并不会执行。
如果想跳过一个中间件,可以直接在该中间件的第一行语句写上return yield next:
app.use(function* (next) { if (skip) return yield next; }) koa中,中间件唯一的参数就是 next。如果要传入其他参数,必须另外写一个返回 Generator 函数的函数。
this.experimental 是为了判断是否支持es7,开启这个属性之后,中间件可以传入async函数:
app.use(async function (next){ await next; this.body = body; }); 但 koa 默认是不支持 es7 的,如果想支持,需要在代码中明确指定 this.experimental = true
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that fn
is a generator function
assert(fn && ‘GeneratorFunction’ == fn.constructor.name, ‘app.use() requires a generator function’);
}
debug(‘use %s’, fn._name || fn.name || ‘-‘);
this.middleware.push(fn);
return this;
};
在 callback 中输出错误信息:
app.callback = function(){ if (this.experimental) { console.error(‘Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.’) } var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); //省略 }; compose 的全名叫 koa-compose,它的作用是把一个个不相干的中间件串联在一起:
// 有3个中间件 this.middlewares = [function m1() {}, function m2() {}, function *m3() {}];
// 通过compose转换 var middleware = compose(this.middlewares);
// 转换后得到的middleware是这个样子的 function () { yield m1(m2(m3(noop()))) } 从上述的 use 的实现可知,由于 use 的每次调用均会返回 this,因而可以进行链式调用:
app.use(function m1() {}).use(function m2() {}).use(function *m3() {}) 路由处理
koa自身有 request 对象和 response 对象来处理路由,一个简单的路由处理如下:
app.use(function* () { if(this.path == ‘/‘){ this.body = ‘hello koa’; } else if(this.path == ‘/get’){ this.body = ‘get’; } else { this.body = ‘404’; } }); 也可以通过 this.request.headers 来获取请求头。由于没有对响应头做设置,默认响应头类型是 text/plain,可以通过 response.set来设置:
app.use(function* (next) { if(this.path == ‘/‘){ this.body = ‘hello koa’; } else if(this.path == ‘/get’){ this.body = ‘get’; } else { yield next; } });
app.use(function* () { this.response.set(‘content-type’, ‘application/json;charset=utf-8’); return this.body = {message: ‘ok’, statusCode: 200}; }); 上面代码中,每一个中间件负责部分路径,如果路径不符合,就传递给下一个中间件。
复杂的路由需要安装 koa-router:
var app = require(‘koa’)(); var Router = require(‘koa-router’);
var myRouter = new Router();
myRouter.get(‘/‘, function *(next) { this.response.body = ‘Hello World!’; });
app.use(myRouter.routes());
app.listen(4000); 由于 koa 使用 generator 作为中间件,所以 myRouter.routes() 返回的是一个 generator,并等同于 myRouter.middleware:
Router.prototype.routes = Router.prototype.middleware = function () { var router = this;
var dispatch = function *dispatch(next) { //code } //省略 return dispatch; }; koa-router 提供了一系列于 HTTP 动词对应的方法:
router.get() router.post() router.put() router.del() router.patch() del 是 delete 的别名:
// Alias for router.delete()
because delete is a reserved word
Router.prototype.del = Router.prototype[‘delete’];
这些动词方法可以接受两个参数,第一个是路径模式,第二个是对应的控制器方法(中间件),定义用户请求该路径时服务器行为。
注意,路径匹配的时候,不会把查询字符串考虑在内。比如,/index?param=xyz 匹配路径 /index。
关于 koa-router 的更多细节,且听下回分解。
- 数据分析告诉你:Php最不安全,Nginx比Apache安全
- 用python抓取摩拜单车API数据并做可视化分析(源码)
- 用Pandas在Python中可视化机器学习数据
- Intent 属性详解(下)
- 把复杂json解析成javabean思路:思路:
- Python数据科学计算库的安装和numpy简单
- 4G安全:研究人员发现攻击4G无线上网卡和SIM卡的方法
- Python文学化编程 - Jupyter notebook使用和插件拓展
- PoisonCake(毒蛋糕):内置于手机ROM的恶意代码模块
- Spring+SpringMVC+MyBatis整合
- Python之numpy数组学习(五)——广播
- WordPress再悲剧:WPcache-Blogger感染事件影响五万WordPress网站
- 浅谈神经机器翻译
- 窃取Facebook用户信息:利用Android同源策略漏洞的恶意应用被发现
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Go语言 | CSP并发模型与Goroutine的基本使用
- LeetCode链表知识点&题型总结
- dp 类找零钱类问题
- 网易校招真题三
- 嵌入式linux之go语言开发(四)go语言8583协议报文解析
- java 线程池设计模式
- 初识分布式:MIT 6.284系列(一)
- LeetCode 93 | 生成所有有效的IP地址
- 嵌入式linux之go语言开发(二)c动态库驱动调用
- 华量杯-股票预测, keras+LSTM
- Apollo配置中心源码编译及搭建
- 嵌入式linux之go语言开发(三)卡库的封装
- 可笑,你竟然不知道 Java 如何生成 UUID
- 招商银行校招题一
- 面试:mysql最全索引与优化详解