koa@2.5.0源代碼解讀

koa簡介

koa是由Express原班人馬開發的一個nodejs服務器框架。koa使用了ES2017的新標準:async function來實現了真正意義上的中間件(middleware)。koa的源代碼極其簡單,可是藉由其強大的中間件擴展能力,使得koa成爲了一個極其強大的服務器框架。藉助中間件,你能夠作任何nodejs能作到的事兒。html

一些繁瑣的交代

本文並不會像其餘的代碼分析那樣貼段代碼加註釋,所以須要你本身打開koa@2.5.0的源代碼一塊兒閱讀。node

koa目前已經更新到了2.x版本,1.x之前的版本相對於koa@2.x已經再也不兼容。本文針對的是koa@2.5.0進行的代碼分析。jquery

此外,koa的源代碼裏面涉及部分http協議的內容,這部份內容本文不會過度強調,默認讀者已經掌握了基本的知識。git

另外,用於koa2是用了ES2017新特性編寫的,所以你須要瞭解一些ES2017的新語法才行。程序員

爲了使得這篇文章簡單,我有意地忽略了錯誤處理,參數判斷之類。github

本文你還能夠在這裏找到。express

$1.查看package.json

對於nodejs甚至是JavaScript項目,第一件事兒就是看看它的package.jsonpackage.json裏面能夠找到很多有用的信息。json

咱們打開koa@2.5.0的目錄,發現它依賴了很多的庫。其實這些庫大多都十分簡單,koa的編寫原則其實就是把功能分割到其餘的庫中去。咱們暫且先無論這些依賴。安全

咱們找到main字段,這裏就是‘通往新世界的大門’了。順着main打開lib/application.js服務器

$2.分析application.js

好傢伙,一上來就是一大串的引入,這可不是什麼好東西,咱麼先不看這些東西。先看下面的代碼。

首先是定義了一個application的類,接着在構造函數中定義了一些變量。咱們主要關注如下幾個變量,由於他們的用處最大:

this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
複製代碼

Object.create是用來克隆對象的,這裏克隆了三個對象,也是koa最重要的三個對象requestresponsecontext。這三個對象幾乎就是koa的所有內容了。待會兒會逐一分析。

咱們接着往下看,listen函數你們都很熟悉了,就是用來監聽端口的。koalisten函數也很簡單。

const server = http.createServer(this.callback());
    return server.listen(...args);
複製代碼

短短兩行,對於nodejs不熟的同窗,建議在這裏就打住了。其中this.callback()是個什麼玩意兒呢?它返回一個函數,這個函數接收兩個參數requestresponse,也就是createServer的回調函數,在中間件原理章節會更詳細介紹。

接着就是toJSSON這個方法。JSON.stringify調用的方法,目的是當你JSON化一個application實例時,返回指定的屬性,而並不是全部。這個用處不大,幾乎用不到。

inspect也就是調用了toJSON這個方法而已。

接着就是use函數了。use函數自己不是很複雜,可是use函數做爲中間件的接口,背後的中間件卻有點兒複雜。爲此,本文在後面專門解讀了中間件相關的源代碼,這裏暫時跳過。

callbackhandleRequestrespond這幾個方法涉及中間件的,所以放到中間件的章節講。

createContext這個方法是用來封裝context的。這個context就是你在使用koause方法,你傳遞的回調函數的第一個ctx參數。createContext執行的最重要的操做就是把context.request設置成了Request,把context.response設置成了Response。以及把Response.resh和Request.req分別設置成了原生的responserequest

爲何這樣說,這個就得追到context.jsrequest.js以及response.js的代碼裏面了,先等等。

值得強調的是,這裏的RequestResponse並非nodejs裏面的,而是koa封裝事後的。爲了區分原生的和koa封裝好的,我把RequestResponse稱爲封裝事後的,requestresponse稱爲原生的。你須要記住的是context.res是指原生的response,而context.response則是封裝後的ResponseRequest以此類推。

