「JavaScript」Nodejs之Koa源碼解讀

簡介

Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函數,Koa 幫你丟棄回調函數,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中間件, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程序。

今天要簡單的看一下koa的源碼,首先需要說明的一點,本文都是筆者個人觀點,同時我接觸koa的時間並不長,設置接觸nodejs的時間也不長,所以大家最好都帶著批判的思維來閱讀本文,希望不要對大家造成誤導。

「JavaScript」Nodejs之Koa源碼解讀

使用

最簡單的一個應用

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可以說是我見過最簡短的一個了。

希望大家能從我的分享中有所收穫,還有不足不對之處歡迎指出。


分享到:


相關文章: