標籤:

Koa源碼分析

最近一直都在開發基於node的前後端項目,分享一下Koa的源碼。

Koa自己的說法是next generation web framework for node.js

Koa算是比較主流的node的web框架了,前身是express。相比於express,koa去除了多餘的middleware,只留下了最基本的對node的網路模塊的繼承和封裝,並且提供了方便的中間件調用機制,Koa的源碼總共加起來就1600+,很快就可以看完。

基礎知識

在分析koa的源碼之前需要先了解一下node的http模塊。

const http = require(http);const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader(『Content-Type』, 『text/plain』); res.end(『Hello World』);}server.listen(3000);

node的http模塊主要負責了node對HTTP處理的封裝。以上這段代碼啟動了一個監聽3000埠的web server,並且返回Hello World給接收到的請求。

每一次接收到一個新的請求的時候會調用回調函數,參數req和res分別是請求的實體和返回的實體,操作req可以獲取收到的請求,操作res對應的是將要返回的packet。

如果你需要對接收到的請求進行一系列處理的話,則需要按順序寫在回調函數裡面。

同樣的功能對應的Koa的寫法如下:

const Koa = require(koa);const app = new Koa();app.use(async ctx => { ctx.body = Hello World;});app.listen(3000);

這裡的user實際上是Koa的中間件機制提供的一個方便的處理請求的介面。請求會被use後面的函數依次按照use的順序被處理,通常稱這些函數為中間件,他們的參數ctx為koa基於node的http模塊的req和res封裝的一個對象,集合了req和res的功能為一體,並且增加了一些簡單的操作。可以通過ctx.req和ctx.res獲取到原生的req和res,與此同時ctx.request和ctx.response是Koa基於req和res封裝的擁有一些新的功能的請求和返回的實體。

以下代碼是Koa中間件使用的栗子:

const Koa = require(koa);const app = new Koa();app.use(async (ctx, next) => { console.log(『pre1』) await next(); console.log(『post1』);});app.use(async (ctx, next) => { console.log(『pre2』); await next(); console.log(『post2』)});app.use(async ctx => { console.log(『pre3』) ctx.body = Hello World; console.log(『post3』);});app.listen(3000);

next()表示將請求的處理交給下一個中間件。如果沒有next(),在該中間件函數執行結束後,將返回執行上一個中間件的next()後續的內容直到最開始的中間件的next()後面的內容執行完畢。

上面的代碼的結果是

pre1pre2pre3post3post2post1

執行的結果和函數遞歸調用何其相似,之後了解了Koa的中間件機制後自然會明白這個結果的原因。

正題

在了解了koa的基本的使用和帶著以上中間件執行的結果,我們來看看koa的源碼吧。

對Koa的理解主要分為兩個部分:

  • Koa對node的http模塊的封裝
  • Koa的中間件機制

按照【栗子】代碼的順序從上到下:

初始化Koa

const app = new Koa();

使用use載入中間件

對應的源碼中

use(fn) { if (typeof fn !== function) throw new TypeError(middleware must be a function!); if (isGeneratorFunction(fn)) { deprecate(Support for generators will be removed in v3. + See the documentation for examples of how to convert old middleware + https://github.com/koajs/koa/blob/master/docs/migration.md); fn = convert(fn); } debug(use %s, fn._name || fn.name || -); this.middleware.push(fn); return this;}

將中間件函數push到middleware數組中。

調用listen方法啟動web server

listen(...args) { debug(listen); const server = http.createServer(this.callback()); return server.listen(...args);}

回調函數:

callback() { const fn = compose(this.middleware); if (!this.listeners(error).length) this.on(error, this.onerror); const handleRequest = (req, res) => { res.statusCode = 404; const ctx = this.createContext(req, res); const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fn(ctx).then(handleResponse).catch(onerror); }; return handleRequest;}

這裡的compose函數是實現Koa的中間件機制的地方之後再細說。

Koa對node的http模塊的封裝

const ctx = this.createContext(req, res);createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; 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.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); request.ip = request.ips[0] || req.socket.remoteAddress || ; context.accept = request.accept = accepts(req); context.state = {}; return context; }

createContext實際上只是創建之前說的集合了req & res & request & response的一個對象,作為參數傳遞給中間件。

對於request和response其中除了一些拓展的方便的介面之外大部分都是直接繼承的http的req和response。

實現的新的介面也是比較簡單的封裝,舉個栗子(response.js):

set status(code) { assert(number == typeof code, status code must be a number); assert(statuses[code], `invalid status code: ${code}`); assert(!this.res.headersSent, headers have already been sent); this._explicitStatus = true; this.res.statusCode = code; this.res.statusMessage = statuses[code]; if (this.body && statuses.empty[code]) this.body = null;},/** * Get response status message * * @return {String} * @api public */get message() { return this.res.statusMessage || statuses[this.status];}

以上是response中實現的兩個新的介面,實際上也就是res的介面再簡單封裝了一下然後返回。

再看(context.js)的最底部

/** * Response delegation. */delegate(proto, response) .method(attachment) .method(redirect) .method(remove) .method(vary) .method(set) .method(append) .method(flushHeaders) .access(status) .access(message) .access(body) .access(length) .access(type) .access(lastModified) .access(etag) .getter(headerSent) .getter(writable);/** * Request delegation. */delegate(proto, request) .method(acceptsLanguages) .method(acceptsEncodings) .method(acceptsCharsets) .method(accepts) .method(get) .method(is) .access(querystring) .access(idempotent) .access(socket) .access(search) .access(method) .access(query) .access(path) .access(url) .getter(origin) .getter(href) .getter(subdomains) .getter(protocol) .getter(host) .getter(hostname) .getter(URL) .getter(header) .getter(headers) .getter(secure) .getter(stale) .getter(fresh) .getter(ips) .getter(ip);

delegate的作用是將對應的對象上的method,getter,setter繼承到另一個對象上。

可以看到,直接繼承了大部分req和res的方法。

接下來就是看看Koa的中間件機制的實現了。

Koa的中間件機制

compose的源碼也是非常簡單的:

use strict/** * Expose compositor. */module.exports = compose/** * 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, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } }}

可以看到其實就是按照順序將middleware數組中的中間件s按照順序遞歸執行,每次執行next()的時候就是執行下一個中間件,最後一個next也就是第一個中間件的第二個參數,因為是undefined,所以會結束遞歸調用反向依次執行每個中間件next後續的代碼。每次return的都是一個Promise對象,因此我們寫的時候是await來等待這個非同步調用的結束,然後執行下一個中間件。而我們一般的寫法是將await next()寫在中間件函數的最後,從而用偽遞歸的方式來實現每個請求依次被中間件函數處理的效果。

恩,Koa的主要的概念就是這些,它的目的就是一個極簡的框架,只提供最基本的介面,大部分的功能,開發者根據需求使用use添加中間件來實現。


推薦閱讀:

React源碼分析 - 組件更新與事務
【aux】使用油猴腳本改進sketch measure
全面了解TCP/IP到HTTP
Typescript玩轉設計模式 之 對象行為型模式(上)
artTemplate基本使用方法詳細

TAG:前端開發 |