在SF平臺潛水好久了,這也是個人第一篇文章,以前一直以學習爲主。但願往後能經過寫技術文章和回答問題的方式來作一些輸出^ ^ 以前沒有這方面的習慣,因此語言組織方面可能會有點混亂.node
最近面試兩次被問到KOA的洋蔥圈模型(由於以前學校的幾個項目我都是用KOA2來寫的),可是本身沒有深挖過洋蔥圈的原理,感受答得不是很滿意。因此此次特意翻出KOA的源碼看了一下。
├── lib │ ├── application.js │ ├── context.js │ ├── request.js │ └── response.js └── package.json
目前咱們下載的node_modules/koa
包中的源文件結構就是如此。而koa處理請求的核心也就是以上四個文件,其中git
application.js
是整個KOA2的入口。最重要的中間件邏輯也是在此進行處理。本次學習的也就是這個文件。context.js
負責處理應用上下文request.js
處理http請求response.js
處理http響應簡單看看KOA
類中封裝了哪些方法,順藤摸瓜看看github
constructor(options) {面試
super(); options = options || {}; this.proxy = options.proxy || false; this.subdomainOffset = options.subdomainOffset || 2; this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'; this.maxIpsCount = options.maxIpsCount || 0; this.env = options.env || process.env.NODE_ENV || 'development'; // 環境變量 if (options.keys) this.keys = options.keys; this.middleware = []; // **中間件隊列** this.context = Object.create(context); // 上下文 this.request = Object.create(request); // 請求對象格式 this.response = Object.create(response); // 響應對象格式 if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; }
}json
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
KOA
的監聽函數是對原生的createServer
作的簡單封裝,傳入的參數也會直接被傳給原生的server.listen
。只不過這裏經過KOA類中的callback
方法生成了一個配置對象傳入server中,從這裏來看,KOA實際的執行邏輯實際上是經過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; }
首先查看第一句app
const fn = compose(this.middleware);
this.midlleware
顯然是實例中的中間件隊列。dom
衆所周知,compose
是組成的意思,之前咱們學過的一個短語就是be composed of
-> 由…所組成
。在代碼中的表現形式則大概爲組合函數koa
g() + h() => g(h())
這裏的compose變量來自koa-compose
這個包,他的做用是將全部的koa中間件進行合併執行。能夠理解爲以前的middleware
數組只是一些零散的洋蔥圈層級,是經過koa-compose
處理事後才成爲了一個完整的洋蔥(文章末尾會附上koa-compose
的原理)異步
回到上面的方法,得到了中間件合體後的組合函數fn
後,聲明瞭一個最終用於輸出的handleRequest
函數,在函數中先經過this.createContext
初始化報文,能夠理解爲生成了一個完整的請求報文和響應報文,而初始化報文的邏輯就寫在了lib/request.js
和lib/response.js
之中。最後將他們經過this.handleRequest
方法對報文進行處理。
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); }
這裏就是將處理後的報文傳給咱們以前生成的組合函數fn
(也就是洋蔥圈),在完成了整個洋蔥圈的處理邏輯以後去作一些後續處理(返回客戶端/錯誤處理)。
衆所周知,洋蔥圈模型的每一層都是一個Promise對象,只有當上遊(官方文檔的說法)的洋蔥圈進入resolved
狀態後,線程的使用權纔會向下遊傳遞。(請注意,此時上一層洋蔥圈並無執行完畢)以後當內層的Promise成爲resolved
狀態後,JS線程的使用權纔會繼續向上冒泡,去處理外層洋蔥圈resolve以後的邏輯。
洋蔥圈函數接收的兩個參數ctx
、next
中的next方法就是一個異步方法,表明將當前這層洋蔥圈強制resolve
,控制權指向下游,當前的函數將被阻塞,直到下游全部邏輯處理完以後,才能繼續執行。
看到這裏咱們已經能夠對目前看到的部分作一個梳理。
use
方法)this.callback
函數中經過koa-compose
將中間件進行組合生成了一個洋蔥圈處理器fn
(處理誰呢,固然是處理報文對象啦)handleRequest
。這個工廠函數只負責接受報文,返回結果。上面說到的use
方法其實也是洋蔥圈模型的核心,也就是字面上的註冊中間件方法。
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; }
use
接受的必須是一個函數(接受兩個參數,一個是ctx上下文,一個是next函數),若是這個函數是生成器(*generator
),(KOA1中的中間件只接收迭代器方法,KOA2中則所有用async await來實現),那麼就須要作一個優雅降級處理,經過koa-convert
函數去進行轉換。(這一起我也尚未看= =)
最後就很簡單了,將這個方法push到KOA實例的middleware
隊列中
看到這裏,application
文件的內容就結束了,這樣看來KOA的源碼仍是比較少的,可是由於中間件的存在,可擴展性變得很是強,這應該也是爲何Koa目前這麼火的緣由。
koa-compose
的代碼量很是少,只有50行不到(49行)。可是設計很是精妙。
function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } return function (context, next) { // 最後一次被調用的中間件下標 let index = -1 return dispatch(0) function dispatch (i) { // i <= index 說明有中間件被重複調用了,拋出錯誤 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] // fn取對應層的中間件,若是傳入了next函數,那麼next函數的優先級更高 if (i === middleware.length) fn = next // 此處fn指向的就是第i層的中間件函數 // 若是沒有了,那麼直接resolve if (!fn) return Promise.resolve() try { // 實際的執行步驟在這,執行fn方法,同時將下一層中間件的執行函數也傳遞過去 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
籠統的歸納這個語法就是每一箇中間件執行到await next()
語句的時候,都會調用下一層的中間件。你也能夠將代碼理解爲
// 前半部分處理邏輯 await next() // 後半部分處理邏輯 /* ================= 等價於 ================= */ // 前半部分處理邏輯 await new Promise([下一層中間件的邏輯]) // 後半部分處理邏輯
而這樣的插入邏輯在下一層中間件的邏輯中又在遞歸的發生着,洋蔥圈的執行邏輯其實存儲在數組koa.middleware
中,只有執行到洋蔥圈的第n
層的時候,纔會經過下標n+1
去取下一層的處理邏輯,而且生成Promise插入到上層洋蔥圈的函數體中,造成了一個不斷重疊的函數做用域。
END