【4858美高梅】koa源码阅读,自学笔记

By admin in 4858美高梅 on 2019年4月4日

koa源码阅读[2]-koa-router

其三篇,有关koa生态中比较关键的二当中间件:koa-router

第一篇:koa源码阅读-0
第二篇:koa源码阅读-一-koa与koa-compose

Koa
应用程序是3个分包一组中间件函数的对象,它是根据类似堆栈的措施组织和实施的。

此种类文章会不停立异。。。


koa-router是什么

先是,因为koa是一个管理中间件的阳台,而注册叁在那之中间件使用use来执行。
不论是什么请求,都会将有所的中间件执行3次(假设未有中途甘休的话)
所以,这就会让开发者很干扰,假设大家要做路由该怎么写逻辑?

app.use(ctx => {
  switch (ctx.url) {
    case '/':
    case '/index':
      ctx.body = 'index'
      break
    case 'list':
      ctx.body = 'list'
      break
    default:
      ctx.body = 'not found'
  }
})

 

的确,这样是二个简易的诀窍,然而毫无疑问不适用于大型项目,数10个接口通过一个switch来控制未免太繁琐了。
何况请求恐怕只协助get或者post,以及那种办法并不能够很好的援助UCRUISERL中含有参数的乞求/info/:uid
express中是不会有那样的难题的,本身已经提供了getpost等等等的与METHOD同名的函数用来注册回调:
express

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('hi there.')
})

 

但是koa做了诸多的简要,将洋洋逻辑都拆分出来作为独立的中间件来存在。
据此导致众多express连串搬迁为koa时,需求额外的设置1些中间件,koa-router相应说是最常用的四个。
所以在koa中则必要万分的装置koa-router来兑现类似的路由成效:
koa

const Koa = require('koa')
const Router = require('koa-router')

const app = new Koa()
const router = new Router()

router.get('/', async ctx => {
  ctx.body = 'hi there.'
})

app.use(router.routes())
  .use(router.allowedMethods())

 

看起来代码确实多了部分,毕竟将许多逻辑都从框架之中间转播移到了中间件中来处理。
也算是为了保全1个简便的koa框架所取舍的局地东西啊。
koa-router的逻辑确实要比koa的错综复杂壹些,能够将koa想象为四个市面,而koa-router则是个中三个地摊
koa仅要求确认保证市镇的平稳运维,而真的和消费者打交道真的是在内部摆摊的koa-router

那是 koa 对协调的牵线,其余 koa 依赖的库其实都得以算是中间件,koa-router
也不例外。

那篇重要讲 koa二 的路由

动用体验

koa-router的大约结构

koa-router的协会并不是很复杂,也就分了多少个文件:

.
├── layer.js
└── router.ja

 

layer主若是本着有的音讯的卷入,首要路基由router提供:

tag desc
layer 信息存储:路径、METHOD、路径对应的正则匹配、路径中的参数、路径对应的中间件
router 主要逻辑:对外暴露注册路由的函数、提供处理路由的中间件,检查请求的URL并调用对应的layer中的路由处理

ps: 本文代码中的中文演讲是对代码的执教,省略号(…)代表省略部分代码
文章最终有简版router的类型地址

首先,大家先用 koa2 原生实现3个简易的路由然后再利用 koa-router
中间件深远学习,学路由自然先从 U悍马H2L 动手,请看上面的事例

koa

const Koa = require('koa');
const app = new Koa();
app.use(ctx => {
   ctx.body = 'Hello Koa';
 });
app.listen(3000);

koa-router的运营流程

能够拿上面所抛出的骨干例子来注脚koa-router是哪些的二个实施流程:

const router = new Router() // 实例化一个Router对象

// 注册一个路由的监听
router.get('/', async ctx => {
  ctx.body = 'hi there.'
})

app
  .use(router.routes()) // 将该Router对象的中间件注册到Koa实例上,后续请求的主要处理逻辑
  .use(router.allowedMethods()) // 添加针对OPTIONS的响应处理,一些预检请求会先触发 OPTIONS 然后才是真正的请求

 

对 koa-router 的猜想

通过 koa 最简便的 hellow world 例子能够看来原生对请求的处理格局:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

如若大家想大约的完毕路由的话,能够添加一些断定标准

app.use(async ctx => {
  if (ctx.path === '/one' && ctx.method === 'get') {
    ctx.body = 'Hello World';
  } else {
    ctx.status = 404;
    ctx.body = '';
  }
});

那样的话能兑现简单对路由的贯彻,但是路由更多的话消耗的性质也就越大,而且不便于对尤其路由添加中间件。而更好的不二等秘书诀是利用面向对象的格局,遵照请求的
path 和 method 重回相应的中间件处理函数和施行函数。

const koa = require('koa');
const fs = require('fs');
const app = new koa();

app.use(ctx => {
    console.log(ctx.request.url);
});

app.listen(3000);

express

const app = require("express")();
app.use((req,res,next)=>{
    res.status(200).send("<h1>headers ...</h1>");
});
app.listen(3001);

注意:本文全体使用es6语法编写,假使条件不匡助请自行升级node大概选用babel实行转码。

创立实例时的局地政工

首先,在koa-router实例化的时候,是足以传递三个布置项参数作为伊始化的陈设音信的。
可是那个布局项在readme中只是简短的被描述为:

Param Type Description
[opts] Object  
[opts.prefix] String prefix router paths(路由的前缀)

报告大家得以增加多少个Router挂号时的前缀,也正是说假如根据模块化分,能够不必在各个路径匹配的前端都丰盛巨长的前缀:

