深刻koa2源碼

koa是當下很是流行的node框架,相比笨重的expresskoa只專一於中間件模型的創建,以及請求和響應控制權的轉移。本文將以koa2爲例,深刻源碼分析框架的實現細節。 koa2的源碼位於lib目錄,結構很是簡單和清晰,只有四個文件,以下:javascript

根據package.json中的main字段,能夠知道入口文件是lib/application.js,application.js定義了koa的構造函數以及實例擁有的方法,以下圖: java

構造函數

首先看一下構造函數的代碼node

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;
    }
  }
複製代碼

這裏定義了實例的8個屬性,各自的含義以下:git

屬性 含義
proxy 表示是否開啓代理,默認爲false,若是開啓代理,對於獲取request請求中的hostprotocolip分別優先從Header字段中的X-Forwarded-HostX-Forwarded-ProtoX-Forwarded-For獲取。
middleware 最重要的一個屬性,存放全部的中間件,存放和執行的過程後文細說。
subdomainOffset 子域名的偏移量,默認值爲2,這個參數決定了request.subdomains的返回結果。
env node的執行環境, 默認是development
context 中間件第一個實參ctx的原型, 具體在講context.js時會說到。
request ctx.request的原型,定義在request.js中。
response ctx.response的原型,定義在response.js中。
[util.inspect.custom] util.inspect這個方法用於將對象轉換爲字符串, 在node v6.6.0及以上版本中util.inspect.custom是一個Symbol類型的值,經過定義對象的[util.inspect.custom]屬性爲一個函數,能夠覆蓋util.inspect的默認行爲。

use()

use方法很簡單,接受一個函數做爲參數,並加入middleware數組。因爲koa最開始支持使用generator函數做爲中間件使用,但將在3.x的版本中放棄這項支持,所以koa2中對於使用generator函數做爲中間件的行爲給與將來將被廢棄的警告,但會將generator函數轉化爲async函數。返回this便於鏈式調用。github

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;
  }
複製代碼

listen()

下面是listen方法,能夠看到內部是經過原生的http模塊建立服務器並監聽的,請求的回調函數是callback函數的返回值。express

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
複製代碼

callback()

下面是callback的代碼,compose函數將中間件數組轉換成執行鏈函數fncompose的實現是重點,下文會分析。koa繼承自Emitter,所以能夠經過listenerCount屬性判斷監聽了多少個error事件, 若是外部沒有進行監聽,框架將自動監聽一個error事件。callback函數返回一個handleRequest函數,所以真正的請求處理回調函數是handleRequest。在handleRequest函數內部,經過createContext建立了上下文ctx,並交給koa實例的handleRequest方法去處理回調邏輯。npm

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;
  }
複製代碼

createContext()

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;
}
複製代碼

上面是createContext的代碼, 從這裏咱們能夠知道,經過ctx.reqctx.res能夠訪問到node原生的請求對象和響應對象, 經過修改ctx.state可讓中間件共享狀態。能夠用一張圖描述這個函數中定義的關係,以下: json

接下來咱們分析細節,this.contextthis.requestthis.response分別經過contextrequestresponse三個對象的原型建立, 咱們先看一下request的定義,它位於request.js文件中。數組

request.js

request.js定義了ctx.request的原型對象的原型對象,所以該對象的任意屬性均可以經過ctx.request獲取。這個對象一共有20多個屬性和若干方法。其中屬性多數都定義了getset方法,截取一小部分代碼以下:緩存

module.exports = {
 get header() {
    return this.req.headers;
 },
 set header(val) {
    this.req.headers = val;
 },
 ...
}

複製代碼

上面代碼中定義了header屬性,根據前面的關係圖可知,this.req指向的是原生的req,所以ctx.request.header等於原生reqheaders屬性,修改ctx.request.header就是修改reqheadersrequest對象中全部的屬性和方法列舉以下:

