国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

koa源碼閱讀[2]-koa-router

oneasp / 3228人閱讀

摘要:第三篇,有關(guān)生態(tài)中比較重要的一個(gè)中間件第一篇源碼閱讀第二篇源碼閱讀與是什么首先,因?yàn)槭且粋€(gè)管理中間件的平臺(tái),而注冊(cè)一個(gè)中間件使用來(lái)執(zhí)行。這里寫(xiě)入的多個(gè)中間件都是針對(duì)該生效的。

第三篇,有關(guān)koa生態(tài)中比較重要的一個(gè)中間件:koa-router

第一篇:koa源碼閱讀-0  
第二篇:koa源碼閱讀-1-koa與koa-compose
koa-router是什么

首先,因?yàn)閗oa是一個(gè)管理中間件的平臺(tái),而注冊(cè)一個(gè)中間件使用use來(lái)執(zhí)行。
無(wú)論是什么請(qǐng)求,都會(huì)將所有的中間件執(zhí)行一遍(如果沒(méi)有中途結(jié)束的話)
所以,這就會(huì)讓開(kāi)發(fā)者很困擾,如果我們要做路由該怎么寫(xiě)邏輯?

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

誠(chéng)然,這樣是一個(gè)簡(jiǎn)單的方法,但是必然不適用于大型項(xiàng)目,數(shù)十個(gè)接口通過(guò)一個(gè)switch來(lái)控制未免太繁瑣了。
更何況請(qǐng)求可能只支持get或者post,以及這種方式并不能很好的支持URL中包含參數(shù)的請(qǐng)求/info/:uid。
express中是不會(huì)有這樣的問(wèn)題的,自身已經(jīng)提供了get、post等之類的與METHOD同名的函數(shù)用來(lái)注冊(cè)回調(diào):
express

const express = require("express")
const app = express()

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

但是koa做了很多的精簡(jiǎn),將很多邏輯都拆分出來(lái)作為獨(dú)立的中間件來(lái)存在。
所以導(dǎo)致很多express項(xiàng)目遷移為koa時(shí),需要額外的安裝一些中間件,koa-router應(yīng)該說(shuō)是最常用的一個(gè)。
所以在koa中則需要額外的安裝koa-router來(lái)實(shí)現(xiàn)類似的路由功能:
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())

看起來(lái)代碼確實(shí)多了一些,畢竟將很多邏輯都從框架內(nèi)部轉(zhuǎn)移到了中間件中來(lái)處理。
也算是為了保持一個(gè)簡(jiǎn)練的koa框架所取舍的一些東西吧。
koa-router的邏輯確實(shí)要比koa的復(fù)雜一些,可以將koa想象為一個(gè)市場(chǎng),而koa-router則是其中一個(gè)攤位
koa僅需要保證市場(chǎng)的穩(wěn)定運(yùn)行,而真正和顧客打交道的確是在里邊擺攤的koa-router

koa-router的大致結(jié)構(gòu)

koa-router的結(jié)構(gòu)并不是很復(fù)雜,也就分了兩個(gè)文件:

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

layer主要是針對(duì)一些信息的封裝,主要路基由router提供:

File Description
layer 信息存儲(chǔ):路徑、METHOD、路徑對(duì)應(yīng)的正則匹配、路徑中的參數(shù)、路徑對(duì)應(yīng)的中間件
router 主要邏輯:對(duì)外暴露注冊(cè)路由的函數(shù)、提供處理路由的中間件,檢查請(qǐng)求的URL并調(diào)用對(duì)應(yīng)的layer中的路由處理
koa-router的運(yùn)行流程

可以拿上邊所拋出的基本例子來(lái)說(shuō)明koa-router是怎樣的一個(gè)執(zhí)行流程:

const router = new Router() // 實(shí)例化一個(gè)Router對(duì)象

// 注冊(cè)一個(gè)路由的監(jiān)聽(tīng)
router.get("/", async ctx => {
  ctx.body = "hi there."
})

app
  .use(router.routes()) // 將該Router對(duì)象的中間件注冊(cè)到Koa實(shí)例上,后續(xù)請(qǐng)求的主要處理邏輯
  .use(router.allowedMethods()) // 添加針對(duì)OPTIONS的響應(yīng)處理,以及一些METHOD不支持的處理
創(chuàng)建實(shí)例時(shí)的一些事情

首先,在koa-router實(shí)例化的時(shí)候,是可以傳遞一個(gè)配置項(xiàng)參數(shù)作為初始化的配置信息的。
然而這個(gè)配置項(xiàng)在readme中只是簡(jiǎn)單的被描述為:

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

告訴我們可以添加一個(gè)Router注冊(cè)時(shí)的前綴,也就是說(shuō)如果按照模塊化分,可以不必在每個(gè)路徑匹配的前端都添加巨長(zhǎng)的前綴:

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. 不過(guò)要記住,如果prefix/結(jié)尾,則路由的注冊(cè)就可以省去前綴的/了,不然會(huì)出現(xiàn)/重復(fù)的情況

實(shí)例化Router時(shí)的代碼:

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

可見(jiàn)的只有一個(gè)methods的賦值,但是在查看了其他源碼后,發(fā)現(xiàn)除了prefix還有一些參數(shù)是實(shí)例化時(shí)傳遞進(jìn)來(lái)的,但是不太清楚為什么文檔中沒(méi)有提到:

Param Type Default Description
sensitive Boolean false 是否嚴(yán)格匹配大小寫(xiě)
strict Boolean false 如果設(shè)置為false則匹配路徑后邊的/是可選的
methods Array[String] ["HEAD","OPTIONS","GET","PUT","PATCH","POST","DELETE"] 設(shè)置路由可以支持的METHOD
routerPath String null
sensitive

如果設(shè)置了sensitive,則會(huì)以更嚴(yán)格的匹配規(guī)則來(lái)監(jiān)聽(tīng)路由,不會(huì)忽略URL中的大小寫(xiě),完全按照注冊(cè)時(shí)的來(lái)匹配:

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

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

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

strictsensitive功能類似,也是用來(lái)設(shè)置讓路徑的匹配變得更加嚴(yán)格,在默認(rèn)情況下,路徑結(jié)尾處的/是可選的,如果開(kāi)啟該參數(shù)以后,如果在注冊(cè)路由時(shí)尾部沒(méi)有添加/,則匹配的路由也一定不能夠添加/結(jié)尾:

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
methods

methods配置項(xiàng)存在的意義在于,如果我們有一個(gè)接口需要同時(shí)支持GETPOSTrouter.getrouter.post這樣的寫(xiě)法必然是丑陋的。
所以我們可能會(huì)想到使用router.all來(lái)簡(jiǎn)化操作:

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!

這簡(jiǎn)直是太完美了,可以很輕松的實(shí)現(xiàn)我們的需求,但是如果再多實(shí)驗(yàn)一些其他的methods以后,尷尬的事情就發(fā)生了:

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

這顯然不是符合我們預(yù)期的結(jié)果,所以,在這種情況下,基于目前koa-router需要進(jìn)行如下修改來(lái)實(shí)現(xiàn)我們想要的功能:

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) => {
  // 理想情況下,這些判斷應(yīng)該交由中間件來(lái)完成
  if (!~methods.indexOf(ctx.method)) {
    return await next()
  }

  ctx.body = "pong!"
})

這樣的兩處修改,就可以實(shí)現(xiàn)我們所期望的功能:

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

我個(gè)人覺(jué)得這是allowedMethods實(shí)現(xiàn)的一個(gè)邏輯問(wèn)題,不過(guò)也許是我沒(méi)有g(shù)et到作者的點(diǎn),allowedMethods中比較關(guān)鍵的一些源碼:

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

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

      // 如果進(jìn)行了ctx.body賦值,必然不會(huì)執(zhí)行后續(xù)的邏輯
      // 所以就需要我們自己在中間件中進(jìn)行判斷
      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是作為一個(gè)后置的中間件存在的,因?yàn)樵诜祷氐暮瘮?shù)中先調(diào)用了next,其次才是針對(duì)METHOD的判斷,而這樣帶來(lái)的一個(gè)后果就是,如果我們?cè)诼酚傻幕卣{(diào)中進(jìn)行類似ctx.body = XXX的操作,實(shí)際上會(huì)修改本次請(qǐng)求的status值的,使之并不會(huì)成為404,而無(wú)法正確的觸發(fā)METHOD檢查的邏輯。
想要正確的觸發(fā)METHOD邏輯,就需要自己在路由監(jiān)聽(tīng)中手動(dòng)判斷ctx.method是否為我們想要的,然后在跳過(guò)當(dāng)前中間件的執(zhí)行。
而這一判斷的步驟實(shí)際上與allowedMethods中間件中的!~implemented.indexOf(ctx.method)邏輯完全是重復(fù)的,不太清楚koa-router為什么會(huì)這么處理。

當(dāng)然,allowedMethods是不能夠作為一個(gè)前置中間件來(lái)存在的,因?yàn)橐粋€(gè)Koa中可能會(huì)掛在多個(gè)RouterRouter之間的配置可能不盡相同,不能保證所有的Router都和當(dāng)前Router可處理的METHOD是一樣的。
所以,個(gè)人感覺(jué)methods參數(shù)的存在意義并不是很大。。

routerPath

這個(gè)參數(shù)的存在。。感覺(jué)會(huì)導(dǎo)致一些很詭異的情況。
這就要說(shuō)到在注冊(cè)完中間件以后的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)
    // 如果匹配到則執(zhí)行對(duì)應(yīng)的中間件
    // 執(zhí)行后續(xù)操作
  }
  return dispatch
}

因?yàn)槲覀儗?shí)際上向koa注冊(cè)的是這樣的一個(gè)中間件,在每次請(qǐng)求發(fā)送過(guò)來(lái)時(shí),都會(huì)執(zhí)行dispatch,而在dispatch中判斷是否命中某個(gè)router時(shí),則會(huì)用到這個(gè)配置項(xiàng),這樣的一個(gè)表達(dá)式:router.opts.routerPath || ctx.routerPath || ctx.path,router代表當(dāng)前Router實(shí)例,也就是說(shuō),如果我們?cè)趯?shí)例化一個(gè)Router的時(shí)候,如果填寫(xiě)了routerPath,這會(huì)導(dǎo)致無(wú)論任何請(qǐng)求,都會(huì)優(yōu)先使用routerPath來(lái)作為路由檢查:

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"))