const Router = require('koa-router')
const router = new Router({
  prefix: '/my/awesome/prefix'
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /my/awesome/prefix/index => pong!

 

P.S.
但是要铭记在心,若是prefix/最后,则路由的登记就能够节省前缀的/了,不然会合世/双重的情况

实例化Router时的代码:

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 = []
}

 

可知的只有八个methods的赋值,不过在查看了其它源码后,发现除去prefix再有一些参数是实例化时传递进入的,然则不太理解为何文书档案中并未有关联:

Param Type Default Description
sensitive Boolean false 是否严格匹配大小写
strict Boolean false 如果设置为false则匹配路径后边的/是可选的
methods Array[String] ['HEAD','OPTIONS','GET','PUT','PATCH','POST','DELETE'] 设置路由可以支持的METHOD
routerPath String null  

解读思路

那边要介绍下自身解读 koa-router 源码的秘诀,笔者会先把 koa-router
的源码下载到本地,然后通读1遍(因为源码算是比较少的),从大体上上知道
koa-router 执行流程,然后通过单元测试去 debug 分析。

当大家进入
http://localhost:3000/index
地址时可以窥见 Iterm 中打字与印刷出了 /index 表明大家能够透过
ctx.request.url 属性得到 U景逸SUVL
的不二等秘书籍,那样我们就足以接纳这一个路子找到呼应的页面完结路由。

初叶格局

  1. koa采用了new Koa()的方法,而express选拔古板的函数情势,比较源码如下:

//koa
const Emitter = require('events');
module.exports = class Application extends Emitter {
...
}
//express
exports = module.exports = createApplication;
function createApplication() {
...
}

能够看出koa@2接纳了es六的语法完结,继承了Emitter类,具体新闻能够参考Emitter说明。那就表示koa@3只万幸es陆以上的环境下运转,低版本能够思量选取koa@壹.x。而express则比较守旧,使用的是function的款式导出。

  1. 中间件情势贰者不均等,那是由两岸处理中间件的逻辑差别导致的,实际上那也是两岸最根本的出入,具体的辨析留作后边实行比较,那里关键相比两者的利用上的异样,如下所示:

sensitive

只要设置了sensitive,则会以更严苛的匹配规则来监听路由,不会忽略U本田CR-VL中的大小写,完全根据注册时的来协作:

const Router = require('koa-router')
const router = new Router({
  sensitive: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index => pong!
// curl /Index => 404

 

Router 执行流程图

4858美高梅 1

koa-router 流程.png

本人觉得 koa-router 最中央且基本的API有两个:

  1. router.match
    可以根据请求的 path 和 method 筛选出协作的 route
  2. router.register
    注册 route
  3. router.routes
    回到用于 koa 加载的中间件,通过 koa-compose 将middlewares
    压缩成叁个函数
  4. router.method(get、post等)
    能够依据path、method 定义 router,并且可以将middleware绑定在路由上

上边完成一个简约的路由,文件目录如下

express处理两在那之中间件

const app = require("express")();
app.use((req,res,next)=>{
    console.log("first");
    //next();
});
app.use((req,res,next)=>{
    console.log("second");
    //next();
});
app.use((req,res,next)=>{
    console.log("third");
    res.status(200).send("<h1>headers ...</h1>");
});
app.listen(3001);

就算写成那样,终端只会打字与印刷出first,而且不会反悔,前端请求会直接等候到过期,导致这一难点的原因是:express必须主动调用next()才能让中间价继续执行,放手注释即可。那也准保了大家得以自主决定什么响应请求。

strict

strictsensitive功能看似,也是用来设置让路径的匹配变得越来越严酷,在私下认可景况下,路径结尾处的/是可选的,即使翻开该参数以往,若是在注册路由时尾巴部分未有增进/,则匹配的路由也必定不可见添加/结尾:

const Router = require('koa-router')
const router = new Router({
  strict: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index  => pong!
// curl /Index  => pong!
// curl /index/ => 404

 

解读

大家得以构成代码和单元测试对源码实行通晓,由最简便易行的测试发轫debug:

it('router can be accecced with ctx', function (done) {
      var app = new Koa();
      var router = new Router();
      router.get('home', '/', function (ctx) {
          ctx.body = {
            url: ctx.router.url('home')
          };
      });

      console.log(router.routes()); // 这是我加的,查看最后加载的routes
      app.use(router.routes());
      request(http.createServer(app.callback()))
          .get('/')
          .expect(200)
          .end(function (err, res) {
              if (err) return done(err);
              expect(res.body.url).to.eql("/");
              done();
          });
  });

router.routes() 返回:

function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;
    ...
    ctx.router = router;
    if (!matched.route) return next();
    // 获取已匹配的 routes (实例化 Layer 对象)
    var matchedLayers = matched.pathAndMethod
    ...
    // 若匹配了多个 route,则将多个执行函数 push 进一个数组
    layerChain = matchedLayers.reduce(function(memo, layer) {
      ...
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  }

router.routes() 重返一个 dispatch 函数,从中可以见见请求进来会经过
router.match(前边有分析),然后将十分到的 route 的执行函数 push
进数组,并透过 compose(koa-compose) 函数合并再次来到。

下一场在打字与印刷出 compose(layerChain)
方法,能够见到实际提起底呼吁执行的函数是对ctx.body = {url: ctx.router.url('home')};
的 compose 封装函数,在效益上一对一于

app.use(ctx => {
  ctx.body = {
    url: ctx.router.url('home')
  };
});
  • Router 构造函数

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 = {};
  // 初始化定义 route 栈
  this.stack = [];
};
  • 分析 router.method 方法

// methods ['get', 'post', 'delete', 'put', 'patch', ...]
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    if (typeof path === 'string' || path instanceof RegExp) {
      // 若第二个参数是 string 或 正则表达式,则将后面的参数归为 middleware
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      // 否则说明没有传 name 参数,将第一个参数置为path,之后的参数归为 middleware
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }

    // 注册 route(下面会讲到 register 方法)
    this.register(path, [method], middleware, {
      name: name
    });

    // 返回 Router 对象,可以链式调用
    return this;
  };
});
  • 分析 router.register 方法

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var stack = this.stack;
  ...
  // create route
  // 实例化一个 Layer 对象,Layer 对象将 path 转为 regexp,并增加了匹配 path 的可选 ops 参数
  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || "",
    ignoreCaptures: opts.ignoreCaptures
  });

  console.log(route);
  /**
   * Layer {
   * ...省略部分属性
   * methods: [ 'HEAD', 'GET' ],
   * stack: [ [Function] ],
   * path: '/',
   * regexp: { /^(?:\/(?=$))?$/i keys: [] } } // 用于匹配 path
   */
  ...
  // 将注册的 route 存放在 stack 队列中
  stack.push(route);

  return route;
};

register 方法重要用于实例化 Layer 对象,并帮衬多各 path
同时登记、添加路由前缀等效率(突显代码忽略)。

  • 分析 router.match

Router.prototype.match = function (path, method) {
  // 获取已经注册的 routes (实例化Layer对象)
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };

  // 循环查找能够匹配的route
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);

    // 根据layer.regexp.test(path) 匹配
    if (layer.match(path)) {
      matched.path.push(layer);

      // todo ~操作符暂时没懂
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        // 将匹配标志 route 设为 true,这里我觉得改为 hitRoute 更容易理解
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};
.
├── index.js
├── package.json
└── view
    ├── 404.html
    ├── index.html
    └── todo.html

koa处理几个中间件

const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
   ctx.body = 'Hello Koa-1';
   next();
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-2';
   next();
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-3';
   next();
 });
app.listen(3000);

与express类似,koa中间件的入参也有五个,后3个正是next。next的功用与express一样,那里不再赘述。

地点介绍了koa的next()的功能,这里的next()亟需1块调用,千万不要采纳异步调用,不要写成下边包车型客车款式,那样也正是未调用next(),具体原因后边源码部分会分析:

    app.use((ctx,next) => {
   ctx.body = 'Hello Koa-2';
   setTimeout(()=>next(),3000);
   //next();
 });

固然如此上边分析了2者的选择逻辑不同,可是由于koa在入参处给出了context,而该结构体包含了小编们回来请求的具备音讯,所以大家如故能够写出下边包车型大巴代码:

const Koa = require('koa');
const app = new Koa();

app.use((ctx)=>{
    const res = ctx.res;
    res.writeHead(200, {'Content-Type': 'text/html;charset=utf-8','Accept-Language':'zh-CN,zh;q=0.8,en;q=0.6'});
    res.end('<h1>标题</h1>');
});

// response
 app.use(ctx => {
   ctx.body = 'Hello Koa';
 });
app.listen(3000);

诸如此类的逻辑就和express很类似了,原理也一致。这样写现在,前端的呼吁获得的结果正是<h1>标题</h1>,而继续的app.use实际并不曾获得执行。

methods

methods布置项存在的意义在于,尽管我们有一个接口须要同时协助GETPOSTrouter.getrouter.post如此那般的写法必然是丑陋的。
故而我们大概会想到利用router.all来简化操作:

const Router = require('koa-router')
const router = new Router()

router.all('/ping', ctx => { ctx.body = 'pong!' })

// curl -X GET  /index  => pong!
// curl -X POST /index  => pong!

 

那大约是太完善了,能够很自在的兑现大家的须求,不过假设再多实验1些别样的methods后来,难堪的事情就时有产生了:

> curl -X DELETE /index  => pong!
> curl -X PUT    /index  => pong!

 

那鲜明不是符合大家预料的结果,所以,在那种情况下,基于近年来koa-router亟需开始展览如下修改来兑现我们想要的意义:

const Koa = require('koa')
const Router = require('router')

const app = new Koa()
// 修改处1
const methods = ['GET', 'POST']
const router = new Router({
  methods
})

// 修改处2
router.all('/', async (ctx, next) => {
  // 理想情况下,这些判断应该交由中间件来完成
  if (!~methods.indexOf(ctx.method)) {
    return await next()
  }

  ctx.body = 'pong!'
})

 

这么的两处改动,就足以达成大家所企望的成效:

> curl -X GET    /index  => pong!
> curl -X POST   /index  => pong!
> curl -X DELETE /index  => Not Implemented
> curl -X PUT    /index  => Not Implemented

 

本人个人认为那是allowedMethods金玉锦绣的多个逻辑难点,可是恐怕是自作者并未get到小编的点,allowedMethods中相比较根本的部分源码:

