这几天我一直在思索,关于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整体的设计模式还是很值得学习的。