摘要:第三篇,有關(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源碼閱讀-0koa-router是什么
第二篇:koa源碼閱讀-1-koa與koa-compose
首先,因?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)并不是很復(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中的路由處理 |
可以拿上邊所拋出的基本例子來(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 |
如果設(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 => 404strict
strict與sensitive功能類似,也是用來(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/ => 404methods
methods配置項(xiàng)存在的意義在于,如果我們有一個(gè)接口需要同時(shí)支持GET和POST,router.get、router.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è)Router,Router之間的配置可能不盡相同,不能保證所有的Router都和當(dāng)前Router可處理的METHOD是一樣的。
所以,個(gè)人感覺(jué)methods參數(shù)的存在意義并不是很大。。
這個(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)自Router的register方法:
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、sensitive和prefix在這里都有體現(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ù)組
在函數(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ì)提到
middlewaremiddleware則是一次路由真正執(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í)例的中間件
optsopts則是用來(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,主要是用于這幾個(gè)地方:
拋出異常時(shí)更方便的定位
可以通過(guò)router.url(
在中間件執(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ù),params和options,因?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; irouter.param的作用// { age: 18, name: "Niko" } 上述是關(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字段。
router.use-Router內(nèi)部的中間件
只是很簡(jiǎn)單的處理了一下命名name路由相關(guān)的邏輯,然后進(jìn)行調(diào)用register完成操作。以及上文中也提到的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)的,但是.use在methods參數(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)提到了,Layer的setPrefix是拼接的,而不是覆蓋的。
而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ì)覆蓋。path與pathAndMethod都是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
摘要:本打算教一步步實(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...
摘要:?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...
摘要:源碼閱讀的第四篇,涉及到向接口請(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)所有...
摘要:代碼結(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...
摘要:四路由注冊(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。 ??...
閱讀 2257·2021-11-23 09:51
閱讀 1051·2021-11-18 10:02
閱讀 3444·2021-10-13 09:49
閱讀 1275·2021-09-22 14:57
閱讀 10495·2021-08-18 10:20
閱讀 1189·2019-08-30 15:55
閱讀 2236·2019-08-29 16:06
閱讀 3241·2019-08-29 11:14