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
對於nodejs
甚至是JavaScript
項目,第一件事兒就是看看它的package.json
。package.json
裏面能夠找到很多有用的信息。json
咱們打開koa@2.5.0
的目錄,發現它依賴了很多的庫。其實這些庫大多都十分簡單,koa
的編寫原則其實就是把功能分割到其餘的庫中去。咱們暫且先無論這些依賴。安全
咱們找到main
字段,這裏就是‘通往新世界的大門’了。順着main
打開lib/application.js
。服務器
好傢伙,一上來就是一大串的引入,這可不是什麼好東西,咱麼先不看這些東西。先看下面的代碼。
首先是定義了一個application
的類,接着在構造函數中定義了一些變量。咱們主要關注如下幾個變量,由於他們的用處最大:
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
複製代碼
Object.create
是用來克隆對象的,這裏克隆了三個對象,也是koa
最重要的三個對象request
,response
和context
。這三個對象幾乎就是koa
的所有內容了。待會兒會逐一分析。
咱們接着往下看,listen
函數你們都很熟悉了,就是用來監聽端口的。koa
的listen
函數也很簡單。
const server = http.createServer(this.callback());
return server.listen(...args);
複製代碼
短短兩行,對於nodejs
不熟的同窗,建議在這裏就打住了。其中this.callback()
是個什麼玩意兒呢?它返回一個函數,這個函數接收兩個參數request
和response
,也就是createServer
的回調函數,在中間件原理章節會更詳細介紹。
接着就是toJSSON
這個方法。JSON.stringify
調用的方法,目的是當你JSON
化一個application
實例時,返回指定的屬性,而並不是全部。這個用處不大,幾乎用不到。
inspect
也就是調用了toJSON
這個方法而已。
接着就是use
函數了。use
函數自己不是很複雜,可是use
函數做爲中間件的接口,背後的中間件卻有點兒複雜。爲此,本文在後面專門解讀了中間件相關的源代碼,這裏暫時跳過。
callback
,handleRequest
,respond
這幾個方法涉及中間件的,所以放到中間件的章節講。
createContext
這個方法是用來封裝context
的。這個context
就是你在使用koa
的use
方法,你傳遞的回調函數的第一個ctx
參數。createContext
執行的最重要的操做就是把context.request
設置成了Request
,把context.response
設置成了Response
。以及把Response.res
h和Request.req
分別設置成了原生的response
和request
。
爲何這樣說,這個就得追到context.js
和request.js
以及response.js
的代碼裏面了,先等等。
值得強調的是,這裏的Request
和Response
並非nodejs
裏面的,而是koa
封裝事後的。爲了區分原生的和koa
封裝好的,我把Request
和Response
稱爲封裝事後的,request
和response
稱爲原生的。你須要記住的是context.res
是指原生的response
,而context.response
則是封裝後的Response
。Request
以此類推。
封裝的東西看起來並無什麼高大上,無非是把經常使用的一些方法給簡化了。就像jquery
簡化了js
對dom
的操做同樣。
打開context.js
,代碼很少,可是含金量挺高的。首先是把proto
賦值成一個對象,這個對象也是模塊的導出值。
inspect
和toJSON
功能和application.js
裏面同樣,不作過多介紹了。
接着看到個assert
,這個和nodejs裏面的assert
實際上是差很少,它實際上是提供了一些斷言的操做。好比equal
,notEqual
,strictEqual
之類的。比較有意思的是,assert
提供了一個深度比較的方法deepEqual
,這個但是個好東西。js
裏面的深度比較一直是個比較麻煩的問題,有經驗的程序員會使用JSON
來比較,這裏提供了一種性能更好的方法。代碼其實不復雜,就是引用了deep-eqaul
這個庫而已,有興趣的能夠去看看哦。
跳過兩個關於錯誤處理的函數(本文不講解錯誤處理),來到了context.js
最精華的地方了。 這裏使用了delegate
這個庫。這是個啥?delegate
其實很簡單的,你甚至不須要去查看delegate
的源代碼,看我解釋就好了。
delegate
提供了一種相似Proxy
的手段,也就是代理。代理什麼?具體來講delegate(proto, 'response')
這段代碼的意思就是把proto
上的一些屬性代理到proto.response
上面去。具體是哪些代理呢?就是接下來排列工整的代碼作的了。delegate
區分了method
,getter
,access
等類型。前面兩個還好理解,就是方法和只讀屬性,第三個呢?其實就是可讀可寫屬性罷了,至關於同時代理了getter
和setter
。因此其實你訪問ctx.redirect
實際上訪問的是ctx.request.redirect
,以此類推。須要注意的是,這裏的request
和response
不是nodejs原生的,是koa
封裝事後的。
context.js
就這麼簡單。
request.js
和response.js
分別是對createServer
回調函數接收的的request
和response
進行封裝。
先看request.js
。還記得createContext
嗎?咱們說過,他把Request.req
設置成了原生的request
。因此你能夠看到,不少方法其實本質就是在操做this.req
,這一點和response.js
相似,後面就不重複說了。
首先是一些個經常使用的屬性,header
分別設置了getter
和setter
,都是對this.req.headers
操做。headers
和header
如出一轍,並非用來區分單複數的(這有點兒坑,初學覺得headers是設置多個的)。接下來還有不少經常使用的屬性,就不一一介紹了,什麼url
,method
之類的,稍微熟悉點兒nodejs的同窗都可以實現出來。
值得注意的是query
和querystring
,一個返回的是對象,一個是字符串哦。
你或許會問search
和querystring
有啥區別。區別,emmmmn。。。多是爲了完整吧,畢竟express都有個search,koa
也要提供。。。
另外須要說一下的是,這裏的不少屬性的操做涉及到了http
協議的內容了,好比fresh
。http
是個很大的內容,不作講解。若是遇到看不懂的代碼,不妨去查看相關的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
,同headers
。status
設置狀態碼,好比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的數據是個什麼類型。
當傳遞的是字符串時,它使用了一個正則:/^\s*</
來判斷是html仍是text。很明顯,這個正則很簡陋,在不少狀況下並不能正確判斷,好比<----
就會被判斷成html。因此body
的類型仍是要手動的設置type
才行。
當傳遞的是buffer的時候,把類型設置稱爲bin
(記住,type
是koa封裝事後的屬性,它會根據你設置的type
自動匹配最佳的Content-Type
。好比你把type
設置成'json',實際上最後的Content-Type
會是application/json
。後面會說實現方法的)。
當傳遞的是個stream(經過判斷它是否擁有pipe這個函數),先綁定回調函數,當res發送完畢的時候,銷燬這個stream,避免內存浪費。接着錯誤處理。接着判斷如下如今這個stream和原來body
的值是否相同,若是不是的話,那就移除Content-Length
,交給nodejs本身處理。(實際上nodejs也並不會處理,爲啥呢?header必須在正文發送以前發送,可是Stream的字節數要在發送完才知道,so,你懂得)。最後把type
設置成bin
,由於stream是二進制的數據流。
不知足以上三種,那麼就只能是json了唄(別問我爲何不判斷boolean,symbol這些,誰會沒事兒幹發送這些玩意兒?)。移除Content-Type
(你可能想問,爲啥呢?由於你傳遞的其實是個Object對象,須要stringify以後才能知道它的字節數,這個其實會在後面處理的)。設置type
成json
。
至此,body
的setter
分析得差很少了。
接着到了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-types
的contentType
函數,而後返回json
類型的mime
,也就是application/json
。
同request.js
同樣,response.js
一樣封裝了set
和get
兩個方法,用於設置和讀取header
。
inspect
和toJSON
又來了。。。
response.js
不少的屬性和方法並無說起,這是由於這些屬性和方法就是作了簡單的封裝而已,方便調用,很容易理解。
好了,至此response.js
也分析完了。
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
語句,返回一個函數。這個函數有兩個參數context
和next
,熟悉嗎?這不就是中間件函數嗎!別慌,接着往下看。
首先聲明一個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
的返回值,是一個相似中間件的函數。
回到application
的callback
方法中。定義了一個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
這個方法了,這個方法實際上就是對statusCode
,header
以及body
進行處理,最後調用nodejs提供了發送數據的方法,向客戶端發送數據。最後調用ctx.end(body)
,結束本次http請求
。
那麼至此,koa
中間件也就完了。
koa
的源碼並非十分複雜,有興趣的同窗能夠本身再看看。但願這篇文章能給你幫助。