屬性/方法 含義
header 原生req對象的headers
headers 原生req對象的headers, 同上
url 原生req對象的url
origin protocol://host
href 請求的完整url
method 原生req對象的method
path 請求urlpathname
query 請求urlquery,對象形式
queryString 請求urlquery,字符串形式
search ?queryString
hostname hostname
URL 完整的URL對象
fresh 判斷緩存是否新鮮,只針對HEADGET方法,其他請求方法均返回false
stale fresh取反
idempotent 檢查請求是否冪等,符合冪等性的請求有GET, HEAD, PUT, DELETE, OPTIONS, TRACE6個方法
socket 原生req對象的套接字
charset 請求字符集
type 獲取請求頭的Content-Type 不含參數 charset
length 請求的 Content-Length
secure 判斷是否是https請求
ips X-Forwarded-For 存在而且 app.proxy 被啓用時,這些 ips的數組被返回,從上游到下游排序。 禁用時返回一個空數組。
ip 請求遠程地址。 當 app.proxytrue 時支持 X-Forwarded-Proto
protocol 返回請求協議,httpshttp。當 app.proxytrue 時支持 X-Forwarded-Proto
host 獲取當前主機(hostname:port)。當 app.proxytrue 時支持 X-Forwarded-Host,不然使用Host
subdomains 根據app.subdomainOffset設置的偏移量,將子域返回爲數組
get(...args) 獲取請求頭字段
accepts(...args) 檢查給定的 type(s) 是否能夠接受,若是 true,返回最佳匹配,不然爲 false
acceptsEncodings(...args) 檢查 encodings 是否能夠接受,返回最佳匹配爲 true,不然爲 false
acceptsCharsets(...args) 檢查 charsets 是否能夠接受,在 true 時返回最佳匹配,不然爲 false
acceptsLanguages(...args) 檢查 langs 是否能夠接受,若是爲 true,返回最佳匹配,不然爲 false
[util.inspect.custom] 自定義的util.inspect

response.js

response.js定義了ctx.response的原型對象的原型對象,所以該對象的任意屬性均可以經過ctx.response獲取。和request相似,response的屬性多數也定義了getset方法。response的屬性和方法以下:

屬性/方法 含義
header 原生res對象的headers
headers 原生res對象的headers, 同上
status 響應狀態碼, 原生res對象的statusCode
message 響應的狀態消息. 默認狀況下, response.messageresponse.status 關聯
socket 套接字,原生res對象的socket
type 獲取響應頭的 Content-Type 不含參數 charset
body 響應體,支持stringbufferstreamjson
lastModified Last-Modified 標頭返回爲 Date, 若是存在
etag 響應頭的ETag
length 數字返回響應的 Content-Length,使用Buffer.byteLengthbody進行計算
headerSent 檢查是否已經發送了一個響應頭, 用於查看客戶端是否可能會收到錯誤通知
vary(field) field 上變化。
redirect(url, alt) 執行重定向
attachment(filename, options) Content-Disposition 設置爲 「附件」 以指示客戶端提示下載。(可選)指定下載的 filename
get(field) 返回指定的響應頭部
set(field, val) 設置響應頭部
is(type) 響應類型是不是所提供的類型之一
append(field, val) 設置規範以外的響應頭
remove(field) 刪除指定的響應頭
flushHeaders() 刷新全部響應頭
writable() 判斷響應是否可寫,原生res對象的finishedtrue,則返回false, 不然判斷原生res對象是否創建套接字socket, 若是沒有返回false, 有則返回socket.writable

requestresponse中每一個屬性getset的定義以及方法的實現多數比較簡單直觀,若是對每一個進行單獨分析會致使篇幅過長,並且這些不是理解koa運行機制的核心所在,所以本文只羅列屬性和方法的用途,這些大部分也能夠在koa的官方文檔中找到。關心細節的朋友能夠直接閱讀request.jsresponse.js這兩個文件,若是你熟悉http協議,相信這些代碼對你並無障礙。接下來咱們的重點是context.js

context.js

context.js定義了ctx的原型對象的原型對象, 所以這個對象中全部屬性均可以經過ctx訪問到。context.js中除了定義[util.inspect.custom]這個不是很重要的屬性外,只直接定義了一個屬性cookies,也定義了幾個方法,這裏分別進行介紹:

cookies
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;
  }
複製代碼

上面的代碼中定義了cookies屬性的setget方法。set方法很簡單,COOKIES是一個Symbol類型的私有變量。須要注意的是咱們通常不經過ctx.cookies來直接設置cookies,官方文檔推薦使用ctx.cookies.set(name, value, options)來設置,但是這裏並無cookies.set呀,其實這裏稍微一看就明白,cookies的值是this[COOKIES],它是Cookies的一個實例,在Cookie這個npm包中是定義了實例的getset方法的。