封裝的東西看起來並無什麼高大上,無非是把經常使用的一些方法給簡化了。就像jquery簡化了jsdom的操做同樣。

$3.分析context.js

打開context.js,代碼很少,可是含金量挺高的。首先是把proto賦值成一個對象,這個對象也是模塊的導出值。

inspecttoJSON功能和application.js裏面同樣,不作過多介紹了。

接着看到個assert,這個和nodejs裏面的assert實際上是差很少,它實際上是提供了一些斷言的操做。好比equalnotEqualstrictEqual之類的。比較有意思的是,assert提供了一個深度比較的方法deepEqual,這個但是個好東西。js裏面的深度比較一直是個比較麻煩的問題,有經驗的程序員會使用JSON來比較,這裏提供了一種性能更好的方法。代碼其實不復雜,就是引用了deep-eqaul這個庫而已,有興趣的能夠去看看哦。

跳過兩個關於錯誤處理的函數(本文不講解錯誤處理),來到了context.js最精華的地方了。 這裏使用了delegate這個庫。這是個啥?delegate其實很簡單的,你甚至不須要去查看delegate的源代碼,看我解釋就好了。

delegate提供了一種相似Proxy的手段,也就是代理。代理什麼?具體來講delegate(proto, 'response')這段代碼的意思就是把proto上的一些屬性代理到proto.response上面去。具體是哪些代理呢?就是接下來排列工整的代碼作的了。delegate區分了methodgetteraccess等類型。前面兩個還好理解,就是方法和只讀屬性,第三個呢?其實就是可讀可寫屬性罷了,至關於同時代理了gettersetter。因此其實你訪問ctx.redirect實際上訪問的是ctx.request.redirect,以此類推。須要注意的是,這裏的requestresponse不是nodejs原生的,是koa封裝事後的。

context.js就這麼簡單。

$4.request.js & response.js

request.jsresponse.js分別是對createServer回調函數接收的的requestresponse進行封裝。

先看request.js。還記得createContext嗎?咱們說過,他把Request.req設置成了原生的request。因此你能夠看到,不少方法其實本質就是在操做this.req,這一點和response.js相似,後面就不重複說了。

首先是一些個經常使用的屬性,header分別設置了gettersetter,都是對this.req.headers操做。headersheader如出一轍,並非用來區分單複數的(這有點兒坑,初學覺得headers是設置多個的)。接下來還有不少經常使用的屬性,就不一一介紹了,什麼urlmethod之類的,稍微熟悉點兒nodejs的同窗都可以實現出來。

值得注意的是queryquerystring,一個返回的是對象,一個是字符串哦。

你或許會問searchquerystring有啥區別。區別,emmmmn。。。多是爲了完整吧,畢竟express都有個search,koa也要提供。。。

另外須要說一下的是,這裏的不少屬性的操做涉及到了http協議的內容了,好比freshhttp是個很大的內容,不作講解。若是遇到看不懂的代碼,不妨去查看相關的http協議哦。

另外在idempotent你能夠看到!!~,這是個啥玩意兒???第一次看見都是一臉懵逼。這個其實就是位操做而已。咱們通常把!!看作一組,它的做用是把任意數據變成boolean值。這個操做其實很簡單,就是判斷是否是-1,若是是-1,那麼就是false;若是不是-1,那麼都是true。這個操做很巧妙。稍微解釋一下吧。

咱們假設數字是8位表示的,那麼-1的原碼就是1000 0001,反碼就是1111 1110,補碼就是1111 1111。而~操做符是取反的意思,因此取反之後就成了0000 0000。計算機存儲負數是用的補碼(相關知識能夠取google搜索一下),因此最後就是判斷是否是-1的。

有幾個accept打頭的函數能夠忽略,這幾個函數是判斷是否符合指定類型、語言、編碼的,它內部調用了一個accepts的庫。這個功能其實用得不多,但涉及編碼之類較爲複雜的知識了。

在最後的代碼裏面,request.js提供了get方法,其實就是獲取header