Router.prototype.allowedMethods = function (options) {
  options = options || {}
  let implemented = this.methods

  return function allowedMethods(ctx, next) {
    return next().then(function() {
      let allowed = {}

      // 如果进行了ctx.body赋值,必然不会执行后续的逻辑
      // 所以就需要我们自己在中间件中进行判断
      if (!ctx.status || ctx.status === 404) {
        if (!~implemented.indexOf(ctx.method)) {
          if (options.throw) {
            let notImplementedThrowable
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented() // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented()
            }
            throw notImplementedThrowable
          } else {
            ctx.status = 501
            ctx.set('Allow', allowedArr.join(', '))
          }
        } else if (allowedArr.length) {
          // ...
        }
      }
    })
  }
}

 

首先,allowedMethods是当做三个前置的中间件存在的,因为在回到的函数中先调用了next,其次才是指向METHOD的判断,而那样带来的2个后果就是,假使大家在路由的回调中展开类似ctx.body = XXX的操作,实际上会修改本次请求的status值的,使之并不会变成404,而望洋兴叹正确的接触METHOD反省的逻辑。
想要正确的接触METHOD逻辑,就供给本身在路由监听中手动判断ctx.method是不是为大家想要的,然后在跳过当前中间件的实行。
而这一判断的步调实际上与allowedMethods中间件中的!~implemented.indexOf(ctx.method)逻辑完全是再次的,不太精晓koa-router为什么会如此处理。

当然,allowedMethods是无法作为三个内置中间件来存在的,因为二个Koa中只怕会挂在多个RouterRouter以内的配置或然大有径庭,不能够确认保障拥有的Router都和当前Router可处理的METHOD是一样的。
故此,个人感觉methods参数的存在意义并不是相当的大。。

达成简版Router

经过地方的剖析,其实早就讲解了 koa-router 宗旨的有的:构造 Router 对象
=> 定义 router 入口 => 匹配路由 =>
合并中间件和实践函数输出;那四个API可以处理差不多的 restful
请求,额外的API例如重定向、router.use、路由前缀等在领会基本代码后阅读起来就大致很多了;简版其实正是地点api的精简版,原理一致,可以到自作者的门类看下
simple-koa-router:https://github.com/masongzhi/simple-koa-router

404.html

express分路由拍卖

express的代码1般如下:

const app = require("express")();
app.use("/first",(req,res,next)=>{
    console.log("first");
    res.status(200).send("<h1>headers-first ...</h1>");
});
app.use("/second",(req,res,next)=>{
    console.log("second");
    res.status(200).send("<h1>headers-second ...</h1>");
});
app.use("/third",(req,res,next)=>{
    console.log("third");
    res.status(200).send("<h1>headers-third ...</h1>");
});
app.listen(3001);

那很好驾驭,根据请求路径再次回到分化结果,koa呢?

routerPath

那些参数的存在。。感觉会招致有些很奇幻的地方。
那就要谈到在登记完全中学间件未来的router.routes()的操作了:

Router.prototype.routes = Router.prototype.middleware = function () {
  let router = this
  let dispatch = function dispatch(ctx, next) {
    let path = router.opts.routerPath || ctx.routerPath || ctx.path
    let matched = router.match(path, ctx.method)
    // 如果匹配到则执行对应的中间件
    // 执行后续操作
  }
  return dispatch
}

 

因为我们实际上向koa挂号的是如此的三个中间件,在每一趟请求发送过来时,都会实施dispatch,而在dispatch中判断是或不是命中有些router时,则会用到那一个布局项,那样的3个表明式:router.opts.routerPath || ctx.routerPath || ctx.pathrouter意味着当前Router实例,约等于说,即便我们在实例化3个Router的时候,如若填写了routerPath,那会导致无论任何请求,都会预先使用routerPath来作为路由检查:

const router = new Router({
  routerPath: '/index'
})

router.all('/index', async (ctx, next) => {
  ctx.body = 'pong!'
})
app.use(router.routes())

app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))

 

假诺有诸如此类的代码,无论请求什么UTiggoL,都会觉得是/index来进展匹配:

> curl http://127.0.0.1:8888
pong!
> curl http://127.0.0.1:8888/index
pong!
> curl http://127.0.0.1:8888/whatever/path
pong!

 

总结

【4858美高梅】koa源码阅读,自学笔记。koa-router
帮大家定义并采取相应的路由,对路由添加中间件和有个别同盟和表明的劳作;在
koa 中间件应用的底子上,相比较便于了然中间件的落实,koa-router
为大家做了更好的路由层管理,在设计上得以参照完成,同时商量精彩源码也是对团结的一种提高。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>404</title>
</head>
<body>
    404.html
</body>
</html>

koa分路由拍卖

const Koa = require('koa');
const app = new Koa();
app.use("/",ctx => {
   ctx.body = 'Hello Koa';
 });
app.listen(3000);

这么写会报错,因为koa自身并不协助按路由相应,假诺急需那样做,能够经过引入第二方包完结。在koajs中有1个总结的router包。
具体写法如下:

//摘抄自Koa Trie Router
const Koa = require('koa')
const Router = require('koa-trie-router')

let app = new Koa()
let router = new Router()

router
  .use(function(ctx, next) {
    console.log('* requests')
    next()
  })
  .get(function(ctx, next) {
    console.log('GET requests')
    next()
  })
  .put('/foo', function (ctx) {
    ctx.body = 'PUT /foo requests'
  })
  .post('/bar', function (ctx) {
    ctx.body = 'POST /bar requests'
  })

app.use(router.middleware())
app.listen(3000)

在切切实实应用中也足以行使别的的路由包来做,在github上能搜到不少。
其它,由于达成的原由,下面介绍叁个有趣的情景,看上面两段代码,初衷是打印请求处理耗费时间。

巧用routerPath达成转载作用

相同的,这些短路运算符一共有几个表明式,第贰个的ctx则是当前恳请的上下文,相当于说,借使大家有3个早于routes实施的中间件,也足以拓展赋值来修改路由判断所使用的URL

const router = new Router()

router.all('/index', async (ctx, next) => {
  ctx.body = 'pong!'
})

app.use((ctx, next) => {
  ctx.routerPath = '/index' // 手动改变routerPath
  next()
})
app.use(router.routes())

app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))

 

如此的代码也能够达成平等的作用。
实例化中传来的routerPath令人捉摸不透,不过在中间件中改变routerPath的那一个还能找到确切的气象,这些能够省略的知晓为转载的壹种实现,转发的经过是对客户端不可知的,在客户端看来依旧访问的是早期的U库罗德L,不过在中间件中改变ctx.routerPath能够很自由的使路由卓殊到我们想转载的地点去

// 老版本的登录逻辑处理
router.post('/login', ctx => {
  ctx.body = 'old login logic!'
})

// 新版本的登录处理逻辑
router.post('/login-v2', ctx => {
  ctx.body = 'new login logic!'
})

app.use((ctx, next) => {
  if (ctx.path === '/login') { // 匹配到旧版请求,转发到新版
    ctx.routerPath = '/login-v2' // 手动改变routerPath
  }
  next()
})
app.use(router.routes())

 

如此就达成了一个不难的转化:

> curl -X POST http://127.0.0.1:8888/login
new login logic!

 

todo.html

koa版本

const Koa = require('koa');
const app = new Koa();
app.use((ctx,next) => {
   ctx.body = 'Hello Koa-1';
   let start = new Date();
   next().then(()=>{
        console.log("time cost:",new Date()-start);
   });
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-2';
   next();
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-3';
   next();
 });
app.listen(3000);

出于koa选拔了promise的不贰秘诀处理中间件,next()实在重返的是2个promise对象,所以能够用地点简单的点子记录处理耗费时间。假诺在es7下,能够行使更简便易行的写法:

const Koa = require('koa');
const app = new Koa();
app.use(async (ctx,next) => {
   ctx.body = 'Hello Koa-1';
   let start = new Date();
   await next();
   console.log("time cost:",new Date()-start);
 });
 app.use(async (ctx,next) => {
   ctx.body = 'Hello Koa-2';
   //这里用了一个定时器表示实际的操作耗时
   await new Promise((resolve,reject)=>setTimeout(()=>{next();resolve();},3000));
 });
 app.use((ctx,next) => {
   ctx.body = 'Hello Koa-3';
   next();
 });
app.listen(3000);

这么只须要在入口放置贰个中间件即可落成耗费时间记下。

注册路由的监听

