簡介
Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函數,Koa 幫你丟棄回調函數,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中間件, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程序。
今天要簡單的看一下koa的源碼,首先需要說明的一點,本文都是筆者個人觀點,同時我接觸koa的時間並不長,設置接觸nodejs的時間也不長,所以大家最好都帶著批判的思維來閱讀本文,希望不要對大家造成誤導。
使用
最簡單的一個應用
const Koa = require('koa'); const app = new Koa(); app.listen(3000);
這是一個無作用的 Koa 應用程序被綁定到 3000 端口
上手使用一個框架,只需要5分鐘。正因為如此很多人都忽略了框架的內部實現。
知其然,知其所以然。我們下面瞭解一下其源碼構成。
源碼解析
進入正題,關於源碼,找到了兩套,一個在github上,https://github.com/koajs/koa
另一個是在我之前使用的koa項目裡,項目裡通過ide點擊跳轉過去的是一個 index.d.ts,是一個typescript的實現,不過這兩套代碼在功能上應該是完全一樣的。
這裡由於我之前並未接觸過typescript,所以選擇去解讀一下GitHub上的開源代碼。
在代碼lib目錄下,有4個文件application.js、context.js、request.js、response.js 差不多可以猜測結構,對外暴露的koa就是這邊的 application.js 裡export的對象,其他三個為application的重要組成部分。
先從application看起。
'use strict'; /** * Module dependencies. */ const isGeneratorFunction = require('is-generator-function'); const debug = require('debug')('koa:application'); const onFinished = require('on-finished'); const response = require('./response'); const compose = require('koa-compose'); const isJSON = require('koa-is-json'); const context = require('./context'); const request = require('./request'); const statuses = require('statuses'); const Emitter = require('events'); const util = require('util'); const Stream = require('stream'); const http = require('http'); const only = require('only'); const convert = require('koa-convert'); const deprecate = require('depd')('koa');
這邊use strict開啟JavaScript的嚴格模式,這種寫法在框架代碼中很常見,越是底層的代碼,越需要嚴格,所以在我們平時寫上層應用時,use strict就用的少很多了。
然後是依賴的相關文件,這裡可以看到的確依賴同級目錄下的context、request、response文件,但同時還依賴很多外部擴展。
- is-generator-function 判斷是否是generator函數
- debug來調試,用於打印相關log
- on-finished 看了說明,用來監聽http請求結束時的回調
- koa-compose用於構建中間件
- koa-is-json用於Check if a body is JSON
- statuses提供http的status單元
- events實現了事件機制 Node's event emitter for all engines
- util提供了一系列常用工具函數
- stream 是 Node.js 中處理流式數據的抽象接口。 stream 模塊提供了一些 API,用於構建實現了流接口的對象。Node.js 提供了多種流對象。 例如,HTTP 服務器的請求和 process.stdout 都是流的實例。流可以是可讀的、可寫的、或者可讀可寫的。 所有的流都是 EventEmitter 的實例。
- http 要使用 HTTP 服務器與客戶端,需要 require('http')
- only Return whitelisted properties of an object.
- koa-convert 用於支持之前版本的koajs
- depd類似命名空間的用法
到這裡就把依賴的庫大致講了一遍,當然沒搞明白也沒關係,這裡只需要有個大致的概念,後面具體用到了可以回過頭來在看。
然後就是application的主要部分了
/** * Expose `Application` class. * Inherits from `Emitter.prototype`. */ module.exports = class Application extends Emitter { }
這裡可以看到 application是繼承自Emitter,也就是 events.繼續看內部的代碼
/** * Initialize a new `Application`. * * @api public */ constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } }
這是構造函數,裡面定義了一些成員變量,不一一說明了。繼續往下到了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其實就對http.createServer(callback).listen的一種縮寫。算是一個語法糖。不難理解。繼續往下。
/** * Return JSON representation. * We only bother showing settings. * * @return {Object} * @api public */ toJSON() { return only(this, [ 'subdomainOffset', 'proxy', 'env' ]); } /** * Inspect implementation. * * @return {Object} * @api public */ inspect() { return this.toJSON(); }
這個就是一個簡單的成員變量保護,只對外暴露三個屬性。
/** * 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!'); 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; }
這個是use方法,平時的use就是把函數推到middleware屬性中,如果是generator函數,這邊會做個轉化,然後再推入,邏輯也相當簡單。
/** * 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; }
callback先把middleware組裝好,然後開啟了一個錯誤事件監聽。然後有調用了下面的createContext和handleRequest方法。結合上面的listen方法可以看到callback被用到http.createServer(this.callback())中,也就是一個簡單的封裝而已。並不難理解,那繼續往下看。
/** * 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); }
這個用在callback裡的方法,可以看到執行了fnMiddleware方法,同時用到下面的onerror和respond。這裡的onFinished也是一個監聽吧,暫時這麼理解,先往下看。
/** * Initialize a new context. * * @api private */ 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.state = {}; return context; }
這一步也不難理解,創建了一個context對象,並且初始化了他的各項屬性值。接下來的onerror也很好理解。
/** * Default error handler. * * @param {Error} err * @api private */ onerror(err) { if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)); if (404 == err.status || err.expose) return; if (this.silent) return; const msg = err.stack || err.toString(); console.error(); console.error(msg.replace(/^/gm, ' ')); console.error(); }
就在控制檯打印了一些錯誤,並沒什麼難懂的。
然後application內部的方法就沒了,梳理下來很少,並且幾乎一眼看懂其作用,然後就是定義外的一個輔助方法,或者叫他工具方法。
/** * Response helper. */ function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; const res = ctx.res; if (!ctx.writable) return; let body = ctx.body; const code = ctx.status; // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; return res.end(); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } return res.end(); } // status body if (null == body) { body = ctx.message || String(code); if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }
這個函數顧名思義,就是一個response的封裝函數,50行代碼,也很清晰。
總的看下來邏輯非常清楚,需要主要到一點的,所有的res和req都是同一個對象,在不同的方法裡反覆傳遞使用,所以非常方便。
看到這邊,感覺koa的核心思想真的非常簡單,說直接點僅僅就是實現了use而已,提供了一個乾淨的中間件框架。我們可以很方便的寫中間件,除此之後,和原生的http模塊幾乎沒什麼區別。
看過很多框架源碼、類庫源碼,koa可以說是我見過最簡短的一個了。
希望大家能從我的分享中有所收穫,還有不足不對之處歡迎指出。