讓咱們轉到response.js裏面去。劈頭蓋臉一看,和request.js差很少,知識封裝的方法和屬性不同而已。

首先是socket,這個是套接字,http模塊的底層,不講解。

header調用的是getHeaders(),獲取已經設置好的全部的header,同headersstatus設置狀態碼,好比200,404之類的。值得一提的是,一般狀況下使用nodejs的statusCode還須要你設置一個statusMessage來提示用戶發生了什麼錯誤,koa會智能的爲你設置好。好比你設置好了status爲404,會自動把statusMessage設置成404 not found。這是由於koa使用了statuses這個庫,這個庫會根據你傳入的狀態碼返回指定的狀態信息。

接下來是Response最重要的一個屬性,也就是body。對body的操做反應在內部的_body上面。body的setter作了各類處理。好比判斷傳給body的值是否是空,若是是空就進行一些操做。比較有意思的是,body的setter會在你沒有設置Content-Type時,判斷一下傳遞給body的數據是個什麼類型。

  1. 當傳遞的是字符串時,它使用了一個正則:/^\s*</來判斷是html仍是text。很明顯,這個正則很簡陋,在不少狀況下並不能正確判斷,好比<----就會被判斷成html。因此body的類型仍是要手動的設置type才行。

  2. 當傳遞的是buffer的時候,把類型設置稱爲bin(記住,type是koa封裝事後的屬性,它會根據你設置的type自動匹配最佳的Content-Type。好比你把type設置成'json',實際上最後的Content-Type會是application/json。後面會說實現方法的)。

  3. 當傳遞的是個stream(經過判斷它是否擁有pipe這個函數),先綁定回調函數,當res發送完畢的時候,銷燬這個stream,避免內存浪費。接着錯誤處理。接着判斷如下如今這個stream和原來body的值是否相同,若是不是的話,那就移除Content-Length,交給nodejs本身處理。(實際上nodejs也並不會處理,爲啥呢?header必須在正文發送以前發送,可是Stream的字節數要在發送完才知道,so,你懂得)。最後把type設置成bin,由於stream是二進制的數據流。

  4. 不知足以上三種,那麼就只能是json了唄(別問我爲何不判斷boolean,symbol這些,誰會沒事兒幹發送這些玩意兒?)。移除Content-Type(你可能想問,爲啥呢?由於你傳遞的其實是個Object對象,須要stringify以後才能知道它的字節數,這個其實會在後面處理的)。設置typejson

至此,bodysetter分析得差很少了。

接着到了length,這個其實就是封裝了設置Content-Length的方法。反卻是它的getter有點兒複雜來着。咱們不妨細看一下。

首先判斷Content-Length設置沒有,有就直接返回,有的話那就分狀況讀body的字節數。當body是stream的時候,啥都不返回。

這裏有個奇淫巧技,~~這個玩意兒能夠用來把字符串轉換成數字。爲何呢?!我就知道你要問!其實這個東西要對js有比較高的理解才行的,js裏面存在隱式類型轉換,當遇到一些特殊的操做符,例如位操做符,會把字符串轉換成數字來進行計算。其實+這個符號也能夠進行字符串轉數字(str+str這個不算哈,這個不會進行隱式類型轉換),那麼爲何要用~~而不是+呢?我思索再三,認爲是做者可能不瞭解。但實際上,~~要比'+'安全,+在遇到不能轉換的式子時,會返回NaN,而~~是基於位操做的,返回安全的0。

跳到type,這個和length相似,是對Content-Type實現的封裝。這裏引用了一個mime-types的庫,這個庫功能很強大,能夠根據傳入的參數返回指定的mime類型。好比咱們設置type爲json,會去調用mime-typescontentType函數,而後返回json類型的mime,也就是application/json

request.js同樣,response.js一樣封裝了setget兩個方法,用於設置和讀取header

inspecttoJSON又來了。。。

response.js不少的屬性和方法並無說起,這是由於這些屬性和方法就是作了簡單的封裝而已,方便調用,很容易理解。

