最近一直都在开发基于node的前后端项目,分享一下Koa的源码。
Koa自己的说法是next generation web framework for node.js
Koa算是比较主流的node的web框架了,前身是express。相比于express,koa去除了多余的middleware,只留下了最基本的对node的网络模块的继承和封装,并且提供了方便的中间件调用机制,Koa的源码总共加起来就1600+,很快就可以看完。
基础知识
在分析koa的源码之前需要先了解一下node的http模块。
const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader(‘Content-Type’, ‘text/plain’);
res.end(‘Hello World’);
}
server.listen(3000);
复制代码
node的http模块主要负责了node对HTTP处理的封装。以上这段代码启动了一个监听3000端口的web server,并且返回'Hello World'给接收到的请求。
每一次接收到一个新的请求的时候会调用回调函数,参数req和res分别是请求的实体和返回的实体,操作req可以获取收到的请求,操作res对应的是将要返回的packet。
如果你需要对接收到的请求进行一系列处理的话,则需要按顺序写在回调函数里面。
同样的功能对应的Koa的写法如下:
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
这里的user实际上是Koa的中间件机制提供的一个方便的处理请求的接口。请求会被use后面的函数依次按照use的顺序被处理,通常称这些函数为中间件,他们的参数ctx为koa基于node的http模块的req和res封装的一个对象,集合了req和res的功能为一体,并且增加了一些简单的操作。可以通过ctx.req和ctx.res获取到原生的req和res,与此同时ctx.request和ctx.response是Koa基于req和res封装的拥有一些新的功能的请求和返回的实体。 以下代码是Koa中间件使用的栗子:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(‘pre1’)
await next();
console.log(‘post1’);
});
app.use(async (ctx, next) => {
console.log(‘pre2’);
await next();
console.log(‘post2’)
});
app.use(async ctx => {
console.log(‘pre3’)
ctx.body = 'Hello World';
console.log(‘post3’);
});
app.listen(3000);
复制代码
next()表示将请求的处理交给下一个中间件。如果没有next(),在该中间件函数执行结束后,将返回执行上一个中间件的next()后续的内容直到最开始的中间件的next()后面的内容执行完毕。
上面的代码的结果是
pre1
pre2
pre3
post3
post2
post1
复制代码
执行的结果和函数递归调用何其相似,之后了解了Koa的中间件机制后自然会明白这个结果的原因。
正题
在了解了koa的基本的使用和带着以上中间件执行的结果,我们来看看koa的源码吧。
对Koa的理解主要分为两个部分:
- Koa对node的http模块的封装
- Koa的中间件机制
按照【栗子】代码的顺序从上到下:
初始化Koa
const app = new Koa();
复制代码
使用use加载中间件
对应的源码中
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/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
复制代码
将中间件函数push到middleware数组中。
调用listen方法启动web server
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
复制代码
回调函数:
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fn(ctx).then(handleResponse).catch(onerror);
};
return handleRequest;
}
复制代码
这里的compose函数是实现Koa的中间件机制的地方之后再细说。
Koa对node的http模块的封装
const ctx = this.createContext(req, res);
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 = 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;
}
复制代码
createContext实际上只是创建之前说的集合了req & res & request & response的一个对象,作为参数传递给中间件。 对于request和response其中除了一些拓展的方便的接口之外大部分都是直接继承的http的req和response。 实现的新的接口也是比较简单的封装,举个栗子(response.js):
set status(code) {
assert('number' == typeof code, 'status code must be a number');
assert(statuses[code], `invalid status code: ${code}`);
assert(!this.res.headersSent, 'headers have already been sent');
this._explicitStatus = true;
this.res.statusCode = code;
this.res.statusMessage = statuses[code];
if (this.body && statuses.empty[code]) this.body = null;
},
/**
* Get response status message
*
* @return {String}
* @api public
*/
get message() {
return this.res.statusMessage || statuses[this.status];
}
复制代码
以上是response中实现的两个新的接口,实际上也就是res的接口再简单封装了一下然后返回。 再看(context.js)的最底部
/**
* 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');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
复制代码
delegate的作用是将对应的对象上的method,getter,setter继承到另一个对象上。 可以看到,直接继承了大部分req和res的方法。
接下来就是看看Koa的中间件机制的实现了。
Koa的中间件机制
compose的源码也是非常简单的:
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
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) {
// last called middleware #
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
可以看到其实就是按照顺序将middleware数组中的中间件s按照顺序递归执行,每次执行next()的时候就是执行下一个中间件,最后一个next也就是第一个中间件的第二个参数,因为是undefined,所以会结束递归调用反向依次执行每个中间件next后续的代码。每次return的都是一个Promise对象,因此我们写的时候是await来等待这个异步调用的结束,然后执行下一个中间件。而我们一般的写法是将await next()写在中间件函数的最后,从而用尾递归的方式来实现每个请求依次被中间件函数处理的效果。
恩,Koa的主要的概念就是这些,它的目的就是一个极简的框架,只提供最基本的接口,大部分的功能,开发者根据需求使用use添加中间件来实现。