摘要:若大于時,將賦予,此時與相等。通過源碼分析,我們知道了的核心思想建立于中間件機制,它是一個設(shè)計十分簡潔巧妙的框架,擴展性極強,就是建立于之上的上層框架。
Koa是一款設(shè)計優(yōu)雅的輕量級Node.js框架,它主要提供了一套巧妙的中間件機制與簡練的API封裝,因此源碼閱讀起來也十分輕松,不論你從事前端或是后端研發(fā),相信都會有所收獲。
目錄結(jié)構(gòu)首先將源碼下載到本地,可以看到Koa的源碼只包含下述四個文件:
lib ├── application.js ├── context.js ├── request.js └── response.jsapplication.js
application.js為Koa的主程序入口文件,在package.json的main字段有定義。它主要負(fù)責(zé)HTTP服務(wù)的注冊、封裝請求相應(yīng)對象,并初始化中間件數(shù)組并通過compose方法進行執(zhí)行。
context.jscontext.js的核心工作為將請求與響應(yīng)方法集成到一個上下文(Context)中,上下文中的大多數(shù)方法都是直接委托到了請求與響應(yīng)對象上,本身并沒做什么改變,它能為編寫Web應(yīng)用程序提供便捷。
request.jsrequest.js將http庫的request方法進行抽象與封裝,通過它可以訪問到各種請求信息。
response.jsresponse.js與request功能類似,它是對response對象的抽象與封裝。
中間件 示例對于Koa的中間件機制相信大家都耳熟能詳了,現(xiàn)在讓我們來看看源碼實現(xiàn)。在這里還是先舉一個最簡單的例子:
const Koa = require("koa"); const app = new Koa(); app.use((ctx, next) => { console.log("enter 1"); next(); console.log("out 1"); }); app.use((ctx, next) => { console.log("enter 2"); next(); console.log("out 2"); }); app.use((ctx, next) => { console.log("enter 3"); next(); console.log("out 3"); }); app.listen(3000);
現(xiàn)在讓我們來訪問應(yīng)用:curl 127.0.0.1:3000,可以看到以下輸出結(jié)果:
enter 1 enter 2 enter 3 out 3 out 2 out 1next是什么?
通過以上的結(jié)果進行分析,當(dāng)我們執(zhí)行next()的時候,可能程序的執(zhí)行權(quán)交給了下一個中間件,next函數(shù)會等待下一個中間件執(zhí)行完畢,然后接著執(zhí)行,這樣的執(zhí)行機制被稱為“洋蔥模型”,因為它就像請求穿過一層洋蔥一樣,先從外向內(nèi)一層一層執(zhí)行,再從內(nèi)向外一層一層返回,而next就是進行下一層的一把鑰匙:
聊完了理想,現(xiàn)在我們來聊現(xiàn)實。首先來看看app.use函數(shù):
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; }
整個函數(shù)只做了一件事情,將中間件函數(shù)添加到了實例中的middleware數(shù)組,其他的即是對類型進行校驗,若不為函數(shù)則直接報TypeError,若為生成器則發(fā)出deprecated警告并使用koa-convert[注1]對其轉(zhuǎn)化。
中間件在什么時候執(zhí)行的呢?首先我們找到listen的回調(diào)函數(shù):
const server = http.createServer(this.callback());
然后來看看這個神奇的callback函數(shù):
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; }
函數(shù)首先將中間件使用koa-compose進行處理,那個compose到底是個什么呢?不如直接來看源碼吧(省略掉了注釋與類型檢測):
function compose (middleware) { 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) } } } }
首先我們把目光放到index與i兩個變量上,當(dāng)執(zhí)行執(zhí)行compose(middleware)函數(shù)時,會返回一個閉包函數(shù)distpach(0),閉包函數(shù)執(zhí)行時,dispatch函數(shù)內(nèi)部的判斷邏輯如下:
若i小于等于index 則報出錯誤:"next() called multiple times"。
若i大于index時,將i賦予index,此時i與index相等。
邏輯很簡單,但這樣做的目的是什么呢?假若程序按著預(yù)期執(zhí)行,每個中間件內(nèi)部都執(zhí)行next(),假若有3個中間件,那么當(dāng)每次執(zhí)行dispatch(i)時,到Line8之前index與i的值分別為:-1/0, 0/1, 1/2,可以看出i始終要大于index,index的閉包變量每次在執(zhí)行完函數(shù)后都會加1,因此可以知道的是若同一個中間件執(zhí)行了兩次,index就會等于i,再執(zhí)行一次index就會大于i,由此可知,index的存在意義在于限制next能執(zhí)行不超過1次。
Line9到Line11用于取出middleware中的當(dāng)前中間件,若數(shù)組為最大索引標(biāo)識,則會將fn等于next函數(shù),意味著將再執(zhí)行一次越級的索引i + 1,由于取不到值,于是就執(zhí)行到Line11返回Promise.resolve()。
當(dāng)函數(shù)執(zhí)行到Line13,則會運行當(dāng)前中間件,并將是否執(zhí)行下一個中間件dispatch(i + 1)的決定權(quán)傳遞到next參數(shù),將運行結(jié)果返回,返回函數(shù)的運行結(jié)果的意義在于每次執(zhí)行next的返回結(jié)果都是下一個中間件的執(zhí)行結(jié)果的Promise對象。
回到callback讓我們繼續(xù)看callback函數(shù)等剩余邏輯:
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; }
首先來看看Line3,因為Application繼承與Emitter,故此方法是用于監(jiān)聽實例中的error事件的,當(dāng)listenerCount的數(shù)值為0時,表示沒有監(jiān)聽過,則注冊監(jiān)聽函數(shù)。
接著生成一個handleRequest回調(diào),當(dāng)每個請求過來時,都會創(chuàng)建ctx上下文對象,并將中間件函數(shù)傳入實例方法handleRequest,讓我們來看看此時的處理函數(shù):
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
在這里多出了幾個函數(shù):
on-finished,監(jiān)聽請求是否正常結(jié)束。
respond, 當(dāng)中間件執(zhí)行完畢后,處理response對象的status與body的字段。
回到示例回到示例,是否恍如隔日?現(xiàn)在的代碼還困擾你嗎?讓我們稍作修改:
app.use((ctx, next) => { console.log("enter 1"); next(); console.log("out 1"); }); app.use((ctx, next) => { console.log("enter 2"); }); app.use((ctx, next) => { console.log("enter 3"); next(); console.log("out 3"); });
此時你能準(zhǔn)確的知道執(zhí)行結(jié)果嗎?此時打印順序為:enter 1 -> enter 2 -> out 1。因為只有next才是進入到下一中間件的鑰匙。若再將程序改一改:
app.use(async (ctx, next) => { console.log("enter 1"); next(); console.log("out 1"); }); app.use(async (ctx, next) => { console.log("enter 2"); await next(); console.log("out 2"); });
此時執(zhí)行結(jié)果為:enter1 -> enter2 -> out 1 -> out2,這你能答對嗎?你不需要記住范式與結(jié)果,回想一下核心的compose函數(shù):return Promise.resolve(fn(context, dispatch.bind(null, i + 1))),首先中間件全為async函數(shù),若使用await next(),則會等待下一個中間件返回resolve狀態(tài)才會執(zhí)行此代碼,如果某一個Promise中間件不使用await關(guān)鍵字呢?它會在主進程上進行排隊等待,等到函數(shù)執(zhí)行棧返回到當(dāng)前函數(shù)后立即執(zhí)行。對于此示例來講,當(dāng)進入到第二個中間件,遇到await關(guān)鍵字時,console.log("out 2")則不會再執(zhí)行,而是進入到微任務(wù)隊列中,此時主進程已無其他任務(wù),則函數(shù)退出當(dāng)前棧,返回到了第一個函數(shù)中,此時輸出out 1,當(dāng)?shù)谝粋€中間件執(zhí)行結(jié)束后,事件循環(huán)才會將中間件2的微任務(wù)取出來執(zhí)行,因此你見到了上述的輸出順序。
上下文通過上述分析,我們了解到http.createServer中有一個callback函數(shù),它不僅負(fù)責(zé)執(zhí)行compose函數(shù),也會調(diào)用createContext方法創(chuàng)建函數(shù)上下文,源碼如下:
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.state = {}; return context; }
可以由這個函數(shù)得知,ctx對象包含了`context.js
request.js、response.js`的代碼。通過訪問req與res的源代碼,大家可以發(fā)現(xiàn)在request與response對象中封裝了許許多多http庫的請求方法與各類工具函數(shù),若對http底層實現(xiàn)感興趣的小伙伴可以仔細讀一下request與response文件,否則多查閱幾遍官網(wǎng)文檔,大概了解其中的api即可。
而對于context.js,其實十分簡單,它也封裝了部分工具方法,并使用node-delegates進行委托方法與屬性,對于此類方法的時間,估計koa3會將這一部分進行重構(gòu)為Proxy吧。
注解 1. koa-convert轉(zhuǎn)化在Koa版本號為1.x時,中間件都是使用Generator實現(xiàn)的,因此可以通過官方提供的koa-convert臨時對其進行轉(zhuǎn)化與兼容,基本用法為:
function * legacyMiddleware (next) { // before yield next // after } app.use(convert(legacyMiddleware))
然后打開源碼發(fā)現(xiàn),核心代碼大概如下:
function convert (mw) { return (ctx, next) => co.call(ctx, mw.call(ctx, createGenerator(next))) }
convert函數(shù)將生成器通過co進行包裝為Promise函數(shù),在ctx上下文進行執(zhí)行,并傳入next函數(shù)。
總結(jié)凡是涉及到原理性的東西,感覺自己很難避免自顧自說,用圖片進行可視化的方式會更加直觀,易于理解,希望之后自己多多使用圖片來闡述原理。
通過源碼分析,我們知道了Koa的核心思想建立于中間件機制,它是一個設(shè)計十分簡潔、巧妙的Web框架,擴展性極強,egg.js就是建立于Koa之上的上層框架。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/101844.html
摘要:中異步處理在中使用的是,在中使用的是,我們本次采用來處理異步。函數(shù)參數(shù)為的數(shù)組,數(shù)組成員是通過方法添加的中間件。小結(jié)從最開始的編寫中間件,到現(xiàn)在閱讀函數(shù)源碼,中間件機制并不復(fù)雜,了解之后,我們可以運用編寫更合適的中間件,構(gòu)建自己的應(yīng)用。 上一篇講了如何編寫屬于自己的 Koa 中間件,本篇將根據(jù)原理實現(xiàn)一個簡單的中間件處理函數(shù),并對 Koa 中間件處理函數(shù) compose 函數(shù)進行源碼解...
摘要:實現(xiàn)的四大模塊上文簡述了源碼的大體框架結(jié)構(gòu),接下來我們來實現(xiàn)一個的框架,筆者認(rèn)為理解和實現(xiàn)一個框架需要實現(xiàn)四個大模塊,分別是封裝創(chuàng)建類構(gòu)造函數(shù)構(gòu)造對象中間件機制和剝洋蔥模型的實現(xiàn)錯誤捕獲和錯誤處理下面我們就逐一分析和實現(xiàn)。 什么是koa框架? ? ? ? ?koa是一個基于node實現(xiàn)的一個新的web框架,它是由express框架的原班人馬打造的。它的特點是優(yōu)雅、簡潔、表達力強、自由度...
摘要:實現(xiàn)的四大模塊上文簡述了源碼的大體框架結(jié)構(gòu),接下來我們來實現(xiàn)一個的框架,筆者認(rèn)為理解和實現(xiàn)一個框架需要實現(xiàn)四個大模塊,分別是封裝創(chuàng)建類構(gòu)造函數(shù)構(gòu)造對象中間件機制和剝洋蔥模型的實現(xiàn)錯誤捕獲和錯誤處理下面我們就逐一分析和實現(xiàn)。 什么是koa框架? ? ? ? ?koa是一個基于node實現(xiàn)的一個新的web框架,它是由express框架的原班人馬打造的。它的特點是優(yōu)雅、簡潔、表達力強、自由度...
閱讀 657·2021-11-25 09:43
閱讀 1928·2021-11-17 09:33
閱讀 842·2021-09-07 09:58
閱讀 2076·2021-08-16 10:52
閱讀 494·2019-08-30 15:52
閱讀 1736·2019-08-30 15:43
閱讀 1016·2019-08-30 15:43
閱讀 2938·2019-08-29 16:41