上述全体是关于实例化Router时的1些操作,上面就来说一下施用最多的,注册路由相关的操作,最熟识的自然就是router.get4858美高梅,,router.post这么些的操作了。
但实质上这个也只是1个快速方式罢了,在里边调用了来自Routerregister方法:

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {}

  let router = this
  let stack = this.stack

  // support array of paths
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts)
    })

    return this
  }

  // create route
  let route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || '',
    ignoreCaptures: opts.ignoreCaptures
  })

  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix)
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param])
  }, this)

  stack.push(route)

  return route
}

 

该办法在诠释中标为了 private
不过里面包车型大巴有的参数在代码中各类地点都未曾反映出来,鬼知道为何会留着那八个参数,但既然存在,就要求领悟他是怎么的

那一个是路由监听的根基艺术,函数签名大约如下:

Param Type Default Description
path String/Array[String] 一个或者多个的路径
methods Array[String] 该路由需要监听哪几个METHOD
middleware Function/Array[Function] 由函数组成的中间件数组,路由实际调用的回调函数
opts Object {} 一些注册路由时的配置参数,上边提到的strictsensitiveprefix在这里都有体现

能够看来,函数大约正是实现了那样的流程:

  1. 检查path是还是不是为数组,即便是,遍历item拓展调用本身
  2. 实例化两个Layer目的,设置有个别初阶化参数
  3. 安装针对1些参数的中间件处理(要是局地话)
  4. 将实例化后的目的放入stack中存储

