一个简单的示例
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'hello world'
})
app.listen(3000);
koa是对node请求响应进行封装, 从这个示例中我们不难猜出通过listen方法进行http.createServer
的调用。
koa的是基于中间件的思想, 在调用use的时候会将一系列的中间件进行存储,启用server的时候回调经过中间件包装的函数。
其主要的几个代码如下
new 实例
new的时候进行一些属性的设置, 包括context、request、response等。
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
// util.inspect.custom support for node 6+
/* istanbul ignore else */
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
use
进行中间件的挂载
use(fn) {
this.middleware.push(fn);
return this;
}
listen
调用http.createServer
时会对回调函数进行封装, 通过compose
将中间件包装成高阶函数, 最后在this.handleRequest
中进行调用。
respond
将返回的结果进行处理,最终返回至客户端。
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
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;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
...
res.end(body);
}
compose
compose返回的是个高阶函数, 在函数内设置dispatch
方法的返回值为promise对象, fn
的调用参数为ctx和 next。
通过 ctx获取上下文信息, 通过next的调用可触发下一个中间件(其过程为通常说的剥洋葱效果)。
function compose (middleware) {
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
koa-router
示例
const parentRouter = new Router();
const nestedRouter = new Router();
nestedRouter
.get(/^\/\w$/i, function (ctx, next) {
return next();
})
.get('/first-nested-route', function (ctx, next) {
return next();
})
.get('/second-nested-route', function (ctx, next) {
ctx.body = 'hello sub';
return next();
});
parentRouter.use('/parent-route', function (ctx, next) {
return next();
}, nestedRouter.routes());
app.use(parentRouter.routes());
app.listen(3000);
如果在不用router插件的话, 对于不同的URL走不同的分支,我们需要自己去做判断解决这些问题。
koa-router很好的帮我们解决了路由匹配的问题。
new 一个router
new 一个router的时候会对一些属性做初始化或者赋值操作。 其中params用来保存参数信息, stack则用来存储layer信息。
function Router(opts) {
if (!(this instanceof Router)) return new Router(opts);
this.opts = opts || {};
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];
this.params = {};
this.stack = [];
};
get过程
该方法主要是获取中间件, 然后调用register方法。
Router.prototype[method] = function(name, path, middleware) {
if (typeof path === "string" || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
this.register(path, [method], middleware, {
name: name
});
return this;
};
register
从该方法可以看出每次对path设置get、post等方法都会new一个Layer作为rout对象返回, 最终把route放入stack数组中。
layer用来存储单个路由信息,包括URL、 params、生成正则匹配regexp以及match方法。
从代码上还可以看出如果path为数组则对该数组进行递归的注册。
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
const router = this;
const stack = this.stack;
// support array of paths
if (Array.isArray(path)) {
for (let i = 0; i < path.length; i++) {
const curPath = path[i];
router.register.call(router, curPath, methods, middleware, opts);
}
return this;
}
// create route
const route = new Layer(path, methods, middleware, {...});
if (this.opts.prefix) {
route.setPrefix(this.opts.prefix);
}
// add parameter middleware
for (let i = 0; i < Object.keys(this.params).length; i++) {
const param = Object.keys(this.params)[i];
route.param(param, this.params[param]);
}
stack.push(route);
debug('defined route %s %s', route.methods, route.path);
return route;
};
中间件注册过程
在koa中通过use进行中间件的注册, 而use中注册的是一个包括ctx、next的方法。 从app.use(parentRouter.routes());
中我们不难推测调用routes方法即返回这样一个对象。
routes调用
通过该方法的调用返回一个带有ctx、next参数的方法。 dispatch的主要功能点:
- 获取当前的path, 通过router.match获取匹配后的信息。
- 获取所以匹配到的layer, 依次遍历将其内容封装成中间件,push到数组中。
- 通过compose方法将已封装的中间件数组进行粘合, 然后调用。 即实现路由功能。
Router.prototype.routes = Router.prototype.middleware = function () {
const router = this;
let dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);
const path = router.opts.routerPath || ctx.routerPath || ctx.path;
const matched = router.match(path, ctx.method);
let layerChain;
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
ctx.router = router;
if (!matched.route) return next();
const matchedLayers = matched.pathAndMethod
const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = ctx.request.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerPath = layer.path;
ctx.routerName = layer.name;
ctx._matchedRoute = layer.path;
if (layer.name) {
ctx._matchedRouteName = layer.name;
}
return next();
});
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};
router.use
在示例中我们还看到,在父路由上通过use方法可以注册子路由,从而实现多级路由功能。
接下来我们可以大致看一下use的过程。
在use中可以传递多个middleare参数。
- 当middleare为数组时,进行递归调用
- 当middleare上包含router属性时, 说明是通过
router.routes()
返回的,此时为一个子路由的注册。
针对这种情况,首先对子路由的信息进行copy, 对其layer信息进行copy,然后将copy后的layer信息push的父路由的stack中; - 如果是单个middleare 中间件方法时, 则直接执行注册。
Router.prototype.use = function () {
const router = this;
const middleware = Array.prototype.slice.call(arguments);
let path;
// support array of paths
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
let arrPaths = middleware[0];
for (let i = 0; i < arrPaths.length; i++) {
const p = arrPaths[i];
router.use.apply(router, [p].concat(middleware.slice(1)));
}
return this;
}
const hasPath = typeof middleware[0] === 'string';
if (hasPath) path = middleware.shift();
for (let i = 0; i < middleware.length; i++) {
const m = middleware[i];
if (m.router) { // 子路由判断
const cloneRouter = Object.assign(Object.create(Router.prototype), m.router, {
stack: m.router.stack.slice(0)
});
for (let j = 0; j < cloneRouter.stack.length; j++) {
const nestedLayer = cloneRouter.stack[j];
const cloneLayer = Object.assign(
Object.create(Layer.prototype),
nestedLayer
);
if (path) cloneLayer.setPrefix(path);
if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
// 将克隆后的layer push到当前路由的stack中
router.stack.push(cloneLayer);
cloneRouter.stack[j] = cloneLayer;
}
if (router.params) {
function setRouterParams(paramArr) {
const routerParams = paramArr;
for (let j = 0; j < routerParams.length; j++) {
const key = routerParams[j];
cloneRouter.param(key, router.params[key]);
}
}
setRouterParams(Object.keys(router.params));
}
} else {
const keys = [];
pathToRegexp(router.opts.prefix || '', keys);
const routerPrefixHasParam = router.opts.prefix && keys.length;
router.register(path || '([^\/]*)', [], m, { end: false, ignoreCaptures: !hasPath && !routerPrefixHasParam });
}
}
return this;
};