如果有這樣的代碼,無(wú)論請(qǐng)求什么URL,都會(huì)認(rèn)為是/index來(lái)進(jìn)行匹配:

> 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!
巧用routerPath實(shí)現(xiàn)轉(zhuǎn)發(fā)功能

同樣的,這個(gè)短路運(yùn)算符一共有三個(gè)表達(dá)式,第二個(gè)的ctx則是當(dāng)前請(qǐng)求的上下文,也就是說(shuō),如果我們有一個(gè)早于routes執(zhí)行的中間件,也可以進(jìn)行賦值來(lái)修改路由判斷所使用的URL

const router = new Router()

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

app.use((ctx, next) => {
  ctx.routerPath = "/index" // 手動(dòng)改變r(jià)outerPath
  next()
})
app.use(router.routes())

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

這樣的代碼也能夠?qū)崿F(xiàn)相同的效果。
實(shí)例化中傳入的routerPath讓人捉摸不透,但是在中間件中改變routerPath的這個(gè)還是可以找到合適的場(chǎng)景,這個(gè)可以簡(jiǎn)單的理解為轉(zhuǎn)發(fā)的一種實(shí)現(xiàn),轉(zhuǎn)發(fā)的過(guò)程是對(duì)客戶端不可見(jiàn)的,在客戶端看來(lái)依然訪問(wèn)的是最初的URL,但是在中間件中改變ctx.routerPath可以很輕易的使路由匹配到我們想轉(zhuǎn)發(fā)的地方去

// 老版本的登錄邏輯處理
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") { // 匹配到舊版請(qǐng)求,轉(zhuǎn)發(fā)到新版
    ctx.routerPath = "/login-v2" // 手動(dòng)改變r(jià)outerPath
  }
  next()
})
app.use(router.routes())

這樣就實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的轉(zhuǎn)發(fā):

> curl -X POST http://127.0.0.1:8888/login
new login logic!
注冊(cè)路由的監(jiān)聽(tīng)

上述全部是關(guān)于實(shí)例化Router時(shí)的一些操作,下面就來(lái)說(shuō)一下使用最多的,注冊(cè)路由相關(guān)的操作,最熟悉的必然就是router.get,router.post這些的操作了。
但實(shí)際上這些也只是一個(gè)快捷方式罷了,在內(nèi)部調(diào)用了來(lái)自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
}

該方法在注釋中標(biāo)為了 private 但是其中的一些參數(shù)在代碼中各種地方都沒(méi)有體現(xiàn)出來(lái),鬼知道為什么會(huì)留著那些參數(shù),但既然存在,就需要了解他是干什么的
這個(gè)是路由監(jiān)聽(tīng)的基礎(chǔ)方法,函數(shù)簽名大致如下:

Param Type Default Description
path String/Array[String] - 一個(gè)或者多個(gè)的路徑
methods Array[String] - 該路由需要監(jiān)聽(tīng)哪幾個(gè)METHOD
middleware Function/Array[Function] - 由函數(shù)組成的中間件數(shù)組,路由實(shí)際調(diào)用的回調(diào)函數(shù)
opts Object {} 一些注冊(cè)路由時(shí)的配置參數(shù),上邊提到的strict、sensitiveprefix在這里都有體現(xiàn)

可以看到,函數(shù)大致就是實(shí)現(xiàn)了這樣的流程:

檢查path是否為數(shù)組,如果是,遍歷item進(jìn)行調(diào)用自身

實(shí)例化一個(gè)Layer對(duì)象,設(shè)置一些初始化參數(shù)

設(shè)置針對(duì)某些參數(shù)的中間件處理(如果有的話)

將實(shí)例化后的對(duì)象放入stack中存儲(chǔ)

所以在介紹這幾個(gè)參數(shù)之前,簡(jiǎn)單的描述一下Layer的構(gòu)造函數(shù)是很有必要的:

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是負(fù)責(zé)存儲(chǔ)路由監(jiān)聽(tīng)的信息的,每次注冊(cè)路由時(shí)的URL,URL生成的正則表達(dá)式,該URL中存在的參數(shù),以及路由對(duì)應(yīng)的中間件。
統(tǒng)統(tǒng)交由Layer來(lái)存儲(chǔ),重點(diǎn)需要關(guān)注的是實(shí)例化過(guò)程中的那幾個(gè)數(shù)組參數(shù):

methods

paramNames

stack

methods存儲(chǔ)的是該路由監(jiān)聽(tīng)對(duì)應(yīng)的有效METHOD,并會(huì)在實(shí)例化的過(guò)程中針對(duì)METHOD進(jìn)行大小寫(xiě)的轉(zhuǎn)換。
paramNames因?yàn)橛玫牟寮?wèn)題,看起來(lái)不那么清晰,實(shí)際上在pathToRegExp內(nèi)部會(huì)對(duì)paramNames這個(gè)數(shù)組進(jìn)行push的操作,這么看可能會(huì)舒服一些pathToRegExp(path, &this.paramNames, this.opts),在拼接hash結(jié)構(gòu)的路徑參數(shù)時(shí)會(huì)用到這個(gè)數(shù)組
stack存儲(chǔ)的是該路由監(jiān)聽(tīng)對(duì)應(yīng)的中間件函數(shù),router.middleware部分邏輯會(huì)依賴于這個(gè)數(shù)組