从而在介绍那多少个参数以前,简单的叙述一下Layer的构造函数是很有供给的:

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {}
  this.name = this.opts.name || null
  this.methods = []
  this.paramNames = []
  this.stack = Array.isArray(middleware) ? middleware : [middleware]

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD')
    }
  }, this)

  // ensure middleware is a function
  this.stack.forEach(function(fn) {
    var type = (typeof fn)
    if (type !== 'function') {
      throw new Error(
        methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`"
      )
    }
  }, this)

  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

 

layer是负责储存路由监听的新闻的,每趟注册路由时的UEvoqueL,U库罗德L生成的正则表明式,该UPRADOL中设有的参数,以及路由对应的中间件。
统统交由Layer来囤积,重点须求关爱的是实例化进度中的那个数组参数:

  • methods
  • paramNames
  • stack

methods存款和储蓄的是该路由监听对应的立竿见影METHOD,并会在实例化的长河中针对METHOD进展高低写的更换。
paramNames因为用的插件难题,看起来不那么清晰,实际上在pathToRegExp里头会对paramNames本条数组进行push的操作,这么看恐怕会痛快一下pathToRegExp(path, &this.paramNames, this.opts),在拼接hash布局的门路参数时会用到这么些数组
stack积存的是该路由监听对应的中间件函数,router.middleware一些逻辑会重视于这一个数组

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>todo</title>
</head>
<body>
    todo.html
</body>
</html>

express版本

由于express并从未运用promise而是接纳了回调的主意处理中间件,所以不可能利用地点那样便于的秘诀赢得耗费时间。固然是对next()举行打包,也无效,因为必须保证持续的next()全部都被打包才能得到不错的结果。
上面给出3个参照实现:

let time = null;
.use('/', (req, res, next) => {
      time = Date.now();
      next()
    })
    .use('/eg', bidRequest)
    .use('/', (req, res, next) => {
      console.log(`<= time cost[${req.baseUrl}] : `, Date.now() - time, 'ms');
    })

path

在函数尾部的处理逻辑,主假如为着辅助多路径的同时登记,固然发现第二个path参数为数组后,则会遍历path参数进行调用本人。
因此针对三个URL的相同路由得以那样来拍卖:

router.register(['/', ['/path1', ['/path2', 'path3']]], ['GET'], ctx => {
  ctx.body = 'hi there.'
})

 

如此完全是叁个有效的安装:

> curl http://127.0.0.1:8888/
hi there.
> curl http://127.0.0.1:8888/path1
hi there.
> curl http://127.0.0.1:8888/path3
hi there.

 

index.html

总结

koa和express的分别还是比较大的,koa的内容很少,便是对nodejs本身的createServer函数做了简便的包装,未有做过多的延伸;而express主假诺比koa多了router。二者的的代码思路照旧很不平等的,不超过实际在应用中并不会有太大障碍。

methods

而关于methods参数,则暗中同意认为是1个数组,就算是只监听3个METHOD也急需传入二个数组作为参数,如若是空数组的话,尽管URL十三分,也会一贯跳过,执行下三在那之中间件,那些在再三再四的router.routes中会提到

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <h1>koa2 demo index page</h1>
    <p>this is a index page</p>
    <ul>
        <li>
            <a href="/">/</a>
        </li>
        <li>
            <a href="/index">/index</a>
        </li>
        <li>
            <a href="/todo">/todo</a>
        </li>
        <li>
            <a href="/404">/404</a>
        </li>
        <li>
            <a href="/nofund">/nofund</a>
        </li>
    </ul>
</body>
</html>

源码分析

middleware

middleware则是三遍路由真正举行的工作了,照旧是吻合koa标准的中间件,能够有七个,依照洋葱模型的主意来执行。
这也是koa-router中最器重的地点,能够让我们的部分中间件只在特定的URL时执行。
此间写入的两当中间件都是指向该URL生效的。

P.S.
koa-router中,还提供了三个办法,叫做router.use,那么些会登记四个基于router实例的中间件

重点是 index.js

koa

koa的源码主要有三个文本:application.js, context.js, request.js,
response.js

opts

opts则是用来安装有些路由生成的计划规则的,蕴含如下多少个可选的参数:

Param Type Default Description
name String 设置该路由所对应的name,命名router
prefix String 非常鸡肋的参数,完全没有卵用,看似会设置路由的前缀,实际上没有一点儿用
sensitive Boolean false 是否严格匹配大小写,覆盖实例化Router中的配置
strict Boolean false 是否严格匹配大小写,如果设置为false则匹配路径后边的/是可选的
end Boolean true 路径匹配是否为完整URL的结尾
ignoreCaptures Boolean 是否忽略路由匹配正则结果中的捕获组
const koa = require('koa');
const fs = require('fs');
const app = new koa();

/**
 * 用 Promise 封装一个异步读取文件的方法
 * @param {String} page html 文件名称
 * @returns {Object} Promise 实例对象
 */
function render(page) {
    return new Promise((resolve, reject) => {
        let viewUrl = `./view/${page}`;
        fs.readFile(viewUrl, 'binary', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

/**
 * 根据 URL 获取 HTML 内容
 * @param {String} url ctx.request.url
 * @returns {String} 获取到的 HTML 内容
 */
async function route(url) {
    let view = '404.html';
    switch (url) {
        case '/':
        case '/index':
            view = 'index.html';
            break;
        case '/todo':
            view = 'todo.html';
        case '/404':
            view = '404.html';
        default:
            break;
    }
    let html = await render(view);
    return html;
}

app.use( async(ctx) => {
    const html = await route(ctx.request.url);
    ctx.response.body = html;
});

app.listen(3000);
console.log('server is starting at prot 3000!');

context.js

context未有实际功效性代码,只是局地基础函数和变量,下边是代码片段。

inspect() {
    return this.toJSON();
  },
  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },
name

首先是name,首假如用于那一个地点:

  1. 抛出非凡时更有利的固定
  2. 能够透过router.url(<name>)router.route(<name>)收获到相应的router信息
  3. 在中间件执行的时候,name会被塞到ctx.routerName

    router.register(‘/test1’, [‘GET’], _ => {}, {
    name: ‘module’
    })

    router.register(‘/test2’, [‘GET’], _ => {}, {
    name: ‘module’
    })

    console.log(router.url(‘module’) === ‘/test1’) // true

    try {
    router.register(‘/test2’, [‘GET’], null, {

    name: 'error-module'
    

    })
    } catch (e) {
    console.error(e) // Error: GET error-module: middleware must be a function, not object
    }

 

假若多少个router利用相同的命名,则通过router.url调用重临初阶注册的那个:

// route用来获取命名路由
Router.prototype.route = function (name) {
  var routes = this.stack

  for (var len = routes.length, i=0; i<len; i++) {
    if (routes[i].name && routes[i].name === name) {
      return routes[i] // 匹配到第一个就直接返回了
    }
  }

  return false
}

// url获取该路由对应的URL,并使用传入的参数来生成真实的URL
Router.prototype.url = function (name, params) {
  var route = this.route(name)

  if (route) {
    var args = Array.prototype.slice.call(arguments, 1)
    return route.url.apply(route, args)
  }

  return new Error('No route found for name: ' + name)
}

 

施行脚本

request.js

该公文中关键是一群set和get函数,首倘诺用以获取请求结构体的特定字段或许涂改特定字段,比如下边获取ip的函数,代码很好了解:

get ips() {
    const proxy = this.app.proxy;
    const val = this.get('X-Forwarded-For');
    return proxy && val
      ? val.split(/\s*,\s*/)
      : [];
  },
跑题说下router.url的那多少个事儿

如若在项目中,想要针对少数URL进展跳转,使用router.url来生成path则是一个不易的采纳:

router.register(
  '/list/:id', ['GET'], ctx => {
    ctx.body = `Hi ${ctx.params.id}, query: ${ctx.querystring}`
  }, {
    name: 'list'
  }
)

router.register('/', ['GET'], ctx => {
  // /list/1?name=Niko
  ctx.redirect(
    router.url('list', { id: 1 }, { query: { name: 'Niko' } })
  )
})

// curl -L http://127.0.0.1:8888 => Hi 1, query: name=Niko

 

能够见见,router.url其实调用的是Layer实例的url方法,该措施重假如用来拍卖生成时代时髦传的局地参数。
源码地址:layer.js#L116
函数接收三个参数,paramsoptions,因为自个儿Layer实例是储存了相应的path等等的新闻,所以params哪怕储存的在路线中的1些参数的交替,options在近来的代码中,仅仅存在一个query字段,用来拼接search前边的数量:

const Layer = require('koa-router/lib/layer')
const layer = new Layer('/list/:id/info/:name', [], [_ => {}])

console.log(layer.url({ id: 123, name: 'Niko' }))
console.log(layer.url([123, 'Niko']))
console.log(layer.url(123, 'Niko'))
console.log(
  layer.url(123, 'Niko', {
    query: {
      arg1: 1,
      arg2: 2
    }
  })
)

 

上述的调用格局都是行得通的,在源码中有照应的拍卖,首先是对准多参数的论断,如若params不是3个object,则会觉得是经过layer.url(参数, 参数, 参数, opts)那种措施来调用的。
将其更换为layer.url([参数, 参数], opts)形式的。
此时的逻辑仅须求处理三种境况了:

  1. 数组格局的参数替换
  2. hash情势的参数替换
  3. 无参数

那个参数替换指的是,3个URL会透过叁个其三方的库用来处理链接中的参数部分,也便是/:XXX的那壹部分,然后传入三个hash贯彻类似模版替换的操作:

// 可以简单的认为是这样的操作:
let hash = { id: 123, name: 'Niko' }
'/list/:id/:name'.replace(/(?:\/:)(\w+)/g, (_, $1) => `/${hash[$1]}`)

 

然后layer.url的拍卖正是为着将种种参数生成类似hash那般的布局,最后替换hash获得完整的URL

node index.js

response.js

response与request对应,主若是1对甩卖res的工具类,下边是代码片段,用于安装和获取res的content-length:

set length(n) {
    this.set('Content-Length', n);
  },
  get length() {
    const len = this.header['content-length'];
    const body = this.body;

    if (null == len) {
      if (!body) return;
      if ('string' == typeof body) return Buffer.byteLength(body);
      if (Buffer.isBuffer(body)) return body.length;
      if (isJSON(body)) return Buffer.byteLength(JSON.stringify(body));
      return;
    }

    return ~~len;
  },

中间使用了~~len,这些有个别意思,五回取反,能够确定保证输出为数字,若是len为字符串则再次回到0。(第三次见…)

prefix

下面实例化Layer的历程中类似是opts.prefix的权重更高,不过随后在下边就有了3个论断逻辑进行调用setPrefix重复赋值,在翻遍了总体的源码后发现,那样绝无仅有的二个组别就在于,会有一条debug应用的是注册router时传入的prefix,而其余地方都会被实例化Router时的prefix所覆盖。

再者一旦想要路由不利的采纳prefix,则需求调用setPrefix,因为在Layer实例化的经过中有关path的积存正是来自远传入的path参数。
而应用prefix前缀则要求手动触发setPrefix

// Layer实例化的操作
function Layer(path, methods, middleware, opts) {
  // 省略不相干操作
  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

// 只有调用setPrefix才会应用前缀
Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path
    this.paramNames = []
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts)
  }

  return this
}

 

其一在爆出给使用者的多少个章程中都有呈现,类似的getset以及use
自然在文书档案中也提供了足以平素设置富有router前缀的办法,router.prefix
文书档案中就这么总结的报告你能够安装前缀,prefix在里边会循环调用全部的layer.setPrefix

router.prefix('/things/:thing_id')

 

而是在翻看了layer.setPrefix源码后才察觉此处实在是带有二个暗坑的。
因为setPrefix的落到实处是获得prefix参数,拼接到当前path的头部。
如此就会带来七个题材,若是大家1再调用setPrefix会招致多次prefix叠加,而非替换:

router.register('/index', ['GET'], ctx => {
  ctx.body = 'hi there.'
})

router.prefix('/path1')
router.prefix('/path2')

// > curl http://127.0.0.1:8888/path2/path1/index
// hi there.

 

prefix方法会叠加前缀,而不是覆盖前缀

咱俩得以看出

application.js

上文中用到的app正是在该公文中定义的,也是koa的中坚所在,那里挑选多少个分子函数实行辨析(整个文件代码也就不到250行,本身看完压力也小小的)。

module.exports = class Application extends Emitter {
/*
    构造函数:把req,res,env等常用的变量全都塞进了context,所以我们在中间件中拿到context以后,就可以随心所欲地操作req和res了。
*/
  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);
  }
/*
    实际就是调用了nodejs本身的createServer,没有任何区别。
*/
  listen() {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }
  //下面分析
  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;
  }
  //下面分析
  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;
  }

  //新建context
  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;
  }
//下面分析
function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

上边重点看use,callback,respond那三个函数,实际上精晓koa的音讯流看那多少个函数的源码就基本上丰硕了。
use :
内容不多,个中第叁个if用于安检,第一个if用以落到实处对generator函数的非凡,具体达成进度在is-generator-function本条包里面,有趣味可以看看,依旧挺有技巧的,参考借用。use最后独自就是把中间件push到了this.middleware数组里,并不曾其它实质的逻辑操作。
respond :
该函数便是响应请求的地点,那也是为啥我们得以不用主动地响应请求。函数里做了许多判定,首如果预防一回响应以及分外特定的响应的乞求。
callback :
callback用于转移createServer函数的回调,即handleRequest函数。handleRequest的再次回到值便是2个promise对象。注意那里调用了2个compose措施,该办法的作用便是把中间件数组转换来1个函数,以方便使用。具体的贯彻在koa-compose这几个包里,那里摘抄当中的1段来分析。

//这就是compose(...)返回的函数
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)
      }
    }
  }

能够见见,实际上那里便是用闭包完成了对中间件数组的遍历。具体思路会把第i+一个中间件作为next传给第i个中间件,那也正是干吗必须积极调用next的案由,以为如果不积极调用next这壹循环就会提前甘休了,后续的中间件就不可能获取执行。

到此甘休,koa源码分析就甘休了,koa源码很少,未有多余的事物,甚至连路由都亟需引入其余的包。

sensitive与strict

那俩参数没啥好说的,正是会覆盖实例化Router时所传递的那俩参数,效果都壹致。

4858美高梅 2

express

express的源码比koa多了累累事物,那里仅仅比较中央部分,忽略其余1些的剧情。

//express.js
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

此间的express正是咱们上文中引入的express对象,可以看到,实际上该函数正是把某些常用的意义和变量绑定到了app对象中去,我们在代码中动用的app.eg_funcs等等的章程都是从那里延续取得的。实际上该对象并不局限于选用app.listen()来运维三个劳动,上边是listen函数的代码。

//application.js
app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

调用app.listen能够运维一个服务器,实际上大家也足以直接手动写出那两句代码来运行2个劳动。在socket.iohttps劳务中就必要协调来达成那壹经过。
下面是app.use的源码:

app.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate app.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var fns = flatten(slice.call(arguments, offset));

  if (fns.length === 0) {
    throw new TypeError('app.use() requires middleware functions');
  }

  // setup router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);
    }

    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });

    // mounted an app
    fn.emit('mount', this);
  }, this);

  return this;
}

那里有部分对参数判断的逻辑,比如第贰个参数假诺是路线照旧函数,但是日常很少这么写。

从中能够看出实际上express是调用了router的use方法对中间件实行拍卖。router.use定义在/router/index.js中,
源码如下:

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires middleware functions');
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
    }

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
};

中间前大致段重点是局地预备工作(那种写法在express貌似很普遍)。后边看到与koa直接把中间件push到数组的做法差别的是,express会把中间件封装成三个Layer,那样做也是为了更好地控制中间件的实施。Layer的代码在/router/layer.js中。(那里不再分析)

上边开端分析express是怎么响应请求的,从地点listen部分的代码能够看来,大家给createServer传了二个this,而这几个this便是express()的重回值,定义在application.js里,源码如下:

var app = function(req, res, next) {
    app.handle(req, res, next);
  };

能够看看实际app是调用了handle方法,而该方法是从application对象继承过来的,而查看application.js发现了上边代码:

//初始化 this._router
this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
//使用 this._router
app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  // no routes
  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

  router.handle(req, res, done);
};

能够看出实际那里调用的是router.handle,上边看router的源码:

proto.handle = function handle(req, res, out) {
  var self = this;

  debug('dispatching %s %s', req.method, req.url);

  var idx = 0;
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  var slashAdded = false;
  var paramcalled = {};

  // store options for OPTIONS request
  // only used if OPTIONS request
  var options = [];

  // middleware and routes
  var stack = self.stack;

  // manage inter-router variables
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');

  // setup next layer
  req.next = next;

  // for options requests, respond with a default if nothing else responds
  if (req.method === 'OPTIONS') {
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err);
      sendOptionsResponse(res, options, old);
    });
  }

  // setup basic req values
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;

  next();

  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    // remove added slash
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // restore altered req.url
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // signal to exit router
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    // no more matching layers
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }

    // get pathname of request
    var path = getPathname(req);

    if (path == null) {
      return done(layerError);
    }

    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      if (!route) {
        // process non-route handlers normally
        continue;
      }

      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }

      var method = req.method;
      var has_method = route._handles_method(method);

      // build up automatic options response
      if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') {
        match = false;
        continue;
      }
    }

    // no match
    if (match !== true) {
      return done(layerError);
    }

    // store route for dispatch on change
    if (route) {
      req.route = route;
    }

    // Capture one-time layer values
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;

    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }

      if (route) {
        return layer.handle_request(req, res, next);
      }

      trim_prefix(layer, layerError, layerPath, path);
    });
  }

  function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
      // Validate path breaks on a path separator
      var c = path[layerPath.length]
      if (c && c !== '/' && c !== '.') return next(layerError)

      // Trim off the part of the url that matches the route
      // middleware (.use stuff) needs to have the path stripped
      debug('trim prefix (%s) from url %s', layerPath, req.url);
      removed = layerPath;
      req.url = protohost + req.url.substr(protohost.length + removed.length);

      // Ensure leading slash
      if (!protohost && req.url[0] !== '/') {
        req.url = '/' + req.url;
        slashAdded = true;
      }

      // Setup base URL (no trailing slash)
      req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
        ? removed.substring(0, removed.length - 1)
        : removed);
    }

    debug('%s %s : %s', layer.name, layerPath, req.originalUrl);

    if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

本条函数十分短,但是事实上多数内容都以匹配路由,类型检查测试等操作,实际的操作集中在next()函数中,与koa壹样,那里也是运用闭包来循环遍历中间件数组。看next()中的执行部分能够观望,平常意况下,实际的操作是由layer.handle_request完成的,下面看layer.js源码:

//初始化
function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}
//处理单元
Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

bingo,那里正是大家在调用use的时候伊始化的Layer结构体,能够见见,实际上我们把中间件函数赋给了layer.handle,而在实际上的处理函数handle_request中,正是调用了this.handle,总算找到了数额处理的来自了….
此处看到实际router中的next()只是运营了中间件回调的经过,然后把团结传给下2其中间件,后续的中间件主动调用next()如此就能够传递下去了。

在拍卖中间件的逻辑上express可以领略为每一趟2在那之中间件执行完结就去主动去通告“中央”去运转下三个中间件;而koa能够精通为链式进程,每一在那之中间件会运行后三当中间件。

到此甘休,我们基本形成了koa和express的相比较分析,二者各有本身的表征,在使用中能够依据供给选取最契合的缓解方案。

end

end是1个很有意思的参数,那些在koa-router中引用的别的模块中有呈现到,path-to-regexp:

if (end) {
  if (!strict) route += '(?:' + delimiter + ')?'

  route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'
} else {
  if (!strict) route += '(?:' + delimiter + '(?=' + endsWith + '))?'
  if (!isEndDelimited) route += '(?=' + delimiter + '|' + endsWith + ')'
}

return new RegExp('^' + route, flags(options))

 

endWith能够总结地精通为是正则中的$,也等于匹配的尾声。
看代码的逻辑,大概正是,如若设置了end: true,则无论任何动静都会在结尾添加$代表极度的结尾。
而如果end: false,则唯有在同时设置了strict: false或者isEndDelimited: false时才会接触。
故而我们可以经过那三个参数来促成UTucsonL的歪曲匹配:

router.register(
  '/list', ['GET'], ctx => {
    ctx.body = 'hi there.'
  }, {
    end: false,
    strict: true
  }
)

 

也等于说上述代码末了生成的用于匹配路由的正则表明式大致是这么的:

/^\/list(?=\/|$)/i

// 可以通过下述代码获取到正则
require('path-to-regexp').tokensToRegExp('/list/', {end: false, strict: true})

 

结尾的$是可选的,那就会导致,我们如若发送任何早先为/list的呼吁都会被那当中间件所收获到。

route

ignoreCaptures

ignoreCaptures参数用来设置是不是供给再次回到URL中匹配的途径参数给中间件。
而壹旦设置了ignoreCaptures尔后那多个参数就会变为空对象:

router.register('/list/:id', ['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // ['1'], { id: '1' }
})

// > curl /list/1

router.register('/list/:id', ['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // [ ], {  }
}, {
  ignoreCaptures: true
})
// > curl /list/1

 

以此是在中间件执行时期调用了来自layer的三个情势获得的。
第三调用captures获得具有的参数,即使设置了ignoreCaptures则会招致一向回到空数组。
下一场调用params将注册路由时所生成的富有参数以及参数们其实的值传了进入,然后生成3个总体的hash注入到ctx对象中:

// 中间件的逻辑
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
// 中间件的逻辑 end

// layer提供的方法
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []
  return path.match(this.regexp).slice(1)
}

Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {}

  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) {
      var c = captures[i]
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c
    }
  }

  return params
}

// 所做的事情大致如下:
// [18, 'Niko'] + ['age', 'name']
// =>
// { age: 18, name: 'Niko' }

 


router.param的作用

上述是有关心册路由时的有个别参数描述,能够见到在register中实例化Layer目的后并不曾从来将其放入stack中,而是进行了那样的二个操作之后才将其推入stack

Object.keys(this.params).forEach(function (param) {
  route.param(param, this.params[param])
}, this)

stack.push(route) // 装载

 

此间是作为添加针对有些URL参数的中间件处理的,与router.param两者关联性很强:

Router.prototype.param = function (param, middleware) {
  this.params[param] = middleware
  this.stack.forEach(function (route) {
    route.param(param, middleware)
  })
  return this
}

 

三头操作看似,前者用于对新增的路由监听添加全数的param中间件,而后人用于针对现有的全体路由添加param中间件。
因为在router.param中有着this.params[param] = XXX的赋值操作。
如此在继承的新增路由监听中,直接循环this.params就足以得到独具的中间件了。

router.param的操作在文书档案中也有介绍,文书档案地址
粗粗便是足以用来做一些参数校验之类的操作,可是因为在layer.param中有了一些突出的拍卖,所以大家不必担心param的实施顺序,layer会保证param一定是早于正视那一个参数的中间件执行的:

router.register('/list/:id', ['GET'], (ctx, next) => {
  ctx.body = `hello: ${ctx.name}`
})

router.param('id', (param, ctx, next) => {
  console.log(`got id: ${param}`)
  ctx.name = 'Niko'
  next()
})

router.param('id', (param, ctx, next) => {
  console.log('param2')
  next()
})


// > curl /list/1
// got id: 1
// param2
// hello: Niko

 

手写路由很费劲,所以需求用中间件的样式处理路由,用到了 koa-router

最常用的get/post之类的火速方式

以及说完了上边包车型客车根底措施register,我们能够来看下暴光给开发者的多少个router.verb方法:

// get|put|post|patch|delete|del
// 循环注册多个METHOD的快捷方式
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    let 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
  }
})

Router.prototype.del = Router.prototype['delete'] // 以及最后的一个别名处理,因为del并不是有效的METHOD

 

令人适得其反的是,verb方法将大气的opts参数都砍掉了,暗中同意只留下了三个name字段。
只是相当粗略的处理了壹晃命名name路由相关的逻辑,然后举办调用register形成操作。

自个儿的就学思路是先用法后原理,所以先读书用法

router.use-Router内部的中间件

以及上文中也关乎的router.use,可以用来注册二个中间件,使用use挂号中间件分为二种情况:

  1. 1般说来的中间件函数
  2. 将现有的router实例作为中间件传入

安装 koa-router

普通的use

这里是use主意的严重性代码:

Router.prototype.use = function () {
  var router = this
  middleware.forEach(function (m) {
    if (m.router) { // 这里是通过`router.routes()`传递进来的
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path)
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) // 调用`use`的Router实例的`prefix`
        router.stack.push(nestedLayer)
      })

      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key])
        })
      }
    } else { // 普通的中间件注册
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath })
    }
  })
}

// 在routes方法有这样的一步操作
Router.prototype.routes = Router.prototype.middleware = function () {
  function dispatch() {
    // ...
  }

  dispatch.router = this // 将router实例赋值给了返回的函数

  return dispatch
}

 

先是种是相比正规的格局,传入3个函数,3个可选的path,来展开挂号中间件。
但是有少数要注意的是,.use('path')那样的用法,中间件不可能独立存在,必必要有一个能够与之途径相匹配的路由监听存在:

router.use('/list', ctx => {
  // 如果只有这么一个中间件,无论如何也不会执行的
})

// 必须要存在相同路径的`register`回调
router.get('/list', ctx => { })

app.use(router.routes())

 

由来是那般的:

  1. .use.get都以依照.register来促成的,不过.usemethods参数中传送的是3个空数组
  2. 在三个途径被匹配到时,会将有着匹配到的中档件取出来,然后检核查应的methods,如果length !== 0则会对脚下匹配组标记一个flag
  3. 在实行中间件以前会先判断有未有那些flag,要是未有则表明该路线全部的中间件都尚未安装METHOD,则会直接跳过进入别的流程(比如allowedMethod

    Router.prototype.match = function (path, method) {
    var layers = this.stack
    var layer
    var matched = {

    path: [],
    pathAndMethod: [],
    route: false
    

    }

    for (var len = layers.length, i = 0; i < len; i++) {

    layer = layers[i]
    
    if (layer.match(path)) {
      matched.path.push(layer)
    
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer)
    
        // 只有在发现不为空的`methods`以后才会设置`flag`
        if (layer.methods.length) matched.route = true
      }
    }
    

    }

    return matched
    }

    // 以及在routes中有那般的操作
    Router.prototype.routes = Router.prototype.middleware = function () {
    function dispatch(ctx, next) {

    // 如果没有`flag`,直接跳过
    if (!matched.route) return next()
    

    }

    return dispatch
    }

 

npm i -S koa-router
将其余router实例传递进入

能够见见,假设选择了router.routes()来格局来复用中间件,会遍历该实例的富有路由,然后设置prefix
并将修改完的layer出产到眼下的router中。
那么今后将要小心了,在上方其实已经涉及了,LayersetPrefix是东拼西凑的,而不是覆盖的。
use是会操作layer对象的,所以那样的用法会导致前面包车型大巴中间件路径也被改动。
而且若是传入use的中间件已经注册在了koa中就会招致相同的中间件会履行五次(万壹有调用next的话):

const middlewareRouter = new Router()
const routerPage1 = new Router({
  prefix: '/page1'
})

const routerPage2 = new Router({
  prefix: '/page2'
})

middlewareRouter.get('/list/:id', async (ctx, next) => {
  console.log('trigger middleware')
  ctx.body = `hi there.`
  await next()
})

routerPage1.use(middlewareRouter.routes())
routerPage2.use(middlewareRouter.routes())

app.use(middlewareRouter.routes())
app.use(routerPage1.routes())
app.use(routerPage2.routes())

 

就像是上述代码,实际上会有多个难题:

  1. 末尾有效的造访路径为/page2/page1/list/1,因为prefix会拼接而非覆盖
  2. 当大家在中间件中调用next以后,console.log会接连输出三次,因为全部的routes都是动态的,实际上prefix都被涂改为了/page2/page1

自然要小心使用,不要觉得那样的点子能够用来兑现路由的复用

举二个简练例子:

请求的拍卖

以及,终于赶到了最终一步,当贰个请求来通晓后,Router是如何处理的。
一个Router实例能够抛出两当中间件注册到koa上:

app.use(router.routes())
app.use(router.allowedMethods())

 

routes负责重点的逻辑。
allowedMethods顶住提供1个前置的METHOD反省立中学间件。

allowedMethods没什么好说的,正是根据如今伏乞的method开始展览的一部分校验,并再次回到1些错误音讯。
而下边介绍的不在少数主意其实都是为了最后的routes服务:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this

  var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path
    var matched = router.match(path, ctx.method)
    var layerChain, layer, i

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path)
    } else {
      ctx.matched = matched.path
    }

    ctx.router = router

    if (!matched.route) return next()

    var matchedLayers = matched.pathAndMethod
    var 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 = layer.params(path, ctx.captures, ctx.params)
        ctx.routerName = layer.name
        return next()
      })
      return memo.concat(layer.stack)
    }, [])

    return compose(layerChain)(ctx, next)
  };

  dispatch.router = this

  return dispatch
}

 

先是能够看看,koa-router与此同时还提供了贰个小名middleware来兑现平等的功力。
以及函数的调用最后会回来2个中间件函数,那一个函数才是实在被挂在到koa上的。
koa的中间件是彻头彻尾的中间件,不管如何请求都会履行所蕴含的中间件。
就此不建议为了利用prefix而创建多个Router实例,那会造成在koa上挂载八个dispatch用来检查UCR-VL是还是不是合乎规则

进入中间件今后会开始展览U汉兰达L的判定,正是大家上面提到的能够用来做foraward福寿康宁的地方。
11分调用的是router.match艺术,虽说看似赋值是matched.path,而其实在match办法的完成中,里边全体是杰出到的Layer实例:

Router.prototype.match = function (path, method) {
  var layers = this.stack // 这个就是获取的Router实例中所有的中间件对应的layer对象
  var layer
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  }

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i]

    if (layer.match(path)) { // 这里就是一个简单的正则匹配
      matched.path.push(layer)

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // 将有效的中间件推入
        matched.pathAndMethod.push(layer)

        // 判断是否存在METHOD
        if (layer.methods.length) matched.route = true
      }
    }
  }

  return matched
}

// 一个简单的正则匹配
Layer.prototype.match = function (path) {
  return this.regexp.test(path)
}

 

而因而会存在说判断是不是有ctx.matched来进展处理,而不是一向对那些个性举行赋值。
这是因为上面也涉嫌过的,一个koa实例可能会登记八个koa-router实例。
那就造成二个router实例的中间件执行完结后,后续只怕还会有任何的router实例也命中了某些URL,不过如此会确定保证matched壹味是在添加的,而非每一趟都会覆盖。

pathpathAndMethod都是match回到的七个数组,两者的区分在于path回来的是匹配U奥迪Q伍L成功的数目,而pathAndMethod则是匹配UTiguanL且匹配到METHOD的数额

const router1 = new Router()
const router2 = new Router()

router1.post('/', _ => {})

router1.get('/', async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router1, matched length: ${ctx.matched.length}`)
  await next()
})

router2.get('/', async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router2, matched length: ${ctx.matched.length}`)
  await next()
})

app.use(router1.routes())
app.use(router2.routes())

// >  curl http://127.0.0.1:8888/
// => trigger router1, matched length: 2
// => trigger router2, matched length: 3

 

至于中间件的实施,在koa-router中也运用了koa-compose来统一洋葱:

var matchedLayers = matched.pathAndMethod

layerChain = matchedLayers.reduce(function(memo, layer) {
  memo.push(function(ctx, next) {
    ctx.captures = layer.captures(path, ctx.captures)
    ctx.params = layer.params(path, ctx.captures, ctx.params)
    ctx.routerName = layer.name
    return next()
  })
  return memo.concat(layer.stack)
}, [])

return compose(layerChain)(ctx, next)

 

那坨代码会在装有匹配到的中间件从前增进1个ctx属性赋值的中间件操作,也便是说reduce的推行会让洋葱模型对应的中间件函数数量最少X2
layer中大概包括八个中间件,不要忘了middleware,这就是干吗会在reduce中使用concat而非push
因为要在每二个中间件执行此前,修改ctx为此番中间件触发时的一些消息。
席卷匹配到的U昂CoraL参数,以及当前中间件的name等等的消息。

[
  layer1[0], // 第一个register中对应的中间件1
  layer1[1], // 第一个register中对应的中间件2
  layer2[0]  // 第二个register中对应的中间件1
]

// =>

[
  (ctx, next) => {
    ctx.params = layer1.params // 第一个register对应信息的赋值  
    return next()
  },
  layer1[0], // 第一个register中对应的中间件1
  layer1[1], // 第一个register中对应的中间件2
  (ctx, next) => {
    ctx.params = layer2.params // 第二个register对应信息的赋值  
    return next()
  },
  layer2[0]  // 第二个register中对应的中间件1
]

 

routes最后,会调用koa-compose来合并reduce所生成的中间件数组,以及利用了前头在koa-compose中涉及了的第二个可选的参数,用来做洋葱执行到位后最后的回调解和处理理。


// app.js
const Koa = require('Koa');
const koaRrouter = require('koa-router');

const app = new Koa();
// 这里可以直接调用函数也可以使用构造函数,const router = new koaRouter();
const router = koaRouter();

router.get(('/', next) => {
  ctx.body = 'this this the home page';
});

app
  .use(router.routes())
  .use(router.allowMethods());

app.listen(3000);

小记

至此,koa-router的重任就已经完毕了,实现了路由的注册,以及路由的监听处理。
在阅读koa-router的源码进程中感到很吸引:

  • 分西晋码中曾经落到实处的效应,为何在文档中就不曾反映出来吗。
  • 要是文书档案中不写明能够如此来用,为何还要在代码中有相应的完成吗?

多少个最简便易行的举例证明:

  1. 能够通过改动ctx.routerPath来实现forward效益,不过在文书档案中不会告知您
  2. 能够经过router.register(path, ['GET', 'POST'])来连忙的监听多少个METHOD,但是register被标记为了@private

参考资料:

  • koa-router |
    docs
  • path-to-regexp |
    docs

示范代码在仓库中的地点:learning-koa-router

执行 node app.js 打开 http://localhost:3000/ 可以看看

4858美高梅 3

koa-router

这样二个最简单易行的路由就布局好了~

下边针对地点的事例做1些认证:

  1. get 方法表示一层层 HTTP 的动词方法,如 get | put | post | patch |
    delete | del。每1种 HTTP 动词对应1种格局,也能够连接写:

router
  .get('/', (ctx, next) => {
    ctx.body = 'Hello World!';
  })
  .post('/users', (ctx, next) => {...})
  .put('/users/:id', (ctx, next) => {...})
  .del('/users/:id', (ctx, next) => {...})
  .all('/users/:id', (ctx, next) => {...})

注:router.all() 用于表示上述全部的动词方法

  1. 上边代码中,router.get()
    方法的率先个参数是相当的门路,第1个参数对应的函数方法。

再者介绍一下动态路由

// 同时打开网址:http://localhost:3000/user/111
router.get('/user/:id', (ctx, next) => {
  console.log(ctx); // 实际就是 context 对象,但是打印出的结果中并没有看到 params 属性,如果有童鞋知道麻烦指点一二,不胜感激~
  console.log(ctx.params); // { id: '111' }
  console.log(ctx._matchedRoute); // /user/:id
})

使用 ctx._matchedRoute
能够得到万分的原路由地点,倘若那一个路由被取名了也可以透过
ctx. _matchedRouteName 获得路由的名号,比如:

// 同时打开网址:http://localhost:3000/user/111
router.get('user', '/user/:id', (ctx, next) => {
  console.log(ctx._matchedRouteName); // user
})

上面讲一下 koa-router 的 middleware(中间件)

那是法定给出的实例代码:

router.get(
  '/users/:id',
  (ctx, next) => {
    return User.findOne(ctx.params.id).then(function(user) {
      ctx.user = user;
      next();
    });
  },
  ctx => {
    console.log(ctx.user);
    // => { id: 17, name: "Alex" }
  }
);

middleware
类似于一个过滤器,在客户端和应用程序之间的3个拍卖请求和呼应的章程,比如上边的实例代码,千万不要纠结这几个
User 是个怎么着鬼,那是1段伪代码,User.findOne(ctx.params.id)
能够了然为从数据库中异步的获得到钦定 id 的用户消息,然后 .then
获取到那一个用户新闻赋值给 ctx.user,然后实施
next()主意那么下2在那之中间件就足以获得具备用户新闻的
ctx,当然还足现在下传,不过急需留意的是中间件的实施顺序,它很像多少个洋葱,但并不是一层一层外下拨,而是先实施顺序层中
next 从前的代码,然后再从最后一层往上返执行 next
前面包车型的士代码,听着是或不是有点晕+_+,有一张图能够相比明晰的看懂中间件的执行各类:

4858美高梅 4

middleware 的实施种种

const Koa = require('koa');
const app = new Koa();

app.use((ctx, next) => {
  console.log(1);
  next(); // next 不写会报错
  console.log(5);
});

app.use((ctx, next) => {
  console.log(2);
  next();
  console.log(4);
});

app.use((ctx, next) => {
  console.log(3)
  ctx.body = 'Hello World';
});

app.listen(3000);
// 打印出1、2、3、4、5,但是好像会打印两遍12345,这个还没有深入了解为啥

上述简单的施用打字与印刷出1、二、3、四、五,这些实在正是koa中间件控制的着力,3个洋葱结构,从上往下壹层一层进来,再从下往上壹层一层回去,乍一看很复杂,为何不直接1层一层下来就停止吧,就如express/connect1样,我们就假使next就去下贰个中间件,干嘛还要回去?

实则这便是为了消除复杂应用中屡屡的回调而规划的级联代码,并不间接把控制权完全交由下1个中间件,而是碰着next去下3个中间件,等下部都进行完了,还会履行next以下的剧情

缓解频仍的回调,那又有哪些依照吗?举个简单的事例,要是咱们须求精通通过中间件的光阴,大家选用koa能够轻松地写出来,可是使用express呢,能够去看下express
reponse-time的源码,它就只好通过监听header被write
out的时候然后触发回调函数总结时间,但是koa完全不用写callback,大家只需求在next后边加几行代码就化解了(直接使用.then()都得以)

只怕照旧会不太精晓这么做的功利,我觉得那种思路在骨子里运用中还会时不时采纳,前边赶上再结合实际应用场景来细读一下~

未完待续…

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有