throw()
throw(...args) {
    throw createError(...args);
  },
複製代碼

當咱們調用ctx.throw拋出一個錯誤時,內部是拋出了一個有狀態碼和信息的錯誤,createError的實如今http-errors這個npm包中。

onerror()

下面是onerror方法的代碼,發生錯誤時首先會觸發koa實例上的error事件來打印一個錯誤日誌, headerSent變量表示響應頭是否發送,若是響應頭已經發送,或者響應處於不可寫狀態,將沒法在響應中添加錯誤信息,直接退出該函數,不然須要將以前寫入的響應頭部信息清空。

onerror(err) {
    // 沒有錯誤時什麼也不作
    if (null == err) return;
    // err不是Error實例時,使用err建立一個Error實例
    if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));

    let headerSent = false;
    // 若是res不可寫或者請求頭已發出
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    // 觸發koa實例app的error事件
    this.app.emit('error', err, this);

    if (headerSent) {
      return;
    }

    const { res } = this;

    // 移除全部設置過的響應頭
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // 設置錯誤頭部
    this.set(err.headers);

    // 設置錯誤時的Content-Type
    this.type = 'text';

    // 找不到文件錯誤碼設爲404
    if ('ENOENT' == err.code) err.status = 404;

    // 不能被識別的錯誤將錯誤碼設爲500
    if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;

    const code = statuses[err.status];
    const msg = err.expose ? err.message : code;
    // 設置錯誤碼
    this.status = err.status;
    this.length = Buffer.byteLength(msg);
    // 結束響應
    res.end(msg);
  },
複製代碼

從上面代碼中會有疑問, this.setthis.type等是哪裏來的?context並無定義這些屬性。咱們知道, ctx中實際上是代理了不少responseresquest的屬性和方法的,this.setthis.type其實就是response.setresponse.type。那麼koa中對象屬性和方法的代理是如何實現的呢,答案是delegate,context中代碼的最後就是使用delegate來代理一些原本只存在於requestresponse上的屬性。接下來咱們看一下delegete是如何實現代理的,delegete的實現代碼在delegetes這個npm包中。

delegate

delegate方法本質上是一個構造函數,接受兩個參數,第一個參數是代理對象,第二個參數是被代理的對象,下面是它的定義, Delegator就是delegate。能夠看到,不論是否使用new關鍵字,該函數老是會返回一個實例。

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 = [];
}
複製代碼

此外,在Delegator構造函數的原型上,定義了幾個方法,koa中用到了Delegator.prototype.methodDelegator.prototype.accsess以及Delegator.prototype.getter,這些都是代理方法, 分別代理setget方法。下面是代碼,其中getset方法的代理主要使用了對象的__defineGetter__以及__defineSetter__方法。

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;
};
Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};
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.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;
};
複製代碼

到這裏,關於requestresponsecontext就聊的差很少了,接下來回到callback繼續咱們的重點,前面說到的compose纔是koa的精華和核心所在,他的代碼在koa-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) {
    // 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)
      }
    }
  }
}
複製代碼

函數接收一個middleware數組爲參數,返回一個函數,給函數傳入ctx時第一個中間件將自動執行,之後的中間件只有在手動調用next,即dispatch時纔會執行。另外從代碼中能夠看出,中間件的執行是異步的,而且中間件執行完畢後返回的是一個Promise,每一個dispatch的返回值也是一個Promise,所以咱們的中間件中能夠方便地使用async函數進行定義,內部使用await next()調用「下游」,而後控制流回「上游」,這是更準確也更友好的中間件模型。從下面的代碼能夠看到,中間件順利執行完畢後將執行respond函數,失敗後將執行ctxonerror函數。onFinished(res, onerror)這段代碼是對響應處理過程當中的錯誤監聽,即handleResponse發生的錯誤或自定義的響應處理中發生的錯誤。

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);
  }
複製代碼

respond

respondkoa內置的響應自動處理函數,代碼以下,它主要功能是判斷ctx.body的類型,而後自動完成最後的響應。另外,若是在koa中須要自行處理響應,能夠設置ctx.respond = false,這樣內置的respond就會被忽略。

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) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      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);
}
複製代碼
相關文章
相關標籤/搜索