koa是一個輕量級的web應用框架。其實現很是精簡和優雅,核心代碼僅有區區一百多行,很是值得咱們去細細品味和學習。javascript
在開始分析源碼以前先上demo~java
const Koa = require('../lib/application'); const app = new Koa(); app.use(async (ctx, next) => { console.log('m1-1'); await next(); console.log('m1-2'); }); app.use(async (ctx, next) => { console.log('m2-1'); await next(); console.log('m2-2'); }); app.use(async (ctx, next) => { console.log('m3-1'); ctx.body = 'there is a koa web app'; await next(); console.log('m3-2'); }); app.listen(8001); 複製代碼
上面代碼最終會在控制檯依次輸出node
m1-1
m2-1
m3-1
m3-2
m2-2
m1-2
複製代碼
當在中間件中調用next()
時,會中止當前中間件的執行,轉而進行下一個中間件。當下一箇中間件執行完後,纔會繼續執行next()
後面的邏輯。web
咱們改一下第一個中間件的代碼,以下所示:json
app.use(async (ctx, next) => { console.log('m1-1'); // await next(); console.log('m1-2'); }); 複製代碼
當把第一個中間件的await next()
註釋後,再次執行,在控制檯的輸出以下:數組
m1-1
m2-1
複製代碼
顯然,若是不執行next()
方法,代碼將只會執行到當前的中間件,不事後面還有多少箇中間件,都不會執行。bash
這個next
爲什麼會具備這樣的魔力呢,下面讓咱們開始愉快地分析koa的源碼,一探究竟~markdown
分析源碼以前咱們先來看一下koa的目錄結構,koa的實現文件只有4個,這4個文件都在lib目錄中。cookie
application.js
— 定義了一個類,這個類定義了koa實例的方法和屬性context.js
— 定義了一個proto對象,並對proto中的屬性進行代理。中間件中使用的ctx對象,其實就是繼承自protorequest.js
— 定義了一個對象,該對象基於原生的req拓展了一些屬性和方法response.js
- 定義了一個對象,該對象基於原生的res拓展了一些屬性和方法經過package.json文件得知,koa的入口文件是lib/application.js,咱們先來看一下這個文件作了什麼。app
打開application.js
查看源碼能夠發現,這個文件主要就是定義了一個類,同時定義了一些方法。
module.exports = class Application extends Emitter { constructor() { super(); this.middleware = []; // 中間件數組 } listen (...args) { // 啓用一個http server並監聽指定端口 const server = http.createServer(this.callback()); return server.listen(...args); } use (fn) { // 把中間添加到中間件數組 this.middleware.push(fn); return this; } } 複製代碼
咱們建立完一個koa對象以後,一般只會使用兩個方法,一個是listen
,一個是use
。listen負責啓動一個http server並監聽指定端口,use用來添加咱們的中間件。
當調用listen
方法時,會建立一個http server,這個http server須要一個回調函數,當有請求過來時執行。上面代碼中的this.callback()
就是用來返回這樣的一個函數:這個函數會讀取應用全部的中間件,使它們按照傳入的順序依次執行,最後響應請求並返回結果。
callback
方法的核心代碼以下:
callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } 複製代碼
callback
函數會在應用啓動時執行一次,而且返回一個函數handleRequest
。每當有請求過來時,handleRequest
都會被調用。咱們將callback
拆分爲三個流程去分析:
fn
,在fn
函數內部會依次執行this.middleware
中的中間件(是否所有執行,取決因而否有調用next
函數執行下一個中間件)createContext
生成一個可供中間件使用的ctx
上下文對象fn
,並執行,最後對結果做出響應const fn = compose(this.middleware); 複製代碼
源碼中使用了一個compose
函數,基於全部可執行的中間件生成了一個可執行函數。當該函數執行時,每個中間件將會被依次應用。compose
函數的定義以下:
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) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) // 確保同一個中間件不會執行屢次 index = i let fn = middleware[i] if (i === middleware.length) fn = next //我的認爲對在koa中這裏的fn = next並無意義 if (!fn) return Promise.resolve() // 執行到最後resolve出來 try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } } 複製代碼
它會先執行第一個中間件,執行過程當中若是遇到next()
調用,就會把控制權交到下一個中間件並執行,等該中間件執行完後,再繼續執行next()
以後的代碼。這裏的dispatch.bind(null, i + 1)
就是next
函數。到這裏就能解答,爲何必需要調用next
方法,才能讓當前中間件後面的中間件執行。(有點拗口…)匿名函數的返回結果是一個Promise
,由於要等到中間件處理完以後,才能進行響應。
中間件執行函數生成好以後,接下來須要建立一個ctx
。這個ctx
能夠在中間件裏面使用。ctx
提供了訪問req
和res
的接口。 建立上下文對象調用了一個createContext
函數,這個函數的定義以下:
/** * 建立一個context對象,也就是在中間件裏使用的ctx,並給ctx添加request, respone屬性 */ createContext(req, res) { const context = Object.create(this.context); // 繼承自context.js中export出來proto const request = context.request = Object.create(this.request); // 把自定義的request做爲ctx的屬性 const response = context.response = Object.create(this.response);// 把自定義的response做爲ctx的屬性 context.app = request.app = response.app = this; // 爲了在ctx, request, response中,都能使用httpServer回調函數中的req和res context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.state = {}; return context; } 複製代碼
ctx
對象其實是繼承自context
模塊中定義的proto
對象,同時添加了request
和response
兩個屬性。request
和response
也是對象,分別繼承自request.js
和response.js
定義的對象。這兩個模塊的功能是基於原生的req
和res
封裝了一些getter
和setter
,原理比較簡單,下面就再也不分析了。
咱們重點來看看context
模塊。
const proto = module.exports = { inspect() { if (this === proto) return this; return this.toJSON(); }, toJSON() { return { request: this.request.toJSON(), response: this.response.toJSON(), app: this.app.toJSON(), originalUrl: this.originalUrl, req: '<original node req>', res: '<original node res>', socket: '<original node socket>' }; }, assert: httpAssert, throw(...args) { throw createError(...args); }, onerror(err) { if (null == err) return; if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err)); let headerSent = false; if (this.headerSent || !this.writable) { headerSent = err.headerSent = true; } // delegate this.app.emit('error', err, this); if (headerSent) { return; } const { res } = this; // first unset all headers /* istanbul ignore else */ if (typeof res.getHeaderNames === 'function') { res.getHeaderNames().forEach(name => res.removeHeader(name)); } else { res._headers = {}; // Node < 7.7 } // then set those specified this.set(err.headers); // force text/plain this.type = 'text'; // ENOENT support if ('ENOENT' == err.code) err.status = 404; // default to 500 if ('number' != typeof err.status || !statuses[err.status]) err.status = 500; // respond const code = statuses[err.status]; const msg = err.expose ? err.message : code; this.status = err.status; this.length = Buffer.byteLength(msg); this.res.end(msg); }, get cookies() { if (!this[COOKIES]) { this[COOKIES] = new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure }); } return this[COOKIES]; }, set cookies(_cookies) { this[COOKIES] = _cookies; } }; 複製代碼
context
模塊定義了一個proto
對象,該對象定義了一些方法(eg: throw
)和屬性(eg: cookies
)。咱們上面經過createContext
函數建立的ctx
對象,就是繼承自proto
。所以,咱們能夠在中間件中直接經過ctx
訪問proto
中定義的方法和屬性。
值得一提的點是,做者經過代理的方式,讓開發者能夠直接經過ctx[propertyName]
去訪問ctx.request
或ctx.response
上的屬性和方法。
實現代理的關鍵邏輯
/** * 代理response一些屬性和方法 * eg: proto.response.body => proto.body */ delegate(proto, 'response') .method('attachment') .method('redirect') .access('body') .access('length') // other properties or methods /** * 代理request的一些屬性和方法 */ delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .method('accepts') .method('get') // other properties or methods 複製代碼
實現代理的邏輯也很是簡單,主要就是使用了__defineGetter__
和__defineSetter__
這兩個對象方法,當set
或get
對象的某個屬性時,調用指定的函數對屬性值進行處理或返回。
當ctx
(上下文對象)和fn
(執行中間件的合成函數)都準備好以後,就能真正的處理請求並響應了。該步驟調用了一個handleRequest
函數。
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; // 狀態碼默認404 const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); // 執行完中間件函數後,執行handleResponse處理結果 return fnMiddleware(ctx).then(handleResponse).catch(onerror); } 複製代碼
handleRequest
函數會把ctx
傳入fnMiddleware
並執行,而後經過respond
方法進行響應。這裏默認把狀態碼設爲了404
,若是在執行中間件的過程當中有返回,例如對ctx.body
進行負責,koa
會自動把狀態碼設成200
,這一部分的邏輯是在response
對象的body
屬性的setter
處理的,有興趣的朋友能夠看一下response.js
。
respond
函數會對ctx
對象上的body
或者其餘屬性進行分析,而後經過原生的res.end()
方法將不一樣的結果輸出。
到這裏,koa2的核心代碼大概就分析完啦。以上是我我的總結,若有錯誤,請見諒。歡迎一塊兒交流學習!