[源码实现] koa-router

182 阅读4分钟

目标:实现简单的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);

这是最简单的路由,从中能得到的信息有(先不考虑其他用法)

  1. Router 是一个构造函数
  2. router 上有处理请求的方法,如get
    1. 有两个参数,路径 path,中间件函数 middleware
    2. 中间件函数 middleware 是异步方法
      1. 有两个参数,上下文 ctx,next函数
      2. 与koa中间件一样,需要调用next程序才能执行之后的路由/中间件
  3. 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。具体流程细节就是:

  1. 实例上创建 _router = [] 准备接受路由相关信息
  2. 初始化调用 router.get(path, middleware),把 get、path、middleware 存入 _router 中
  3. app.use(router.routes()) 把路由挂载到koa中间件上,等待请求来临。注意此时router.routes()已经执行!挂载到中间件的是router.routes()执行后的结果!
  4. 请求来临触发挂载的路由函数。从 _router 中依次取出路由信息,与当前请求的path、method做匹配,直到遍历完毕得到所以匹配上的路由相关信息
  5. 用 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