Node.js深入分析Koa源碼
Koa 的主要代碼位于根目錄下的 lib 文件夾中,只有 4 個文件,去掉注釋后的源碼不到 1000 行,下面列出了這 4 個文件的主要功能。
- request.js:對 http request 對象的封裝。
- response.js:對 http response 對象的封裝。
- context.js:將上面兩個文件的封裝整合到 context 對象中
- application.js:項目的啟動及中間件的加載。
1. Koa 的啟動過程
首先回憶一下一個 Koa 應(yīng)用的結(jié)構(gòu)是什么樣子的。
const Koa = require('Koa'); const app = new Koa(); //加載一些中間件 app.use(...); app.use(....); app.use(.....); app.listen(3000);
Koa 的啟動過程大致分為以下三個步驟:
- 引入 Koa 模塊,調(diào)用構(gòu)造方法新建一個
app
對象。 - 加載中間件。
- 調(diào)用
listen
方法監(jiān)聽端口。
我們逐步來看上面三個步驟在源碼中的實現(xiàn)。
首先是類和構(gòu)造函數(shù)的定義,這部分代碼位于 application.js 中。
// application.js const response = require('./response') const context = require('./context') const request = require('./request') const Emitter = require('events') const util = require('util') // ...... 其他模塊 module.exports = class Application extends Emitter { 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 = [] // 下面的 context,request,response 分別是從其他三個文件夾中引入的 this.context = Object.create(context) this.request = Object.create(request) this.response = Object.create(response) // util.inspect.custom support for node 6+ /* istanbul ignore else */ if (util.inspect.custom) { this[util.inspect.custom] = this.inspect } } // ...... 其他類方法 }
首先我們注意到該類繼承于 Events
模塊,然后當我們調(diào)用 Koa 的構(gòu)造函數(shù)時,會初始化一些屬性和方法,例如以context/response/request
為原型創(chuàng)建的新的對象,還有管理中間件的 middleware
數(shù)組等。
2. 中間件的加載
中間件的本質(zhì)是一個函數(shù)。在 Koa 中,該函數(shù)通常具有 ctx
和 next
兩個參數(shù),分別表示封裝好的 res/req
對象以及下一個要執(zhí)行的中間件,當有多個中間件的時候,本質(zhì)上是一種嵌套調(diào)用,就像洋蔥圖一樣。
Koa 和 Express 在調(diào)用上都是通過調(diào)用 app.use()
的方式來加載一個中間件,但內(nèi)部的實現(xiàn)卻大不相同,我們先來看application.js 中相關(guān)方法的定義。
/** * Use the given middleware `fn`. * * Old-style middleware will be converted. * * @param {Function} fn * @return {Application} self * @api public */ use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') debug('use %s', fn._name || fn.name || '-') this.middleware.push(fn) return this }
Koa 在 application.js 中維持了一個 middleware
的數(shù)組,如果有新的中間件被加載,就 push
到這個數(shù)組中,除此之外沒有任何多余的操作,相比之下,Express 的 use
方法就麻煩得多,讀者可以自行參閱其源碼。
此外,之前版本中該方法中還增加了 isGeneratorFunction
判斷,這是為了兼容 Koa1.x 的中間件而加上去的,在 Koa1.x 中,中間件都是 Generator
函數(shù),Koa2 使用的 async
函數(shù)是無法兼容之前的代碼的,因此 Koa2 提供了 convert
函數(shù)來進行轉(zhuǎn)換,關(guān)于這個函數(shù)我們不再介紹。
if (isGeneratorFunction(fn)) { // ...... fn = convert(fn) }
接下來我們來看看對中間件的調(diào)用。
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ 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 }
可以看出關(guān)于中間件的核心邏輯應(yīng)該位于 compose
方法中,該方法是一個名為 Koa-compose
的第三方模塊https://github.com/Koajs/compose,我們可以看看其內(nèi)部是如何實現(xiàn)的。
該模塊只有一個方法 compose
,調(diào)用方式為 compose([a, b, c, ...])
,該方法接受一個中間件的數(shù)組作為參數(shù),返回的仍然是一個中間件(函數(shù)),可以將這個函數(shù)看作是之前加載的全部中間件的功能集合。
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ 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!') } /** * @param {Object} context * @return {Promise} * @api public */ 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) } } } }
該方法的核心是一個遞歸調(diào)用的 dispatch
函數(shù),為了更好地說明這個函數(shù)的工作原理,這里使用一個簡單的自定義中間件作為例子來配合說明。
function myMiddleware(context, next) { process.nextTick(function () { console.log('I am a middleware'); }) next(); }
可以看出這個中間件除了打印一條消息,然后調(diào)用 next
方法之外,沒有進行任何操作,我們以該中間件為例,在 Koa 的 app.js 中使用 app.use
方法加載該中間件兩次。
const Koa = require('Koa'); const myMiddleware = require("./myMiddleware"); app.use(md1); app.use(dm2); app.listen(3000);
app
真正實例化是在調(diào)用 listen
方法之后,那么中間件的加載同樣位于 listen
方法之后。
那么 compose
方法的實際調(diào)用為 compose[myMiddleware,myMiddleware]
,在執(zhí)行 dispatch(0)
時,該方法實際可以簡化為:
function compose(middleware) { return function (context, next) { try { return Promise.resolve(md1(context, function next() { return Promise.resolve(md2(context, function next() { })) })) } catch (err) { return Promise.reject(err) } } }
可以看出 compose
的本質(zhì)仍是嵌套的中間件。
3. listen() 方法
這是 app
啟動過程中的最后一步,讀者會疑惑:為什么這么一行也要算作單獨的步驟,事實上,上面的兩步都是為了 app
的啟動做準備,整個 Koa 應(yīng)用的啟動是通過 listen
方法來完成的。下面是 application.js 中 listen
方法的定義。
/** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {Server} * @api public */ listen(...args) { debug('listen') const server = http.createServer(this.callback()) return server.listen(...args) }
上面的代碼就是 listen
方法的內(nèi)容,可以看出第 3 行才真正調(diào)用了 http.createServer
方法建立了 http
服務(wù)器,參數(shù)為上節(jié) callback
方法返回的 handleRequest
方法,源碼如下所示,該方法做了兩件事:
- 封裝
request
和response
對象。 - 調(diào)用中間件對
ctx
對象進行處理。
/** * Handle request in callback. * * @api private */ 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) }
4. next()與return next()
我們前面也提到過,Koa 對中間件調(diào)用的實現(xiàn)本質(zhì)上是嵌套的 promise.resolve
方法,我們可以寫一個簡單的例子。
let ctx = 1; const md1 = function (ctx, next) { next(); } const md2 = function (ctx, next) { return ++ctx; } const p = Promise.resolve( mdl(ctx, function next() { return Promise.resolve( md2(ctx, function next() { //更多的中間件... }) ) }) ) p.then(function (ctx) { console.log(ctx); })
代碼在第一行定義的變量 ctx
,我們可以將其看作 Koa 中的 ctx
對象,經(jīng)過中間件的處理后,ctx
的值會發(fā)生相應(yīng)的變化。
我們定義了 md1
和 md2
兩個中間件,md1
沒有做任何操作,只調(diào)用了 next
方法,md2
則是對 ctx
執(zhí)行加一的操作,那么在最后的 then
方法中,我們期望 ctx
的值為 2。
我們可以嘗試運行上面的代碼,最后的結(jié)果卻是 undefined
,在 md1
的 next
方法前加上 return
關(guān)鍵字后,就能得到正常的結(jié)果了。
在 Koa 的源碼 application.js 中,callback
方法的最后一行:
/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ 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 } /** * Handle request in callback. * * @api private */ 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) }
中的 fnMiddleware(ctx)
相當于之前代碼第 8 行聲明的 Promise
對象 p
,被中間件方法修改后的 ctx
對象被 then
方法傳給 handleResponse
方法返回給客戶端。
每個中間件方法都會返回一個 Promise
對象,里面包含的是對 ctx
的修改,通過調(diào)用 next
方法來調(diào)用下一個中間件。
fn(context, function next () { return dispatch(i + 1); })
再通過 return
關(guān)鍵字將修改后的 ctx
對象作為 resolve
的參數(shù)返回。
如果多個中間件同時操作了 ctx
對象,那么就有必要使用 return
關(guān)鍵字將操作的結(jié)果返回到上一級調(diào)用的中間件里。
事實上,如果讀者去讀 Koa-router
或者 Koa-static
的源碼,也會發(fā)現(xiàn)它們都是使用 return next
方法。
5. 關(guān)于 Can’t set headers after they are sent.
這是使用 Express 或者 Koa 常見的錯誤之一,其原因如字面意思,對于同一個 HTTP 請求重復發(fā)送了 HTTP HEADER 。服務(wù)器在處理HTTP 請求時會先發(fā)送一個響應(yīng)頭(使用 writeHead
或 setHeader
方法),然后發(fā)送主體內(nèi)容(通過 send
或者 end
方法),如果對一個 HTTP 請求調(diào)用了兩次 writeHead
方法,就會出現(xiàn) Can't set headers after they are sent
的錯誤提示,例如下面的例子:
const http = require("http"); http.createServer(function (req, res) { res.setHeader('Content-Type', 'text/html'); res.end('ok'); resend(req, res); // 在響應(yīng)結(jié)束后再次發(fā)送響應(yīng)信息 }).listen(5000); function resend(req, res) { res.setHeader('Content-Type', 'text/html'); res.end('error'); }
試著訪問 localhost:5000
就會得到錯誤信息,這個例子太過直白了。下面是一個 Express 中的例子,由于中間件可能包含異步操作,因此有時錯誤的原因比較隱蔽。
const express = require('express'); const app = express(); app.use(function (req, res, next) { setTimeout(function () { res.redirect("/bar"); }, 1000); next(); }); app.get("/foo", function (req, res) { res.end("foo"); }); app.get("/bar", function (req, res) { res.end("bar"); }); app.listen(3000);
運行上面的代碼,訪問 http://localhost:3000/foo 會產(chǎn)生同樣的錯誤,原因也很簡單,在請求返回之后,setTimeout
內(nèi)部的 redirect
會對一個已經(jīng)發(fā)送出去的 response
進行修改,就會出現(xiàn)錯誤,在實際項目中不會像 setTimeout
這么明顯,可能是一個數(shù)據(jù)庫操作或者其他的異步操作,需要特別注意。
6. Context 對象的實現(xiàn)
關(guān)于 ctx
對象是如何得到 request/response
對象中的屬性和方法的,可以閱讀 context.js 的源碼,其核心代碼如下所示。此外,delegate
模塊還廣泛運用在了 Koa 的各種中間件中。
const delegate = require('delegates') delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('has') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable')
delegate
是一個 Node 第三方模塊,作用是把一個對象中的屬性和方法委托到另一個對象上。
讀者可以訪問該模塊的項目地址 https://github.com/tj/node-delegates,然后就會發(fā)現(xiàn)該模塊的主要貢獻者還是TJ Holowaychuk。
這個模塊的代碼同樣非常簡單,源代碼只有 100 多行,我們這里詳細介紹一下。
在上面的代碼中,我們使用了如下三個方法:
- method:用于委托方法到目標對象上。
- access:綜合
getter
和setter
,可以對目標進行讀寫。 - getter:為目標屬性生成一個訪問器,可以理解成復制了一個只讀屬性到目標對象上。
getter
和 setter
這兩個方法是用來控制對象的讀寫屬性的,下面是 method
方法與 access
方法的實現(xiàn)。
/** * Delegate method `name`. * * @param {String} name * @return {Delegator} self * @api public */ Delegator.prototype.method = function(name){ var proto = this.proto; var target = this.target; this.methods.push(name); proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; return this; };
method
方法中使用 apply
方法將原目標的方法綁定到目標對象上。
下面是 access
方法的定義,綜合了 getter
方法和 setter
方法。
/** * Delegator accessor `name`. * * @param {String} name * @return {Delegator} self * @api public */ Delegator.prototype.access = function(name){ return this.getter(name).setter(name); }; /** * Delegator getter `name`. * * @param {String} name * @return {Delegator} self * @api public */ Delegator.prototype.getter = function(name){ var proto = this.proto; var target = this.target; this.getters.push(name); proto.__defineGetter__(name, function(){ return this[target][name]; }); return this; }; /** * Delegator setter `name`. * * @param {String} name * @return {Delegator} self * @api public */ Delegator.prototype.setter = function(name){ var proto = this.proto; var target = this.target; this.setters.push(name); proto.__defineSetter__(name, function(val){ return this[target][name] = val; }); return this; };
最后是 delegate
的構(gòu)造函數(shù),該函數(shù)接收兩個參數(shù),分別是源對象和目標對象。
/** * Initialize a delegator. * * @param {Object} proto * @param {String} target * @api public */ function Delegator(proto, target) { if (!(this instanceof Delegator)) return new Delegator(proto, target); this.proto = proto; this.target = target; this.methods = []; this.getters = []; this.setters = []; this.fluents = []; }
可以看出 deletgate
對象在內(nèi)部維持了一些數(shù)組,分別表示委托得到的目標對象和方法。
關(guān)于動態(tài)加載中間件
在某些應(yīng)用場景中,開發(fā)者可能希望能夠動態(tài)加載中間件,例如當路由接收到某個請求后再去加載對應(yīng)的中間件,但在 Koa 中這是無法做到的。原因其實已經(jīng)包含在前面的內(nèi)容了,Koa 應(yīng)用唯一一次加載所有中間件是在調(diào)用 listen
方法的時候,即使后面再調(diào)用 app.use
方法,也不會生效了。
7. Koa 的優(yōu)缺點
通過上面的內(nèi)容,相信讀者已經(jīng)對 Koa 有了大概的認識,和 Express 相比,Koa 的優(yōu)勢在于精簡,它剝離了所有的中間件,并且對中間件的執(zhí)行做了很大的優(yōu)化。
一個經(jīng)驗豐富的 Express 開發(fā)者想要轉(zhuǎn)到 Koa 上并不需要很大的成本,唯一需要注意的就是中間件執(zhí)行的策略會有差異,這可能會帶來一段時間的不適應(yīng)。
現(xiàn)在我們來說說 Koa 的缺點,剝離中間件雖然是個優(yōu)點,但也讓不同中間件的組合變得麻煩起來,Express 經(jīng)過數(shù)年的沉淀,各種用途的中間件已經(jīng)很成熟;而 Koa 不同,Koa2.0 推出的時間還很短,適配的中間件也不完善,有時單獨使用各種中間件還好,但一旦組合起來,可能出現(xiàn)不能正常工作的情況。
舉個例子,如果想同時使用 router
和 views
兩個中間件,就要在 render
方法前加上 return
關(guān)鍵字(和 return next()
一個道理),對于剛接觸 Koa 的開發(fā)者可能要花很長時間才能定位問題所在。再例如前面的 koa-session
和 Koa-router
,我初次接觸這兩個中間件時也著實花了一些功夫來將他們正確地組合在一塊。雖然中間件概念的引入讓Node開發(fā)變得像搭積木一樣,但積木之間如果不能很順利地拼接在一塊的話,也會增加開發(fā)成本。
到此這篇關(guān)于Node.js深入分析Koa源碼的文章就介紹到這了,更多相關(guān)Node.js Koa內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你如何用Node實現(xiàn)API的轉(zhuǎn)發(fā)(某音樂)
這篇文章主要介紹了教你如何用Node實現(xiàn)API的轉(zhuǎn)發(fā)(某音樂),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09Windows安裝Node.js報錯:2503、2502的解決方法
這篇文章主要給大家介紹了關(guān)于在Windows系統(tǒng)下安裝Node.js報錯:2503、2502的解決方法,文中將解決的方法一步步介紹的非常詳細,需要的朋友可以參考借鑒,下面隨著小編來一起學習學習吧。2017-10-10Express實現(xiàn)定時發(fā)送郵件的示例代碼
在開發(fā)中我們有時候需要每隔?一段時間發(fā)送一次電子郵件,或者在某個特定的時間進行發(fā)送郵件,無需手動去操作,基于這樣的情況下我們需要用到了定時任務(wù)。本文就來用Express實現(xiàn)定時發(fā)送郵件吧2023-04-04nodejs+express實現(xiàn)文件上傳下載管理網(wǎng)站
這篇文章主要為大家詳細介紹了nodejs+express實現(xiàn)文件上傳下載管理的網(wǎng)站,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03node.js中express模塊創(chuàng)建服務(wù)器和http模塊客戶端發(fā)請求
今天小編就為大家分享一篇關(guān)于node.js中express模塊創(chuàng)建服務(wù)器和http模塊客戶端發(fā)請求,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03