摘要:接上次挖的坑,對相關的源碼進行分析第一篇。和同為一批人進行開發,與相比,顯得非常的迷你。在接收到一個請求后,會拿之前提到的與來創建本次請求所使用的上下文。以及如果沒有手動指定,會默認指定為。
接上次挖的坑,對koa2.x相關的源碼進行分析 第一篇。
不得不說,koa是一個很輕量、很優雅的http框架,尤其是在2.x以后移除了co的引入,使其代碼變得更為清晰。
express和koa同為一批人進行開發,與express相比,koa顯得非常的迷你。
因為express是一個大而全的http框架,內置了類似router之類的中間件進行處理。
而在koa中,則將類似功能的中間件全部摘了出來,早期koa里邊是內置了koa-compose的,而現在也是將其分了出來。
koa只保留一個簡單的中間件的整合,http請求的處理,作為一個功能性的中間件框架來存在,自身僅有少量的邏輯。
koa-compose則是作為整合中間件最為關鍵的一個工具、洋蔥模型的具體實現,所以要將兩者放在一起來看。
. ├── application.js ├── request.js ├── response.js └── context.js
關于koa整個框架的實現,也只是簡單的拆分為了四個文件。
就象在上一篇筆記中模擬的那樣,創建了一個對象用來注冊中間件,監聽http服務,這個就是application.js在做的事情。
而框架的意義呢,就是在框架內,我們要按照框架的規矩來做事情,同樣的,框架也會提供給我們一些更易用的方式來讓我們完成需求。
針對http.createServer回調的兩個參數request和response進行的一次封裝,簡化一些常用的操作。
例如我們對Header的一些操作,在原生http模塊中可能要這樣寫:
// 獲取Content-Type request.getHeader("Content-Type") // 設置Content-Type response.setHeader("Content-Type", "application/json") response.setHeader("Content-Length", "18") // 或者,忽略前邊的statusCode,設置多個Header response.writeHead(200, { "Content-Type": "application/json", "Content-Length": "18" })
而在koa中可以這樣處理:
// 獲取Content-Type context.request.get("Content-Type") // 設置Content-Type context.response.set({ "Content-Type": "application/json", "Content-Length": "18" })
簡化了一些針對request與response的操作,將這些封裝在了request.js和response.js文件中。
但同時這會帶來一個使用上的困擾,這樣封裝以后其實獲取或者設置header變得層級更深,需要通過context找到request、response,然后才能進行操作。
所以,koa使用了node-delegates來進一步簡化這些步驟,將request.get、response.set通通代理到context上。
也就是說,代理后的操作是這樣子的:
context.get("Content-Type") // 設置Content-Type context.set({ "Content-Type": "application/json", "Content-Length": "18" })
這樣就變得很清晰了,獲取Header,設置Header,再也不會擔心寫成request.setHeader了,一氣呵成,通過context.js來整合request.js與response.js的行為。
同時context.js也會提供一些其他的工具函數,例如Cookie之類的操作。
由application引入context,context中又整合了request和response的功能,四個文件的作用已經很清晰了:
file | desc |
---|---|
applicaiton | 中間件的管理、http.createServer的回調處理,生成Context作為本次請求的參數,并調用中間件 |
request | 針對http.createServer -> request功能上的封裝 |
response | 針對http.createServer -> response功能上的封裝 |
context | 整合request與response的部分功能,并提供一些額外的功能 |
而在代碼結構上,只有application對外的koa是采用的Class的方式,其他三個文件均是拋出一個普通的Object。
拿一個完整的流程來解釋 創建服務首先,我們需要創建一個http服務,在koa2.x中創建服務與koa1.x稍微有些區別,要求使用實例化的方式來進行創建:
const app = new Koa()
而在實例化的過程中,其實koa只做了有限的事情,創建了幾個實例屬性。
將引入的context、request以及response通過Object.create拷貝的方式放到實例中。
this.middleware = [] // 最關鍵的一個實例屬性 // 用于在收到請求后創建上下文使用 this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response)
在實例化完成后,我們就要進行注冊中間件來實現我們的業務邏輯了,上邊也提到了,koa僅用作一個中間件的整合以及請求的監聽。
所以不會像express那樣提供router.get、router.post之類的操作,僅僅存在一個比較接近http.createServer的use()。
接下來的步驟就是注冊中間件并監聽一個端口號啟動服務:
const port = 8000 app.use(async (ctx, next) => { console.time("request") await next() console.timeEnd("request") }) app.use(async (ctx, next) => { await next() ctx.body = ctx.body.toUpperCase() }) app.use(ctx => { ctx.body = "Hello World" }) app.use(ctx => { console.log("never output") }) app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))
在翻看application.js的源碼時,可以看到,暴露給外部的方法,常用的基本上就是use和listen。
一個用來加載中間件,另一個用來監聽端口并啟動服務。
而這兩個函數實際上并沒有過多的邏輯,在use中僅僅是判斷了傳入的參數是否為一個function,以及在2.x版本針對Generator函數的一些特殊處理,將其轉換為了Promise形式的函數,并將其push到構造函數中創建的middleware數組中。
這個是從1.x過渡到2.x的一個工具,在3.x版本將直接移除Generator的支持。
其實在koa-convert內部也是引用了co和koa-compose來進行轉化,所以也就不再贅述。
而在listen中做的事情就更簡單了,只是簡單的調用http.createServer來創建服務,并監聽對應的端口之類的操作。
有一個細節在于,createServer中傳入的是koa實例的另一個方法調用后的返回值callback,這個方法才是真正的回調處理,listen只是http模塊的一個快捷方式。
這個是為了一些用socket.io、https或者一些其他的http模塊來進行使用的。
也就意味著,只要是可以提供與http模塊一致的行為,koa都可以很方便的接入。
listen(...args) { debug("listen") const server = http.createServer(this.callback()) return server.listen(...args) }使用koa-compose合并中間件
所以我們就來看看callback的實現:
callback() { const fn = compose(this.middleware) if (!this.listenerCount("error")) this.on("error", this.onerror) const handleRequest = (req, res) => { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest }
在函數內部的第一步,就是要處理中間件,將一個數組中的中間件轉換為我們想要的洋蔥模型格式的。
這里就用到了比較核心的koa-compose
其實它的功能上與co類似,只不過把co處理Generator函數那部分邏輯全部去掉了,本身co的代碼也就是一兩百行,所以精簡后的koa-compose代碼僅有48行。
我們知道,async函數實際上剝開它的語法糖以后是長這個樣子的:
async function func () { return 123 } // ==> function func () { return Promise.resolve(123) } // or function func () { return new Promise(resolve => resolve(123)) }
所以拿上述use的代碼舉例,實際上koa-compose拿到的是這樣的參數:
[ function (ctx, next) { return new Promise(resolve => { console.time("request") next().then(() => { console.timeEnd("request") resolve() }) }) }, function (ctx, next) { return new Promise(resolve => { next().then(() => { ctx.body = ctx.body.toUpperCase() resolve() }) }) }, function (ctx, next) { return new Promise(resolve => { ctx.body = "Hello World" resolve() }) }, function (ctx, next) { return new Promise(resolve => { console.log("never output") resolve() }) } ]
就像在第四個函數中輸出表示的那樣,第四個中間件不會被執行,因為第三個中間件并沒有調用next,所以實現類似這樣的一個洋蔥模型是很有意思的一件事情。
首先拋開不變的ctx不談,洋蔥模型的實現核心在于next的處理。
因為next是你進入下一層中間件的鑰匙,只有手動觸發以后才會進入下一層中間件。
然后我們還需要保證next要在中間件執行完畢后進行resolve,返回到上一層中間件:
return 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, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } }
所以明確了這兩點以后,上邊的代碼就會變得很清晰:
next用來進入下一個中間件
next在當前中間件執行完成后會觸發回調通知上一個中間件,而完成的前提是內部的中間件已經執行完成(resolved)
可以看到在調用koa-compose以后實際上會返回一個自執行函數。
在執行函數的開頭部分,判斷當前中間件的下標來防止在一個中間件中多次調用next。
因為如果多次調用next,就會導致下一個中間件的多次執行,這樣就破壞了洋蔥模型。
其次就是compose實際上提供了一個在洋蔥模型全部執行完畢后的回調,一個可選的參數,實際上作用與調用compose后邊的then處理沒有太大區別。
以及上邊提到的,next是進入下一個中間件的鑰匙,可以在這一個柯里化函數的應用上看出來:
Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
將自身綁定了index參數后傳入本次中間件,作為調用函數的第二個參數,也就是next,效果就像調用了dispatch(1),這樣就是一個洋蔥模型的實現。
而fn的調用如果是一個async function,那么外層的Promise.resolve會等到內部的async執行resolve以后才會觸發resolve,例如這樣:
Promise.resolve(new Promise(resolve => setTimeout(resolve, 500))).then(console.log) // 500ms以后才會觸發 console.log
P.S. 一個從koa1.x切換到koa2.x的暗坑,co會對數組進行特殊處理,使用Promise.all進行包裝,但是koa2.x沒有這樣的操作。
所以如果在中間件中要針對一個數組進行異步操作,一定要手動添加Promise.all,或者說等草案中的await*。
// koa1.x yield [Promise.resolve(1), Promise.resolve(2)] // [1, 2] // koa2.x await [Promise.resolve(1), Promise.resolve(2)] // [接收請求,處理返回值, ] // ==> await Promise.all([Promise.resolve(1), Promise.resolve(2)]) // [1, 2] await* [Promise.resolve(1), Promise.resolve(2)] // [1, 2]
經過上邊的代碼,一個koa服務已經算是運行起來了,接下來就是訪問看效果了。
在接收到一個請求后,koa會拿之前提到的context與request、response來創建本次請求所使用的上下文。
在koa1.x中,上下文是綁定在this上的,而在koa2.x是作為第一個參數傳入進來的。
個人猜測可能是因為Generator不能使用箭頭函數,而async函數可以使用箭頭函數導致的吧:) 純屬個人YY
總之,我們通過上邊提到的三個模塊創建了一個請求所需的上下文,基本上是一通兒賦值,代碼就不貼了,沒有太多邏輯,就是有一個小細節比較有意思:
request.response = response response.request = request
讓兩者之間產生了一個引用關系,既可以通過request獲取到response,也可以通過response獲取到request。
而且這是一個遞歸的引用,類似這樣的操作:
let obj = {} obj.obj = obj obj.obj.obj.obj === obj // true
同時如上文提到的,在context創建的過程中,將一大批的request和response的屬性、方法代理到了自身,有興趣的可以自己翻看源碼(看著有點暈):koa.js | context.js
這個delegate的實現也算是比較簡單,通過取出原始的屬性,然后存一個引用,在自身的屬性被觸發時調用對應的引用,類似一個民間版的Proxy吧,期待后續能夠使用Proxy代替它。
然后我們會將生成好的context作為參數傳入koa-compose生成的洋蔥中去。
因為無論何種情況,洋蔥肯定會返回結果的(出錯與否),所以我們還需要在最后有一個finished的處理,做一些類似將ctx.body轉換為數據進行輸出之類的操作。
koa使用了大量的get、set訪問器來實現功能,例如最常用的ctx.body = "XXX",它是來自response的set body。
這應該是request、response中邏輯最復雜的一個方法了。
里邊要處理很多東西,例如在body內容為空時幫助你修改請求的status code為204,并移除無用的headers。
以及如果沒有手動指定status code,會默認指定為200。
甚至還會根據當前傳入的參數來判斷content-type應該是html還是普通的text:
// string if ("string" == typeof val) { if (setType) this.type = /^s*以及還包含針對流(Stream)的特殊處理,例如如果要用koa實現靜態資源下載的功能,也是可以直接調用ctx.body進行賦值的,所有的東西都已經在response.js中幫你處理好了:
// stream if ("function" == typeof val.pipe) { onFinish(this.res, destroy.bind(null, val)) ensureErrorHandler(val, err => this.ctx.onerror(err)) // overwriting if (null != original && original != val) this.remove("Content-Length") if (setType) this.type = "bin" return } // 可以理解為是這樣的代碼 let stream = fs.createReadStream("package.json") ctx.body = stream // set body中的處理 onFinish(res, () => { destory(stream) }) stream.pipe(res) // 使response接收流是在洋蔥模型完全執行完以后再進行的onFinish用來監聽流是否結束、destory用來關閉流
其余的訪問器基本上就是一些常見操作的封裝,例如針對querystring的封裝。
在使用原生http模塊的情況下,處理URL中的參數,是需要自己引入額外的包進行處理的,最常見的是querystring。
koa也是在內部引入的該模塊。
所以對外拋出的query大致是這個樣子的:get query() { let query = parse(this.req).query return qs.parse(query) } // use let { id, name } = ctx.query // 因為 get query也被代理到了context上,所以可以直接引用parse為parseurl庫,用來從request中提出query參數
亦或者針對cookies的封裝,也是內置了最流行的cookies。
在第一次觸發get cookies時才去實例化Cookie對象,將這些繁瑣的操作擋在用戶看不到的地方:get cookies() { if (!this[COOKIES]) { this[COOKIES] = new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure }) } return this[COOKIES] } set cookies(_cookies) { this[COOKIES] = _cookies }所以在koa中使用Cookie就像這樣就可以了:
this.cookies.get("uid") this.cookies.set("name", "Niko") // 如果不想用cookies模塊,完全可以自己賦值為自己想用的cookie this.cookies = CustomeCookie this.cookies.mget(["uid", "name"])這是因為在get cookies里邊有判斷,如果沒有一個可用的Cookie實例,才會默認去實例化。
洋蔥模型執行完成后的一些操作koa的一個請求流程是這樣的,先執行洋蔥里邊的所有中間件,在執行完成以后,還會有一個回調函數。
該回調用來根據中間件執行過程中所做的事情來決定返回給客戶端什么數據。
拿到ctx.body、ctx.status這些參數進行處理。
包括前邊提到的流(Stream)的處理都在這里:if (body instanceof Stream) return body.pipe(res) // 等到這里結束后才會調用我們上邊`set body`中對應的`onFinish`的處理同時上邊還有一個特殊的處理,如果為false則不做任何處理,直接返回:
if (!ctx.writable) return其實這個也是response提供的一個訪問器,這里邊用來判斷當前請求是否已經調用過end給客戶端返回了數據,如果已經觸發了response.end()以后,則response.finished會被置為true,也就是說,本次請求已經結束了,同時訪問器中還處理了一個bug,請求已經返回結果了,但是依然沒有關閉套接字:
get writable() { // can"t write any more after response finished if (this.res.finished) return false const socket = this.res.socket // There are already pending outgoing res, but still writable // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486 if (!socket) return true return socket.writable }這里就有一個koa與express對比的劣勢了,因為koa采用的是一個洋蔥模型,對于返回值,如果是使用ctx.body = "XXX"來進行賦值,這會導致最終調用response.end時在洋蔥全部執行完成后再進行的,也就是上邊所描述的回調中,而express就是在中間件中就可以自由控制何時返回數據:
// express.js router.get("/", function (req, res) { res.send("hello world") // 在發送數據后做一些其他處理 appendLog() }) // koa.js app.use(ctx => { ctx.body = "hello world" // 然而依然發生在發送數據之前 appendLog() })不過好在還是可以通過直接調用原生的response對象來進行發送數據的,當我們手動調用了response.end以后(response.finished === true),就意味著最終的回調會直接跳過,不做任何處理。
app.use(ctx => { ctx.res.end("hello world") // 在發送數據后做一些其他處理 appendLog() })異常處理koa的整個請求,實際上還是一個Promise,所以在洋蔥模型后邊的監聽不僅僅有resolve,對reject也同樣是有處理的。
期間任何一環出bug都會導致后續的中間件以及前邊等待回調的中間件終止,直接跳轉到最近的一個異常處理模塊。
所以,如果有類似接口耗時統計的中間件,一定要記得在try-catch中執行next的操作:app.use(async (ctx, next) => { try { await next() } catch (e) { console.error(e) ctx.body = "error" // 因為內部的中間件并沒有catch 捕獲異常,所以拋出到了這里 } }) app.use(async (ctx, next) => { let startTime = new Date() try { await next() } finally { let endTime = new Date() // 拋出異常,但是不影響這里的正常輸出 } }) app.use(ctx => Promise.reject(new Error("test")))P.S. 如果異常被捕獲,則會繼續執行后續的response:
app.use(async (ctx, next) => { try { throw new Error("test") } catch (e) { await next() } }) app.use(ctx => { ctx.body = "hello" }) // curl 127.0.0.1 // > hello如果自己的中間件沒有捕獲異常,就會走到默認的異常處理模塊中。
在默認的異常模塊中,基本上是針對statusCode的一些處理,以及一些默認的錯誤顯示:const code = statuses[err.status] const msg = err.expose ? err.message : code this.status = err.status this.length = Buffer.byteLength(msg) this.res.end(msg)statuses是一個第三方模塊,包括各種http code的信息: statuses
redirect的注意事項
建議在最外層的中間件都自己做異常處理,因為默認的錯誤提示有點兒太難看了(純文本),自己處理跳轉到異常處理頁面會好一些,以及避免一些接口因為默認的異常信息導致解析失敗。在原生http模塊中進行302的操作(俗稱重定向),需要這么做:
response.writeHead(302, { "Location": "redirect.html" }) response.end() // or response.statusCode = 302 response.setHeader("Location", "redirect.html") response.end()而在koa中也有redirect的封裝,可以通過直接調用redirect函數來完成重定向,但是需要注意的是,調用完redirect之后并沒有直接觸發response.end(),它僅僅是添加了一個statusCode及Location而已:
redirect(url, alt) { // location if ("back" == url) url = this.ctx.get("Referrer") || alt || "/" this.set("Location", url) // status if (!statuses.redirect[this.status]) this.status = 302 // html if (this.ctx.accepts("html")) { url = escape(url) this.type = "text/html charset=utf-8" this.body = `Redirecting to ${url}.` return } // text this.type = "text/plain charset=utf-8" this.body = `Redirecting to ${url}.` }后續的代碼還會繼續執行,所以建議在redirect之后手動結束當前的請求,也就是直接return,不然很有可能后續的status、body賦值很可能會導致一些詭異的問題。
app.use(ctx => { ctx.redirect("https://baidu.com") // 建議直接return // 后續的代碼還在執行 ctx.body = "hello world" ctx.status = 200 // statusCode的改變導致redirect失效 })小記koa是一個很好玩的框架,在閱讀源碼的過程中,其實也發現了一些小問題:
多人合作維護一份代碼,確實能夠看出各人都有不同的編碼風格,例如typeof val !== "string"和"number" == typeof code,很顯然的兩種風格。2333
delegate的調用方式在屬性特別多的時候并不是很好看,一大長串的鏈式調用,如果換成循環會更好看一下
但是,koa依然是一個很棒的框架,很適合閱讀源碼來進行學習,這些都是一些小細節,無傷大雅。
總結一下koa與koa-compose的作用:
koa 注冊中間件、注冊http服務、生成請求上下文調用中間件、處理中間件對上下文對象的操作、返回數據結束請求
koa-compose 將數組中的中間件集合轉換為串行調用,并提供鑰匙(next)用來跳轉下一個中間件,以及監聽next獲取內部中間件執行結束的通知
招人,招人我司現在大量招人咯,前端、Node方向都有HC
公司名:Blued,坐標帝都朝陽雙井
主要技術棧是React,也會有機會玩ReactNative和Electron
Node方向8.x版本+koa 新項目會以TS為主
有興趣的小伙伴可以私聊我,或者:
email: jiashunming@blued.com
wechat: github_jiasm
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96421.html
摘要:于是抱著知其然也要知其所以然的想法,開始閱讀的源代碼。問題讀源代碼時,自然是帶著諸多問題的。源代碼如下在被處理完后,每當有新請求,便會調用,去處理請求。接下來會繼續寫一些閱讀筆記,因為看的源代碼確實是獲益匪淺。 本筆記共四篇Koa源碼閱讀筆記(1) -- coKoa源碼閱讀筆記(2) -- composeKoa源碼閱讀筆記(3) -- 服務器の啟動與請求處理Koa源碼閱讀筆記(4) -...
摘要:引言最近空閑時間讀了一下的源碼在閱讀的源碼的過程中,我的感受是代碼簡潔思路清晰不得不佩服大神的水平。調用的時候就跟有區別使用必須使用來調用除了上面的的構造函數外,還暴露了一些公用的,比如兩個常見的,一個是,一個是。 引言 最近空閑時間讀了一下Koa2的源碼;在閱讀Koa2(version 2.2.0)的源碼的過程中,我的感受是代碼簡潔、思路清晰(不得不佩服大神的水平)。下面是我讀完之后...
摘要:原文博客地址,歡迎學習交流點擊預覽讀了下的源碼,寫的相當的精簡,遇到處理中間件執行的模塊決定學習一下這個模塊的源碼。當在下游沒有更多的中間件執行后,堆棧將展開并且每個中間件恢復執行其上游行為。 原文博客地址,歡迎學習交流:點擊預覽 讀了下Koa的源碼,寫的相當的精簡,遇到處理中間件執行的模塊koa-Compose,決定學習一下這個模塊的源碼。 閱讀本文可以學到: Koa中間件的加載...
摘要:從一個對象里面提取需要的屬性這篇文章一直想寫了還想起那一夜我看到白天的代碼,實在太美了。 koa源碼lib主要文件有 application.js context.js request.js response.js application.js koa主要的邏輯處理代碼整個koa的處理 context.js 將req,res方法 掛載在這,生成ctx上下文對象 requests....
摘要:本筆記共四篇源碼閱讀筆記源碼閱讀筆記源碼閱讀筆記服務器啟動與請求處理源碼閱讀筆記對象起因前兩天終于把自己一直想讀的源代碼讀了一遍。首先放上關鍵的源代碼在上一篇源碼閱讀筆記服務器啟動與請求處理中,我們已經分析了的作用。 本筆記共四篇Koa源碼閱讀筆記(1) -- coKoa源碼閱讀筆記(2) -- composeKoa源碼閱讀筆記(3) -- 服務器の啟動與請求處理Koa源碼閱讀筆記(4...
閱讀 1125·2021-11-24 10:21
閱讀 2561·2021-11-19 11:35
閱讀 1662·2019-08-30 15:55
閱讀 1293·2019-08-30 15:54
閱讀 1192·2019-08-30 15:53
閱讀 3498·2019-08-29 17:21
閱讀 3308·2019-08-29 16:12
閱讀 3412·2019-08-29 15:23