目标:实现简单的koa-router核心代码,便于理解koa-router原理。也能为今后看 express 打一点点基础
简单分析koa-router
先上基础代码
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
router.get('/', async (ctx, next) => {
ctx.body = 'hello world 111';
next();
})
router.get('/', async (ctx, next) => {
ctx.body = 'hello world 222';
next();
})
app.use(router.routes());
app.listen(3000);
这是最简单的路由,从中能得到的信息有(先不考虑其他用法)
- Router 是一个构造函数
- router 上有处理请求的方法,如get
- 有两个参数,路径 path,中间件函数 middleware
- 中间件函数 middleware 是异步方法
- 有两个参数,上下文 ctx,next函数
- 与koa中间件一样,需要调用next程序才能执行之后的路由/中间件
- koa-router 是当中间件使用。router.routes 返回的是中间件方法
实现next函数
next 实现的核心就是让函数按顺序包裹起来,就像洋葱。一般会用一个数组来保持要实现next的方法
比如有两个方法 f1 和 f2,组成一个数组 [f1, f2],我们的目标是组成 () => f1(() => f2()),当我们执行这个函数时执行的代码是 f1(() => f2()),() => f2() 就是 f1的next。具体实现是
let middlewares = [f1, f2, f3...]
function compose(callback) {
async function dispatch(i) {
if (i === routers.length) return callback() // 边界函数
const middleware = middlewares[i] // 取出当前的函数
return middleware(() => dispatch(i+1)) // 出入next
}
return dispatch(0)
}
/*
compose 的执行结果就是把这些 middlewares 串在一起,像这样
() => f1(
() => f2(
() => f3(
// ...
)
)
)
*/
还有一种实现方法是用 reduce 实现,以后会单独开一篇
简单实现 router.get 与 router.routes
router.get 与 router.routes 的关系是什么
每次调用 router.get 把 get、path、middleware 储存到实例的_router中。等到请求来到时候koa会调用 router.routes,从实例的_router中找出 get、path 对应的 middleware 并执行。next的实现也只是多传了一个ctx。具体流程细节就是:
- 实例上创建 _router = [] 准备接受路由相关信息
- 初始化调用 router.get(path, middleware),把 get、path、middleware 存入 _router 中
- app.use(router.routes()) 把路由挂载到koa中间件上,等待请求来临。注意此时router.routes()已经执行!挂载到中间件的是router.routes()执行后的结果!
- 请求来临触发挂载的路由函数。从 _router 中依次取出路由信息,与当前请求的path、method做匹配,直到遍历完毕得到所以匹配上的路由相关信息
- 用 compose 函数串联所以匹配到到函数。并执行串联后最外侧函数
上代码
class Router {
constructor() {
this._router = [];
}
compose(ctx, routers, koa_next) { // 方面阅读理解给next改个名
async function dispatch(i) {
if (i === routers.length) return koa_next() // 边界函数。路由中间件执行完毕
const router = routers[i]
return router.middleware(ctx, () => dispatch(i + 1))
}
return dispatch(0)
}
get(path, middleware) {
this._router.push({ method: 'GET', path, middleware })
}
routes() {
return async (ctx, next) => { // 请求到来时会先调用这个方法
const method = ctx.method;
const path = ctx.path;
// 筛选出匹配的 router,方便取 middleware
const routers = this._router.filter(route => {
// 为了简单不考虑 path 的正则匹配
return route.path === path && route.method === method
})
// 把筛选出来的router串联(传入next)
return this.compose(ctx, routers, next)
}
}
}
module.exports = Router
此时这些代码就能跑前文给出的例子了。
二级路由(前缀)
router.prefix() 可以给路由添加前缀
// ...
const weixin = new Router();
const qq = new Router();
weixin.prefix('/weixin');
weixin.get('/home', async (ctx, next) =>{/*...*/}); // /weixin/home
weixin.get('/user', async (ctx, next) =>{/*...*/}); // /weixin/user
qq.prefix('/qq');
qq.get('/home', async (ctx, next) =>{/*...*/}); // /qq/home
qq.get('/user', async (ctx, next) =>{/*...*/}); // /qq/user
app.use(weixin.routes());
app.use(qq.routes());
// ...
这样访问weixin的user页就要访问 /weixin/user,就与qq区分开来。 weixin 和 qq 这个页面就有自己的前缀,相互不干扰。
这个就很简单了,只需要在实例上记录 prefix,在匹配路由时在原有 path 前加上 prefix 就可以实现
class Router {
constructor() {
this._router = [];
this._prefix = ''; // 记录前缀
}
prefix(prefixPath) {
this._prefix = prefixPath; // 把 prefixPath 记在 _prefix上
}
compose(ctx, routers, koa_next) { // 方面阅读理解给next改个名
async function dispatch(i) {
if (i === routers.length) return koa_next() // 边界函数。路由中间件执行完毕
const router = routers[i]
return router.middleware(ctx, () => dispatch(i + 1))
}
return dispatch(0)
}
get(path, middleware) {
this._router.push({ method: 'GET', path, middleware })
}
routes() {
return async (ctx, next) => { // 请求到来时会先调用这个方法
const method = ctx.method;
const path = ctx.path;
// 筛选出匹配的 router,方便取 middleware
const routers = this._router.filter(route => {
// 为了简单不考虑 path 的正则匹配
return (this._prefix + route.path) === path && route.method === method
})
// 把筛选出来的router串联(传入next)
return this.compose(ctx, routers, next)
}
}
}
module.exports = Router