好了,至此response.js也分析完了。

$5.koa中間件原理分析

koa的中間件原理很強大,實現起來其實並非特別複雜。記得怎麼使用koa中間件嗎?只須要use一個函數就好了!這個函數接受兩個參數,一個是context,咱們已經分析過了。另外一個是next,這個就是中間件的核心了。

讓咱們回到開頭,看看use怎麼實現的。不看錯誤處理的那些內容,這裏先對fn進行了一次判斷。判斷什麼呢?判斷fn是否是generator function。koa官方建議是不要繼續使用Generator function了,換成了async function。若是你使用的是Generator function,那麼內部會調用co模塊來處理。因爲處理內容比較晦澀,且與正文關係不大,故不做講解。咱們假設全部的中間件都是async function。

application維護了一個middleware的隊列,use方法把中間件推送進這個隊列,除此以外什麼都沒作。

還記得listen方法嗎?它調用了callback這個方法。最終的答案都在這裏了!

看到callback方法。首先,它對middleware隊列調用了compose方法。咱們打開compose對應的模塊,短短几十行代碼。

不看錯誤處理,那麼compose只有一個return語句,返回一個函數。這個函數有兩個參數contextnext,熟悉嗎?這不就是中間件函數嗎!別慌,接着往下看。

首先聲明一個index遊標,接着定義一個dispatch函數,而後默認返回dispatch(0)

dispatch函數用來分發中間件(和分發事件很像)。它接收一個數字,這個數字是中間件隊列中某個中間件的下標。首先先判斷一下有沒有越界,也就是index和傳入的i進行比較,沒有越界把遊標移動到當前分發的中間件。接着判斷i是否已經遍歷完了中間件隊列,i === middleware.length判斷。若是完了,就把fn設置成傳入的next。接着使用Promise.resolve,並調用當前中間件,注意

return Promise.resolve(fn(context, function next () {
    return dispatch(i + 1)
}))
複製代碼

這裏傳入中間件的第二個參數,也就是next,是一個函數,這個函數正是用來分發下一個事件的!!!中間件最重要的原理就在這裏,爲何能夠用next轉移控制權,邏輯就在這裏!

compose函數分析完畢了,記住compose的返回值,是一個相似中間件的函數。

回到applicationcallback方法中。定義了一個handleRequest函數而且直接返回,handleRequest其實就是http.createServer的回調函數。這個回調函數首先封裝一下createContext,上面已經講過了。接着調用了application上的handleRequest方法(別搞混了,這個是下面那個handleRequest方法)。

咱們看看handleRequest方法,它接受兩個參數,第一個是context,第二個是什麼呢?其實就是compose處理後的middleware中間件隊列。拋開一些’多餘‘的代碼不看,把它精簡成這樣:

handleRequest(ctx, fnMiddleware) {
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製代碼

記得fnMiddleware的返回值是什麼嗎?是dispatch(0)。那記得dispatch的返回值是什麼嗎?是一個Promise。咱們再來看看這個Promise

return Promise.resolve(fn(context, function next () {
    return dispatch(i + 1)
}))
複製代碼

好好想想,fn如今是第一個中間件,它先被調用了。在這個中間件裏面調用了next函數,也就是至關於調用了dispatch(i + 1),如此下去。這不就至關於依次調用了dispatch函數嗎?

最後一點,中間件是async function,你明白爲何要使用Promise了嗎?對了,就是爲了await。

最後的最後,就是respond這個方法了,這個方法實際上就是對statusCodeheader以及body進行處理,最後調用nodejs提供了發送數據的方法,向客戶端發送數據。最後調用ctx.end(body),結束本次http請求

那麼至此,koa中間件也就完了。

結語

koa的源碼並非十分複雜,有興趣的同窗能夠本身再看看。但願這篇文章能給你幫助。

推廣一下本身的GitHub,個人開源項目doxjs,有興趣的能夠看看,給個star之類的。

相關文章
相關標籤/搜索