前言
这一篇应该就是这个系列的最后一篇了。之后的文章里我会分享node的其他内容,作为node的入门文章来说我觉得这几个足以了。不放前置文章了,大家想看的自己往前翻就是了。
koa
前面写了那么些,我们需要明确的一点是:koa这个框架到底做了些什么事情?
为了说明这个问题,让我们再次比较一下,原始node和koa是怎么写http的
// node
var http = require('http');
http.createServer((request, response)=> {response.end('Hello World\n');}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
//koa
const Koa = require('koa');
const app = new Koa();
app.use((context,next)=>{context.body='hello world';);
app.listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
我的理解是:1、他将中间件组织了起来以便程序对他们进行依次调用,2、他将请求(request)和响应(response)封装成了一个上下文(context),使context可以使用request和response的方法和属性。
分析
我们直接从源码的角度进行分析。
在一个目录下cmd输入 npm install --save koa ,就会下载koa的相关包,这时候查看node_modules中,koa的源代码只有四个:koa、koa-compose、koa-convert、koa-is-json
其中koa-is-json只有这么一点代码 忽略掉
function isJSON(body) {
if (!body) return false;
if ('string' == typeof body) return false;
if ('function' == typeof body.pipe) return false;
if (Buffer.isBuffer(body)) return false;
return true;
}
我们首先来看看koa是如何组织中间件的。我提前说一下结论,首先先在koa包中的app类中编写一个方法use()将中间件添加到数组中,然后使用koa-compose包的中的函数将该数组中的函数组织成中间件。
//koa包 lib/application.js
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3\. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x---deprecated');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
我们知道,在node中,函数是作为第一等公民的,所以函数是可以作为数组中的一个成员的。当我们编写了函数function func1(ctx,next){}并使用app.use(func1)的时候,实际上就是将func1这个函数放入了一个数组中。
接下来,你可以继续添加,当你一旦使用app.listen(port)对端口进行监听的时候,这时候koa就会使用koa-compose对数组中的函数进行组织。
// koa-compose包 index.js
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
returne返回的这个函数有些难以看懂,其实他就是用promise写一个处理逻辑,递归调用dispatch,按照middleware数组的顺序往下一层一层地调用next来执行中间件(回调函数);
可以用async来改写。
function compose(middleware) {
return dispatch(0);
async function dispatch(i) {
let fn = middleware[i];
try {
await fn(dispatch.bind(null, i + 1));
} catch (err) {
return err;
}
}
}
可以看出在调用逻辑上中间件和async调用没有什么本质上的差别。通过这种形式,我们就将顺序操作组织为层级操作。
再来看看koa对request和response的封装。
在node_modules中打开koa包,可以看到他有四个文件(application.js、context.js、request.js、response.js),主要来看看application的实现。
首先在new了一个Koa实例出来后,application(app)构造函数
// application.js
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
并没有做什么实质性的工作,只是根据另外三个文件创建了三个对象。
接下来,app.listen(端口号)。
执行了这一步,koa就会把服务器实例正式运行起来(包括刚才对中间件的组织),具体代码如下
// application.js
listen() {
const server = http.createServer(this.callback()); //这个http.createServer就是上文node的那种创建方式
return server.listen.apply(server, arguments); //这里是用js的语法更改一下this的指向和传入参数并执行
}
callback() {
const fn = compose(this.middleware); // 这个函数调用的就是上文所说的中间件组织
if (!this.listeners('error').length) this.on('error', this.onerror);
return (req, res) => { //返回一个参数为request和response的函数给http.createServer()
res.statusCode = 404;
const ctx = this.createContext(req, res); //将request和response封装成一个context
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror); // 依次执行中间件
};
}
createContext(req, res) {
const context = Object.create(this.context); //每次传入来一个请求,都会复制出来一个新的context对象、request和response对象,让他们拥有指向彼此的指针
const request = context.request = Object.create(this.request);
const 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.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
request.ip = request.ips[0] || req.socket.remoteAddress || '';
context.accept = request.accept = accepts(req);
context.state = {};
return context;
}
由于http模块的作用,每次server在收到一个有效request请求之后,会产生一个request对象和response对象(上一篇讲到的,不记得的可以回去看),这时候koa层面就会把这个request和response做一个合并的处理,让他们都在ctx(context)对象中进行操作。
从代码里面可以看出,ctx里保存着res(response)和req(request)对象的引用,而res对其他两个也是如此。其实我个人认为这样只是单纯有利于调试,对于代码的组织来说似乎没什么作用。我们在java中也习惯了httpRequest处理请求,httpResponse返回消息的操作。
此外值得注意的一点是koa包中context.js文件
// context.js
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
...
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
...
这里只需要知道他是使用了delegate委托的方法,将request和response中的方法代理到context中去,这就够了。