path

在函數(shù)頭部的處理邏輯,主要是為了支持多路徑的同時(shí)注冊(cè),如果發(fā)現(xiàn)第一個(gè)path參數(shù)為數(shù)組后,則會(huì)遍歷path參數(shù)進(jìn)行調(diào)用自身。
所以針對(duì)多個(gè)URL的相同路由可以這樣來(lái)處理:

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

這樣完全是一個(gè)有效的設(shè)置:

> 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.
methods

而關(guān)于methods參數(shù),則默認(rèn)認(rèn)為是一個(gè)數(shù)組,即使是只監(jiān)聽(tīng)一個(gè)METHOD也需要傳入一個(gè)數(shù)組作為參數(shù),如果是空數(shù)組的話,即使URL匹配,也會(huì)直接跳過(guò),執(zhí)行下一個(gè)中間件,這個(gè)在后續(xù)的router.routes中會(huì)提到

middleware

middleware則是一次路由真正執(zhí)行的事情了,依舊是符合koa標(biāo)準(zhǔn)的中間件,可以有多個(gè),按照洋蔥模型的方式來(lái)執(zhí)行。
這也是koa-router中最重要的地方,能夠讓我們的一些中間件只在特定的URL時(shí)執(zhí)行。
這里寫(xiě)入的多個(gè)中間件都是針對(duì)該URL生效的。

P.S. 在koa-router中,還提供了一個(gè)方法,叫做router.use,這個(gè)會(huì)注冊(cè)一個(gè)基于router實(shí)例的中間件

opts

opts則是用來(lái)設(shè)置一些路由生成的配置規(guī)則的,包括如下幾個(gè)可選的參數(shù):

Param Type Default Description
name String - 設(shè)置該路由所對(duì)應(yīng)的name,命名router
prefix String - __非常雞肋的參數(shù),完全沒(méi)有卵用__,看似會(huì)設(shè)置路由的前綴,實(shí)際上沒(méi)有一點(diǎn)兒用
sensitive Boolean false 是否嚴(yán)格匹配大小寫(xiě),覆蓋實(shí)例化Router中的配置
strict Boolean false 是否嚴(yán)格匹配大小寫(xiě),如果設(shè)置為false則匹配路徑后邊的/是可選的
end Boolean true 路徑匹配是否為完整URL的結(jié)尾
ignoreCaptures Boolean - 是否忽略路由匹配正則結(jié)果中的捕獲組
name

首先是name,主要是用于這幾個(gè)地方:

拋出異常時(shí)更方便的定位

可以通過(guò)router.url()、router.route()獲取到對(duì)應(yīng)的router信息

