Node.js深入分析Koa源碼
Koa 的主要代碼位于根目錄下的 lib 文件夾中,只有 4 個(gè)文件,去掉注釋后的源碼不到 1000 行,下面列出了這 4 個(gè)文件的主要功能。
- request.js:對(duì) http request 對(duì)象的封裝。
- response.js:對(duì) http response 對(duì)象的封裝。
- context.js:將上面兩個(gè)文件的封裝整合到 context 對(duì)象中
- application.js:項(xiàng)目的啟動(dòng)及中間件的加載。
1. Koa 的啟動(dòng)過(guò)程
首先回憶一下一個(gè) Koa 應(yīng)用的結(jié)構(gòu)是什么樣子的。
const Koa = require('Koa'); const app = new Koa(); //加載一些中間件 app.use(...); app.use(....); app.use(.....); app.listen(3000);
Koa 的啟動(dòng)過(guò)程大致分為以下三個(gè)步驟:
- 引入 Koa 模塊,調(diào)用構(gòu)造方法新建一個(gè)
app
對(duì)象。 - 加載中間件。
- 調(diào)用
listen
方法監(jiān)聽端口。
我們逐步來(lái)看上面三個(gè)步驟在源碼中的實(shí)現(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 分別是從其他三個(gè)文件夾中引入的 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
模塊,然后當(dāng)我們調(diào)用 Koa 的構(gòu)造函數(shù)時(shí),會(huì)初始化一些屬性和方法,例如以context/response/request
為原型創(chuàng)建的新的對(duì)象,還有管理中間件的 middleware
數(shù)組等。
2. 中間件的加載
中間件的本質(zhì)是一個(gè)函數(shù)。在 Koa 中,該函數(shù)通常具有 ctx
和 next
兩個(gè)參數(shù),分別表示封裝好的 res/req
對(duì)象以及下一個(gè)要執(zhí)行的中間件,當(dāng)有多個(gè)中間件的時(shí)候,本質(zhì)上是一種嵌套調(diào)用,就像洋蔥圖一樣。
Koa 和 Express 在調(diào)用上都是通過(guò)調(diào)用 app.use()
的方式來(lái)加載一個(gè)中間件,但內(nèi)部的實(shí)現(xiàn)卻大不相同,我們先來(lái)看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 中維持了一個(gè) middleware
的數(shù)組,如果有新的中間件被加載,就 push
到這個(gè)數(shù)組中,除此之外沒(méi)有任何多余的操作,相比之下,Express 的 use
方法就麻煩得多,讀者可以自行參閱其源碼。
此外,之前版本中該方法中還增加了 isGeneratorFunction
判斷,這是為了兼容 Koa1.x 的中間件而加上去的,在 Koa1.x 中,中間件都是 Generator
函數(shù),Koa2 使用的 async
函數(shù)是無(wú)法兼容之前的代碼的,因此 Koa2 提供了 convert
函數(shù)來(lái)進(jìn)行轉(zhuǎn)換,關(guān)于這個(gè)函數(shù)我們不再介紹。
if (isGeneratorFunction(fn)) { // ...... fn = convert(fn) }
接下來(lái)我們來(lái)看看對(duì)中間件的調(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
方法中,該方法是一個(gè)名為 Koa-compose
的第三方模塊https://github.com/Koajs/compose,我們可以看看其內(nèi)部是如何實(shí)現(xiàn)的。
該模塊只有一個(gè)方法 compose
,調(diào)用方式為 compose([a, b, c, ...])
,該方法接受一個(gè)中間件的數(shù)組作為參數(shù),返回的仍然是一個(gè)中間件(函數(shù)),可以將這個(gè)函數(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) } } } }
該方法的核心是一個(gè)遞歸調(diào)用的 dispatch
函數(shù),為了更好地說(shuō)明這個(gè)函數(shù)的工作原理,這里使用一個(gè)簡(jiǎn)單的自定義中間件作為例子來(lái)配合說(shuō)明。
function myMiddleware(context, next) { process.nextTick(function () { console.log('I am a middleware'); }) next(); }
可以看出這個(gè)中間件除了打印一條消息,然后調(diào)用 next
方法之外,沒(méi)有進(jìn)行任何操作,我們以該中間件為例,在 Koa 的 app.js 中使用 app.use
方法加載該中間件兩次。
const Koa = require('Koa'); const myMiddleware = require("./myMiddleware"); app.use(md1); app.use(dm2); app.listen(3000);
app
真正實(shí)例化是在調(diào)用 listen
方法之后,那么中間件的加載同樣位于 listen
方法之后。
那么 compose
方法的實(shí)際調(diào)用為 compose[myMiddleware,myMiddleware]
,在執(zhí)行 dispatch(0)
時(shí),該方法實(shí)際可以簡(jiǎn)化為:
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
啟動(dòng)過(guò)程中的最后一步,讀者會(huì)疑惑:為什么這么一行也要算作單獨(dú)的步驟,事實(shí)上,上面的兩步都是為了 app
的啟動(dòng)做準(zhǔn)備,整個(gè) Koa 應(yīng)用的啟動(dòng)是通過(guò) listen
方法來(lái)完成的。下面是 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
對(duì)象。 - 調(diào)用中間件對(duì)
ctx
對(duì)象進(jìn)行處理。
/** * 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()
我們前面也提到過(guò),Koa 對(duì)中間件調(diào)用的實(shí)現(xiàn)本質(zhì)上是嵌套的 promise.resolve
方法,我們可以寫一個(gè)簡(jiǎn)單的例子。
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
對(duì)象,經(jīng)過(guò)中間件的處理后,ctx
的值會(huì)發(fā)生相應(yīng)的變化。
我們定義了 md1
和 md2
兩個(gè)中間件,md1
沒(méi)有做任何操作,只調(diào)用了 next
方法,md2
則是對(duì) ctx
執(zhí)行加一的操作,那么在最后的 then
方法中,我們期望 ctx
的值為 2。
我們可以嘗試運(yùn)行上面的代碼,最后的結(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)
相當(dāng)于之前代碼第 8 行聲明的 Promise
對(duì)象 p
,被中間件方法修改后的 ctx
對(duì)象被 then
方法傳給 handleResponse
方法返回給客戶端。
每個(gè)中間件方法都會(huì)返回一個(gè) Promise
對(duì)象,里面包含的是對(duì) ctx
的修改,通過(guò)調(diào)用 next
方法來(lái)調(diào)用下一個(gè)中間件。
fn(context, function next () { return dispatch(i + 1); })
再通過(guò) return
關(guān)鍵字將修改后的 ctx
對(duì)象作為 resolve
的參數(shù)返回。
如果多個(gè)中間件同時(shí)操作了 ctx
對(duì)象,那么就有必要使用 return
關(guān)鍵字將操作的結(jié)果返回到上一級(jí)調(diào)用的中間件里。
事實(shí)上,如果讀者去讀 Koa-router
或者 Koa-static
的源碼,也會(huì)發(fā)現(xiàn)它們都是使用 return next
方法。
5. 關(guān)于 Can’t set headers after they are sent.
這是使用 Express 或者 Koa 常見的錯(cuò)誤之一,其原因如字面意思,對(duì)于同一個(gè) HTTP 請(qǐng)求重復(fù)發(fā)送了 HTTP HEADER 。服務(wù)器在處理HTTP 請(qǐng)求時(shí)會(huì)先發(fā)送一個(gè)響應(yīng)頭(使用 writeHead
或 setHeader
方法),然后發(fā)送主體內(nèi)容(通過(guò) send
或者 end
方法),如果對(duì)一個(gè) HTTP 請(qǐng)求調(diào)用了兩次 writeHead
方法,就會(huì)出現(xiàn) Can't set headers after they are sent
的錯(cuò)誤提示,例如下面的例子:
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'); }
試著訪問(wèn) localhost:5000
就會(huì)得到錯(cuò)誤信息,這個(gè)例子太過(guò)直白了。下面是一個(gè) Express 中的例子,由于中間件可能包含異步操作,因此有時(shí)錯(cuò)誤的原因比較隱蔽。
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);
運(yùn)行上面的代碼,訪問(wèn) http://localhost:3000/foo 會(huì)產(chǎn)生同樣的錯(cuò)誤,原因也很簡(jiǎn)單,在請(qǐng)求返回之后,setTimeout
內(nèi)部的 redirect
會(huì)對(duì)一個(gè)已經(jīng)發(fā)送出去的 response
進(jìn)行修改,就會(huì)出現(xiàn)錯(cuò)誤,在實(shí)際項(xiàng)目中不會(huì)像 setTimeout
這么明顯,可能是一個(gè)數(shù)據(jù)庫(kù)操作或者其他的異步操作,需要特別注意。
6. Context 對(duì)象的實(shí)現(xiàn)
關(guān)于 ctx
對(duì)象是如何得到 request/response
對(duì)象中的屬性和方法的,可以閱讀 context.js 的源碼,其核心代碼如下所示。此外,delegate
模塊還廣泛運(yùn)用在了 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
是一個(gè) Node 第三方模塊,作用是把一個(gè)對(duì)象中的屬性和方法委托到另一個(gè)對(duì)象上。
讀者可以訪問(wèn)該模塊的項(xiàng)目地址 https://github.com/tj/node-delegates,然后就會(huì)發(fā)現(xiàn)該模塊的主要貢獻(xiàn)者還是TJ Holowaychuk。
這個(gè)模塊的代碼同樣非常簡(jiǎn)單,源代碼只有 100 多行,我們這里詳細(xì)介紹一下。
在上面的代碼中,我們使用了如下三個(gè)方法:
- method:用于委托方法到目標(biāo)對(duì)象上。
- access:綜合
getter
和setter
,可以對(duì)目標(biāo)進(jìn)行讀寫。 - getter:為目標(biāo)屬性生成一個(gè)訪問(wèn)器,可以理解成復(fù)制了一個(gè)只讀屬性到目標(biāo)對(duì)象上。
getter
和 setter
這兩個(gè)方法是用來(lái)控制對(duì)象的讀寫屬性的,下面是 method
方法與 access
方法的實(shí)現(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
方法將原目標(biāo)的方法綁定到目標(biāo)對(duì)象上。
下面是 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ù)接收兩個(gè)參數(shù),分別是源對(duì)象和目標(biāo)對(duì)象。
/** * 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
對(duì)象在內(nèi)部維持了一些數(shù)組,分別表示委托得到的目標(biāo)對(duì)象和方法。
關(guān)于動(dòng)態(tài)加載中間件
在某些應(yīng)用場(chǎng)景中,開發(fā)者可能希望能夠動(dòng)態(tài)加載中間件,例如當(dāng)路由接收到某個(gè)請(qǐng)求后再去加載對(duì)應(yīng)的中間件,但在 Koa 中這是無(wú)法做到的。原因其實(shí)已經(jīng)包含在前面的內(nèi)容了,Koa 應(yīng)用唯一一次加載所有中間件是在調(diào)用 listen
方法的時(shí)候,即使后面再調(diào)用 app.use
方法,也不會(huì)生效了。
7. Koa 的優(yōu)缺點(diǎn)
通過(guò)上面的內(nèi)容,相信讀者已經(jīng)對(duì) Koa 有了大概的認(rèn)識(shí),和 Express 相比,Koa 的優(yōu)勢(shì)在于精簡(jiǎn),它剝離了所有的中間件,并且對(duì)中間件的執(zhí)行做了很大的優(yōu)化。
一個(gè)經(jīng)驗(yàn)豐富的 Express 開發(fā)者想要轉(zhuǎn)到 Koa 上并不需要很大的成本,唯一需要注意的就是中間件執(zhí)行的策略會(huì)有差異,這可能會(huì)帶來(lái)一段時(shí)間的不適應(yīng)。
現(xiàn)在我們來(lái)說(shuō)說(shuō) Koa 的缺點(diǎn),剝離中間件雖然是個(gè)優(yōu)點(diǎn),但也讓不同中間件的組合變得麻煩起來(lái),Express 經(jīng)過(guò)數(shù)年的沉淀,各種用途的中間件已經(jīng)很成熟;而 Koa 不同,Koa2.0 推出的時(shí)間還很短,適配的中間件也不完善,有時(shí)單獨(dú)使用各種中間件還好,但一旦組合起來(lái),可能出現(xiàn)不能正常工作的情況。
舉個(gè)例子,如果想同時(shí)使用 router
和 views
兩個(gè)中間件,就要在 render
方法前加上 return
關(guān)鍵字(和 return next()
一個(gè)道理),對(duì)于剛接觸 Koa 的開發(fā)者可能要花很長(zhǎng)時(shí)間才能定位問(wèn)題所在。再例如前面的 koa-session
和 Koa-router
,我初次接觸這兩個(gè)中間件時(shí)也著實(shí)花了一些功夫來(lái)將他們正確地組合在一塊。雖然中間件概念的引入讓Node開發(fā)變得像搭積木一樣,但積木之間如果不能很順利地拼接在一塊的話,也會(huì)增加開發(fā)成本。
到此這篇關(guān)于Node.js深入分析Koa源碼的文章就介紹到這了,更多相關(guān)Node.js Koa內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你如何用Node實(shí)現(xiàn)API的轉(zhuǎn)發(fā)(某音樂(lè))
這篇文章主要介紹了教你如何用Node實(shí)現(xiàn)API的轉(zhuǎn)發(fā)(某音樂(lè)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09Windows安裝Node.js報(bào)錯(cuò):2503、2502的解決方法
這篇文章主要給大家介紹了關(guān)于在Windows系統(tǒng)下安裝Node.js報(bào)錯(cuò):2503、2502的解決方法,文中將解決的方法一步步介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10從零開始學(xué)習(xí)Node.js系列教程二:文本提交與顯示方法
這篇文章主要介紹了Node.js文本提交與顯示方法,結(jié)合實(shí)例形式分析了nodejs基于http的文本提交、傳輸與顯示相關(guān)操作技巧,需要的朋友可以參考下2017-04-04Express實(shí)現(xiàn)定時(shí)發(fā)送郵件的示例代碼
在開發(fā)中我們有時(shí)候需要每隔?一段時(shí)間發(fā)送一次電子郵件,或者在某個(gè)特定的時(shí)間進(jìn)行發(fā)送郵件,無(wú)需手動(dòng)去操作,基于這樣的情況下我們需要用到了定時(shí)任務(wù)。本文就來(lái)用Express實(shí)現(xiàn)定時(shí)發(fā)送郵件吧2023-04-04nodejs+express實(shí)現(xiàn)文件上傳下載管理網(wǎng)站
這篇文章主要為大家詳細(xì)介紹了nodejs+express實(shí)現(xiàn)文件上傳下載管理的網(wǎng)站,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03node.js中express模塊創(chuàng)建服務(wù)器和http模塊客戶端發(fā)請(qǐng)求
今天小編就為大家分享一篇關(guān)于node.js中express模塊創(chuàng)建服務(wù)器和http模塊客戶端發(fā)請(qǐng)求,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03