这几天我一直在思索,关于Koa
我可以分享什么?
分享Koa
中间件实现原理?
上一篇的《从源码上理解express中间件》已经解释的比较完善了,Koa
中间件实现也比较相似,只不过,是把中间件函数抽离出来为koa-compose
,循环遍历的时候将函数外边套了tryCatch
,里面加了Promise.resolve(fn)
而已。
因此,这篇文章主要分享下Koa的数据劫持。
如: 以下访问器和 Request 别名等效。
ctx.header
ctx.headers
ctx.method
ctx.method=
ctx.url
ctx.url=
ctx.originalUrl
ctx.origin
ctx.href
ctx.path
ctx.path=
ctx.query
ctx.query=
ctx.querystring
ctx.querystring=
ctx.host
ctx.hostname
ctx.fresh
ctx.stale
ctx.socket
ctx.protocol
ctx.secure
ctx.ip
ctx.ips
ctx.subdomains
ctx.is()
ctx.accepts()
ctx.acceptsEncodings()
ctx.acceptsCharsets()
ctx.acceptsLanguages()
ctx.get()
复制代码
上下文Context的创建
Koa
的源码主要分为四个部分:
application
。context
。request
。response
。
application
是继承自Node
核心模块events
,通过实例化Application
,得到Koa
。application
在constructor
的时候会通过Object.create()
创建一个新对象,带着指定的原型对象和属性。
点击这里查看MDN。
constructor() {
super();
this.proxy = false;
//中间件数组
this.middleware = [];
this.subdomainOffset = 2;
//设置环境变量,默认development
this.env = process.env.NODE_ENV || 'development';
//使用现有的对象来提供新创建的对象的__proto__,即this.context.__proto__ === context //true
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
复制代码
当用户执行app.listen
时,调用callback函数,中间件函数的执行和调用createContext()
。
//启动Koa服务器
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
//callback
callback() {
//处理Koa的中间件。
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
//创建context
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
//设置context的app、req、res、res、ctx
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.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
复制代码
上下文Context
Context
属性代理一些参数主要是通过delegates
模块实现的,这里主要是以讲述delegates
为主。
//创建context的原型
const proto = module.exports = {
...
}
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
复制代码
Koa
的context
主要应用了delegates
的三个方法,分别是method
、access
、getter
方法。
初始化Context
的时候,会在createContext
中将response
和request
赋值给ctx
,因此context
含有request
和response
的key
。
//设置context的app、req、res、res、ctx
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
原型proto
时候会调用delegator
,将response
和request
的key
传递进去,再依次链式调用method
,access
,getter
,将request
和response
中需要代理的属性依次传入。
如:当用户通过调用ctx.set()
时, 在此之前,在delegator
中调用了method
方法,已经将set
传递进去,proto[name]
可以理解为ctx['set']
,赋值给proto[name]
一个函数,由于是ctx
调用set
,所以当前函数this
的指向是ctx
。
/**
* 委托方法的名字
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.method = function(name){
// proto原型
var proto = this.proto;
//target 为delegate的第二个参数,这里是response | request
var target = this.target;
this.methods.push(name);
proto[name] = function(){
return this[target][name].apply(this[target], arguments);
};
return this;
};
复制代码
而这个函数实际上就是通过将ctx.response.set
通过apply
进行调用,然后return
出去的值。
/**
* Delegator accessor `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
/**
* Delegator getter `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
/**
* Delegator setter `name`.
*
* @param {String} name
* @return {Delegator} self
* @api public
*/
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
复制代码
而access
方法是setter
、getting
方法的连续调用,通过设置Object.__defineGetter__
和Object.__defineSetter__
来进行数据劫持的。
由于MDN并不推荐使用这种方法,因此这里使用Object.defineProperty()
重新写getter
和setter
方法。
//getter
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
Object.defineProperty(proto, name, {
get: function() {
return this[target][name];
}
});
return this;
};
//setter
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
Object.defineProperty(proto, name, {
set: function(val) {
return this[target][name] = val;
}
});
return this;
};
复制代码
最后
Koa的数据劫持主要是靠Object.__defineSetter__
和Object.__defineSetter__
的应用,不过说起来,Koa
整体的设计模式还是很值得学习的。