在中間件執(zhí)行的時(shí)候,name會(huì)被塞到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`
}

如果多個(gè)router使用相同的命名,則通過(guò)router.url調(diào)用返回最先注冊(cè)的那一個(gè):

// route用來(lái)獲取命名路由
Router.prototype.route = function (name) {
  var routes = this.stack

  for (var len = routes.length, i=0; i
跑題說(shuō)下router.url的那些事兒

如果在項(xiàng)目中,想要針對(duì)某些URL進(jìn)行跳轉(zhuǎn),使用router.url來(lái)生成path則是一個(gè)不錯(cuò)的選擇:

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實(shí)際上調(diào)用的是Layer實(shí)例的url方法,該方法主要是用來(lái)處理生成時(shí)傳入的一些參數(shù)。
源碼地址:layer.js#L116
函數(shù)接收兩個(gè)參數(shù),paramsoptions,因?yàn)楸旧?b>Layer實(shí)例是存儲(chǔ)了對(duì)應(yīng)的path之類的信息,所以params就是存儲(chǔ)的在路徑中的一些參數(shù)的替換,options在目前的代碼中,僅僅存在一個(gè)query字段,用來(lái)拼接search后邊的數(shù)據(jù):

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
    }
  })
)

上述的調(diào)用方式都是有效的,在源碼中有對(duì)應(yīng)的處理,首先是針對(duì)多參數(shù)的判斷,如果params不是一個(gè)object,則會(huì)認(rèn)為是通過(guò)layer.url(參數(shù), 參數(shù), 參數(shù), opts)這種方式來(lái)調(diào)用的。
將其轉(zhuǎn)換為layer.url([參數(shù), 參數(shù)], opts)形式的。
這時(shí)候的邏輯僅需要處理三種情況了:

數(shù)組形式的參數(shù)替換

hash形式的參數(shù)替換

無(wú)參數(shù)

這個(gè)參數(shù)替換指的是,一個(gè)URL會(huì)通過(guò)一個(gè)第三方的庫(kù)用來(lái)處理鏈接中的參數(shù)部分,也就是/:XXX的這一部分,然后傳入一個(gè)hash實(shí)現(xiàn)類似模版替換的操作:

// 可以簡(jiǎn)單的認(rèn)為是這樣的操作:
let hash = { id: 123, name: "Niko" }
"/list/:id/:name".replace(/(?:/:)(w+)/g, (_, $1) => `/${hash[$1]}`)

然后layer.url的處理就是為了將各種參數(shù)生成類似hash這樣的結(jié)構(gòu),最終替換hash獲取完整的URL。

prefix

上邊實(shí)例化Layer的過(guò)程中看似是opts.prefix的權(quán)重更高,但是緊接著在下邊就有了一個(gè)判斷邏輯進(jìn)行調(diào)用setPrefix重新賦值,在翻遍了整個(gè)的源碼后發(fā)現(xiàn),這樣唯一的一個(gè)區(qū)別就在于,會(huì)有一條debug應(yīng)用的是注冊(cè)router時(shí)傳入的prefix,而其他地方都會(huì)被實(shí)例化Router時(shí)的prefix所覆蓋。

而且如果想要路由正確的應(yīng)用prefix,則需要調(diào)用setPrefix,因?yàn)樵?b>Layer實(shí)例化的過(guò)程中關(guān)于path的存儲(chǔ)就是來(lái)自遠(yuǎn)傳入的path參數(shù)。
而應(yīng)用prefix前綴則需要手動(dòng)觸發(fā)setPrefix

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

// 只有調(diào)用setPrefix才會(huì)應(yīng)用前綴
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
}

這個(gè)在暴露給使用者的幾個(gè)方法中都有體現(xiàn),類似的get、set以及use。
當(dāng)然在文檔中也提供了可以直接設(shè)置所有router前綴的方法,router.prefix
文檔中就這樣簡(jiǎn)單的告訴你可以設(shè)置前綴,prefix在內(nèi)部會(huì)循環(huán)調(diào)用所有的layer.setPrefix

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

但是在翻看了layer.setPrefix源碼后才發(fā)現(xiàn)這里其實(shí)是含有一個(gè)暗坑的。
因?yàn)?b>setPrefix的實(shí)現(xiàn)是拿到prefix參數(shù),拼接到當(dāng)前path的頭部。
這樣就會(huì)帶來(lái)一個(gè)問(wèn)題,如果我們多次調(diào)用setPrefix會(huì)導(dǎo)致多次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方法會(huì)疊加前綴,而不是覆蓋前綴
sensitive與strict

這倆參數(shù)沒(méi)啥好說(shuō)的,就是會(huì)覆蓋實(shí)例化Router時(shí)所傳遞的那倆參數(shù),效果都一致。

end

end是一個(gè)很有趣的參數(shù),這個(gè)在koa-router中引用的其他模塊中有體現(xiàn)到,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可以簡(jiǎn)單地理解為是正則中的$,也就是匹配的結(jié)尾。
看代碼的邏輯,大致就是,如果設(shè)置了end: true,則無(wú)論任何情況都會(huì)在最后添加$表示匹配的結(jié)尾。
而如果end: false,則只有在同時(shí)設(shè)置了strict: false或者isEndDelimited: false時(shí)才會(huì)觸發(fā)。
所以我們可以通過(guò)這兩個(gè)參數(shù)來(lái)實(shí)現(xiàn)URL的模糊匹配:

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

也就是說(shuō)上述代碼最后生成的用于匹配路由的正則表達(dá)式大概是這樣的:

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

// 可以通過(guò)下述代碼獲取到正則
require("path-to-regexp").tokensToRegExp("/list/", {end: false, strict: true})

結(jié)尾的$是可選的,這就會(huì)導(dǎo)致,我們只要發(fā)送任何開(kāi)頭為/list的請(qǐng)求都會(huì)被這個(gè)中間件所獲取到。

ignoreCaptures

ignoreCaptures參數(shù)用來(lái)設(shè)置是否需要返回URL中匹配的路徑參數(shù)給中間件。
而如果設(shè)置了ignoreCaptures以后這兩個(gè)參數(shù)就會(huì)變?yōu)榭諏?duì)象:

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

這個(gè)是在中間件執(zhí)行期間調(diào)用了來(lái)自layer的兩個(gè)方法獲取的。
首先調(diào)用captures獲取所有的參數(shù),如果設(shè)置了ignoreCaptures則會(huì)導(dǎo)致直接返回空數(shù)組。
然后調(diào)用params將注冊(cè)路由時(shí)所生成的所有參數(shù)以及參數(shù)們實(shí)際的值傳了進(jìn)去,然后生成一個(gè)完整的hash注入到ctx對(duì)象中:

// 中間件的邏輯
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
// { age: 18, name: "Niko" }
router.param的作用

上述是關(guān)于注冊(cè)路由時(shí)的一些參數(shù)描述,可以看到在register中實(shí)例化Layer對(duì)象后并沒(méi)有直接將其放入stack中,而是執(zhí)行了這樣的一個(gè)操作以后才將其推入stack

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

stack.push(route) // 裝載

這里是用作添加針對(duì)某個(gè)URL參數(shù)的中間件處理的,與router.param兩者關(guān)聯(lián)性很強(qiáng):

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

兩者操作類似,前者用于對(duì)新增的路由監(jiān)聽(tīng)添加所有的param中間件,而后者用于針對(duì)現(xiàn)有的所有路由添加param中間件。
因?yàn)樵?b>router.param中有著this.params[param] = XXX的賦值操作。
這樣在后續(xù)的新增路由監(jiān)聽(tīng)中,直接循環(huán)this.params就可以拿到所有的中間件了。

router.param的操作在文檔中也有介紹,文檔地址
大致就是可以用來(lái)做一些參數(shù)校驗(yàn)之類的操作,不過(guò)因?yàn)樵?b>layer.param中有了一些特殊的處理,所以我們不必?fù)?dān)心param的執(zhí)行順序,layer會(huì)保證param一定是早于依賴這個(gè)參數(shù)的中間件執(zhí)行的:

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
最常用的get/post之類的快捷方式

以及說(shuō)完了上邊的基礎(chǔ)方法register,我們可以來(lái)看下暴露給開(kāi)發(fā)者的幾個(gè)router.verb方法:

// get|put|post|patch|delete|del
// 循環(huán)注冊(cè)多個(gè)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"] // 以及最后的一個(gè)別名處理,因?yàn)閐el并不是有效的METHOD

令人失望的是,verb方法將大量的opts參數(shù)都砍掉了,默認(rèn)只留下了一個(gè)name字段。
只是很簡(jiǎn)單的處理了一下命名name路由相關(guān)的邏輯,然后進(jìn)行調(diào)用register完成操作。

router.use-Router內(nèi)部的中間件

以及上文中也提到的router.use,可以用來(lái)注冊(cè)一個(gè)中間件,使用use注冊(cè)中間件分為兩種情況:

普通的中間件函數(shù)

將現(xiàn)有的router實(shí)例作為中間件傳入

普通的use

這里是use方法的關(guān)鍵代碼:

Router.prototype.use = function () {
  var router = this
  middleware.forEach(function (m) {
    if (m.router) { // 這里是通過(guò)`router.routes()`傳遞進(jìn)來(lái)的
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path)
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) // 調(diào)用`use`的Router實(shí)例的`prefix`
        router.stack.push(nestedLayer)
      })

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

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

  dispatch.router = this // 將router實(shí)例賦值給了返回的函數(shù)

  return dispatch
}

第一種是比較常規(guī)的方式,傳入一個(gè)函數(shù),一個(gè)可選的path,來(lái)進(jìn)行注冊(cè)中間件。
不過(guò)有一點(diǎn)要注意的是,.use("path")這樣的用法,中間件不能獨(dú)立存在,必須要有一個(gè)可以與之路徑相匹配的路由監(jiān)聽(tīng)存在:

router.use("/list", ctx => {
  // 如果只有這么一個(gè)中間件,無(wú)論如何也不會(huì)執(zhí)行的
})

// 必須要存在相同路徑的`register`回調(diào)
router.get("/list", ctx => { })

app.use(router.routes())

原因是這樣的:

.use.get都是基于.register來(lái)實(shí)現(xiàn)的,但是.usemethods參數(shù)中傳遞的是一個(gè)空數(shù)組

在一個(gè)路徑被匹配到時(shí),會(huì)將所有匹配到的中間件取出來(lái),然后檢查對(duì)應(yīng)的methods,如果length !== 0則會(huì)對(duì)當(dāng)前匹配組標(biāo)記一個(gè)flag

在執(zhí)行中間件之前會(huì)先判斷有沒(méi)有這個(gè)flag,如果沒(méi)有則說(shuō)明該路徑所有的中間件都沒(méi)有設(shè)置METHOD,則會(huì)直接跳過(guò)進(jìn)入其他流程(比如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)

        // 只有在發(fā)現(xiàn)不為空的`methods`以后才會(huì)設(shè)置`flag`
        if (layer.methods.length) matched.route = true
      }
    }
  }

  return matched
}

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

    // 如果沒(méi)有`flag`,直接跳過(guò)
    if (!matched.route) return next()
  }

  return dispatch
}
將其他router實(shí)例傳遞進(jìn)來(lái)

可以看到,如果選擇了router.routes()來(lái)方式來(lái)復(fù)用中間件,會(huì)遍歷該實(shí)例的所有路由,然后設(shè)置prefix
并將修改完的layer推出到當(dāng)前的router中。
那么現(xiàn)在就要注意了,在上邊其實(shí)已經(jīng)提到了,LayersetPrefix是拼接的,而不是覆蓋的。
use是會(huì)操作layer對(duì)象的,所以這樣的用法會(huì)導(dǎo)致之前的中間件路徑也被修改。
而且如果傳入use的中間件已經(jīng)注冊(cè)在了koa中就會(huì)導(dǎo)致相同的中間件會(huì)執(zhí)行兩次(如果有調(diào)用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())

就像上述代碼,實(shí)際上會(huì)有兩個(gè)問(wèn)題:

最終有效的訪問(wèn)路徑為/page2/page1/list/1,因?yàn)?b>prefix會(huì)拼接而非覆蓋

當(dāng)我們?cè)谥虚g件中調(diào)用next以后,console.log會(huì)連續(xù)輸出三次,因?yàn)樗械?b>routes都是動(dòng)態(tài)的,實(shí)際上prefix都被修改為了/page2/page1

一定要小心使用,不要認(rèn)為這樣的方式可以用來(lái)實(shí)現(xiàn)路由的復(fù)用

請(qǐng)求的處理

以及,終于來(lái)到了最后一步,當(dāng)一個(gè)請(qǐng)求來(lái)了以后,Router是怎樣處理的。
一個(gè)Router實(shí)例可以拋出兩個(gè)中間件注冊(cè)到koa上:

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

routes負(fù)責(zé)主要的邏輯。
allowedMethods負(fù)責(zé)提供一個(gè)后置的METHOD檢查中間件。

allowedMethods沒(méi)什么好說(shuō)的,就是根據(jù)當(dāng)前請(qǐng)求的method進(jìn)行的一些校驗(yàn),并返回一些錯(cuò)誤信息。
而上邊介紹的很多方法其實(shí)都是為了最終的routes服務(wù):

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同時(shí)還提供了一個(gè)別名middleware來(lái)實(shí)現(xiàn)相同的功能。
以及函數(shù)的調(diào)用最終會(huì)返回一個(gè)中間件函數(shù),這個(gè)函數(shù)才是真正被掛在到koa上的。
koa的中間件是純粹的中間件,不管什么請(qǐng)求都會(huì)執(zhí)行所包含的中間件。
所以不建議為了使用prefix而創(chuàng)建多個(gè)Router實(shí)例,這會(huì)導(dǎo)致在koa上掛載多個(gè)dispatch用來(lái)檢查URL是否符合規(guī)則

進(jìn)入中間件以后會(huì)進(jìn)行URL的判斷,就是我們上邊提到的可以用來(lái)做foraward實(shí)現(xiàn)的地方。
匹配調(diào)用的是router.match方法,雖說(shuō)看似賦值是matched.path,而實(shí)際上在match方法的實(shí)現(xiàn)中,里邊全部是匹配到的Layer實(shí)例:

Router.prototype.match = function (path, method) {
  var layers = this.stack // 這個(gè)就是獲取的Router實(shí)例中所有的中間件對(duì)應(yīng)的layer對(duì)象
  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)) { // 這里就是一個(gè)簡(jiǎn)單的正則匹配
      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
}

// 一個(gè)簡(jiǎn)單的正則匹配
Layer.prototype.match = function (path) {
  return this.regexp.test(path)
}

而之所以會(huì)存在說(shuō)判斷是否有ctx.matched來(lái)進(jìn)行處理,而不是直接對(duì)這個(gè)屬性進(jìn)行賦值。
這是因?yàn)樯线呉蔡岬竭^(guò)的,一個(gè)koa實(shí)例可能會(huì)注冊(cè)多個(gè)koa-router實(shí)例。
這就導(dǎo)致一個(gè)router實(shí)例的中間件執(zhí)行完畢后,后續(xù)可能還會(huì)有其他的router實(shí)例也命中了某個(gè)URL,但是這樣會(huì)保證matched始終是在累加的,而非每次都會(huì)覆蓋。

pathpathAndMethod都是match返回的兩個(gè)數(shù)組,兩者的區(qū)別在于path返回的是匹配URL成功的數(shù)據(jù),而pathAndMethod則是匹配URL且匹配到METHOD的數(shù)據(jù)
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

關(guān)于中間件的執(zhí)行,在koa-router中也使用了koa-compose來(lái)合并洋蔥:

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)

這坨代碼會(huì)在所有匹配到的中間件之前添加一個(gè)ctx屬性賦值的中間件操作,也就是說(shuō)reduce的執(zhí)行會(huì)讓洋蔥模型對(duì)應(yīng)的中間件函數(shù)數(shù)量至少X2。
layer中可能包含多個(gè)中間件,不要忘了middleware,這就是為什么會(huì)在reduce中使用concat而非push
因?yàn)橐诿恳粋€(gè)中間件執(zhí)行之前,修改ctx為本次中間件觸發(fā)時(shí)的一些信息。
包括匹配到的URL參數(shù),以及當(dāng)前中間件的name之類的信息。

[
  layer1[0], // 第一個(gè)register中對(duì)應(yīng)的中間件1
  layer1[1], // 第一個(gè)register中對(duì)應(yīng)的中間件2
  layer2[0]  // 第二個(gè)register中對(duì)應(yīng)的中間件1
]

// =>

[
  (ctx, next) => {
    ctx.params = layer1.params // 第一個(gè)register對(duì)應(yīng)信息的賦值  
    return next()
  },
  layer1[0], // 第一個(gè)register中對(duì)應(yīng)的中間件1
  layer1[1], // 第一個(gè)register中對(duì)應(yīng)的中間件2
  (ctx, next) => {
    ctx.params = layer2.params // 第二個(gè)register對(duì)應(yīng)信息的賦值  
    return next()
  },
  layer2[0]  // 第二個(gè)register中對(duì)應(yīng)的中間件1
]

routes最后,會(huì)調(diào)用koa-compose來(lái)合并reduce所生成的中間件數(shù)組,以及用到了之前在koa-compose中提到了的第二個(gè)可選的參數(shù),用來(lái)做洋蔥執(zhí)行完成后最終的回調(diào)處理。

小記

至此,koa-router的使命就已經(jīng)完成了,實(shí)現(xiàn)了路由的注冊(cè),以及路由的監(jiān)聽(tīng)處理。
在閱讀koa-router的源碼過(guò)程中感到很迷惑:

明明代碼中已經(jīng)實(shí)現(xiàn)的功能,為什么在文檔中就沒(méi)有體現(xiàn)出來(lái)呢。

如果文檔中不寫(xiě)明可以這樣來(lái)用,為什么還要在代碼中有對(duì)應(yīng)的實(shí)現(xiàn)呢?

兩個(gè)最簡(jiǎn)單的舉證:

可以通過(guò)修改ctx.routerPath來(lái)實(shí)現(xiàn)forward功能,但是在文檔中不會(huì)告訴你

可以通過(guò)router.register(path, ["GET", "POST"])來(lái)快速的監(jiān)聽(tīng)多個(gè)METHOD,但是register被標(biāo)記為了@private

參考資料:

koa-router | docs

path-to-regexp | docs

示例代碼在倉(cāng)庫(kù)中的位置:learning-koa-router

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/96767.html

相關(guān)文章

  • 教你從寫(xiě)一個(gè)迷你koa-router到閱讀koa-router源碼

    摘要:本打算教一步步實(shí)現(xiàn),因?yàn)橐忉尩奶嗔?,所以先?jiǎn)化成版本,從實(shí)現(xiàn)部分功能到閱讀源碼,希望能讓你好理解一些。 本打算教一步步實(shí)現(xiàn)koa-router,因?yàn)橐忉尩奶嗔?,所以先?jiǎn)化成mini版本,從實(shí)現(xiàn)部分功能到閱讀源碼,希望能讓你好理解一些。希望你之前有讀過(guò)koa源碼,沒(méi)有的話,給你鏈接 最核心需求-路由匹配 router最重要的就是路由匹配,我們就從最核心的入手 router.get...

    yzzz 評(píng)論0 收藏0
  • Koa-router 優(yōu)先級(jí)問(wèn)題

    摘要:?jiǎn)栴}描述在使用作為路由遇到了一個(gè)優(yōu)先級(jí)問(wèn)題如下代碼在訪問(wèn)時(shí)路由會(huì)優(yōu)先匹配到路由返回這個(gè)問(wèn)題就很尷尬了項(xiàng)目空閑下來(lái)去翻看源碼終于找到了原因問(wèn)題原因的源碼并不長(zhǎng)和兩個(gè)文件加起來(lái)共一千多行代碼建議可以結(jié)合這篇文章閱讀其中造成這個(gè)問(wèn)題的原因 問(wèn)題描述 在使用Koa-router作為路由遇到了一個(gè)優(yōu)先級(jí)問(wèn)題.如下代碼 // routerPage.js file const router = re...

    Paul_King 評(píng)論0 收藏0
  • koa源碼閱讀[3]-koa-send與它的衍生(static)

    摘要:源碼閱讀的第四篇,涉及到向接口請(qǐng)求方提供文件數(shù)據(jù)。是一個(gè)基于的淺封裝。小結(jié)與算是兩個(gè)非常輕量級(jí)的中間件了。 koa源碼閱讀的第四篇,涉及到向接口請(qǐng)求方提供文件數(shù)據(jù)。 第一篇:koa源碼閱讀-0 第二篇:koa源碼閱讀-1-koa與koa-compose 第三篇:koa源碼閱讀-2-koa-router 處理靜態(tài)文件是一個(gè)繁瑣的事情,因?yàn)殪o態(tài)文件都是來(lái)自于服務(wù)器上,肯定不能放開(kāi)所有...

    jimhs 評(píng)論0 收藏0
  • koa-router 源碼淺析

    摘要:代碼結(jié)構(gòu)執(zhí)行流程上面兩張圖主要將的整體代碼結(jié)構(gòu)和大概的執(zhí)行流程畫(huà)了出來(lái),畫(huà)的不夠具體。那下面主要講中的幾處的關(guān)鍵代碼解讀一下。全局的路由參數(shù)處理的中間件組成的對(duì)象。 代碼結(jié)構(gòu) showImg(https://segmentfault.com/img/remote/1460000007468236?w=1425&h=1772); 執(zhí)行流程 showImg(https://segmentf...

    SillyMonkey 評(píng)論0 收藏0
  • 玩轉(zhuǎn)Koa -- koa-router原理解析

    摘要:四路由注冊(cè)構(gòu)造函數(shù)首先看了解一下構(gòu)造函數(shù)限制必須采用關(guān)鍵字服務(wù)器支持的請(qǐng)求方法,后續(xù)方法會(huì)用到保存前置處理函數(shù)存儲(chǔ)在構(gòu)造函數(shù)中初始化的和屬性最為重要,前者用來(lái)保存前置處理函數(shù),后者用來(lái)保存實(shí)例化的對(duì)象。 一、前言 ??Koa為了保持自身的簡(jiǎn)潔,并沒(méi)有捆綁中間件。但是在實(shí)際的開(kāi)發(fā)中,我們需要和形形色色的中間件打交道,本文將要分析的是經(jīng)常用到的路由中間件 -- koa-router。 ??...

    wthee 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<