koa
是當下很是流行的node
框架,相比笨重的express
,koa
只專一於中間件模型的創建,以及請求和響應控制權的轉移。本文將以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 請求中的host ,protocol ,ip 分別優先從Header 字段中的X-Forwarded-Host ,X-Forwarded-Proto ,X-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
方法很簡單,接受一個函數做爲參數,並加入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
方法,能夠看到內部是經過原生的http
模塊建立服務器並監聽的,請求的回調函數是callback
函數的返回值。express
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製代碼
下面是callback
的代碼,compose
函數將中間件數組轉換成執行鏈函數fn
, compose
的實現是重點,下文會分析。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(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.req
和ctx.res
能夠訪問到node
原生的請求對象和響應對象, 經過修改ctx.state
可讓中間件共享狀態。能夠用一張圖描述這個函數中定義的關係,以下: json
接下來咱們分析細節,this.context
、this.request
、this.response
分別經過context
、request
、response
三個對象的原型建立, 咱們先看一下request
的定義,它位於request.js
文件中。數組
request.js
定義了ctx.request
的原型對象的原型對象,所以該對象的任意屬性均可以經過ctx.request
獲取。這個對象一共有20多個屬性和若干方法。其中屬性多數都定義了get
和set
方法,截取一小部分代碼以下:緩存
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
...
}
複製代碼
上面代碼中定義了header
屬性,根據前面的關係圖可知,this.req
指向的是原生的req
,所以ctx.request.header
等於原生req
的headers
屬性,修改ctx.request.header
就是修改req
的headers
。request
對象中全部的屬性和方法列舉以下:
屬性/方法 | 含義 |
---|---|
header | 原生req 對象的headers |
headers | 原生req 對象的headers , 同上 |
url | 原生req 對象的url |
origin | protocol://host |
href | 請求的完整url |
method | 原生req 對象的method |
path | 請求url 的pathname |
query | 請求url 的query ,對象形式 |
queryString | 請求url 的query ,字符串形式 |
search | ?queryString |
hostname | hostname |
URL | 完整的URL對象 |
fresh | 判斷緩存是否新鮮,只針對HEAD 和GET 方法,其他請求方法均返回false |
stale | fresh 取反 |
idempotent | 檢查請求是否冪等,符合冪等性的請求有GET , HEAD , PUT , DELETE , OPTIONS , TRACE 6個方法 |
socket | 原生req 對象的套接字 |
charset | 請求字符集 |
type | 獲取請求頭的Content-Type 不含參數 charset 。 |
length | 請求的 Content-Length |
secure | 判斷是否是https 請求 |
ips | 當 X-Forwarded-For 存在而且 app.proxy 被啓用時,這些 ips 的數組被返回,從上游到下游排序。 禁用時返回一個空數組。 |
ip | 請求遠程地址。 當 app.proxy 是 true 時支持 X-Forwarded-Proto |
protocol | 返回請求協議,https 或 http 。當 app.proxy 是 true 時支持 X-Forwarded-Proto |
host | 獲取當前主機(hostname:port) 。當 app.proxy 是 true 時支持 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
定義了ctx.response
的原型對象的原型對象,所以該對象的任意屬性均可以經過ctx.response
獲取。和request
相似,response
的屬性多數也定義了get
和set
方法。response
的屬性和方法以下:
屬性/方法 | 含義 |
---|---|
header | 原生res 對象的headers |
headers | 原生res 對象的headers , 同上 |
status | 響應狀態碼, 原生res 對象的statusCode |
message | 響應的狀態消息. 默認狀況下, response.message 與 response.status 關聯 |
socket | 套接字,原生res 對象的socket |
type | 獲取響應頭的 Content-Type 不含參數 charset |
body | 響應體,支持string ,buffer 、stream 、json |
lastModified | 將 Last-Modified 標頭返回爲 Date , 若是存在 |
etag | 響應頭的ETag |
length | 數字返回響應的 Content-Length ,使用Buffer.byteLength 對body 進行計算 |
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 對象的finished 爲true ,則返回false , 不然判斷原生res 對象是否創建套接字socket , 若是沒有返回false , 有則返回socket.writable |
request
和response
中每一個屬性get
和set
的定義以及方法的實現多數比較簡單直觀,若是對每一個進行單獨分析會致使篇幅過長,並且這些不是理解koa
運行機制的核心所在,所以本文只羅列屬性和方法的用途,這些大部分也能夠在koa
的官方文檔中找到。關心細節的朋友能夠直接閱讀request.js
和response.js
這兩個文件,若是你熟悉http
協議,相信這些代碼對你並無障礙。接下來咱們的重點是context.js
。
context.js
定義了ctx
的原型對象的原型對象, 所以這個對象中全部屬性均可以經過ctx
訪問到。context.js
中除了定義[util.inspect.custom]
這個不是很重要的屬性外,只直接定義了一個屬性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
屬性的set
和get
方法。set
方法很簡單,COOKIES
是一個Symbol
類型的私有變量。須要注意的是咱們通常不經過ctx.cookies
來直接設置cookies
,官方文檔推薦使用ctx.cookies.set(name, value, options)
來設置,但是這裏並無cookies.set
呀,其實這裏稍微一看就明白,cookies
的值是this[COOKIES]
,它是Cookies
的一個實例,在Cookie
這個npm
包中是定義了實例的get
和set
方法的。
throw(...args) {
throw createError(...args);
},
複製代碼
當咱們調用ctx.throw
拋出一個錯誤時,內部是拋出了一個有狀態碼和信息的錯誤,createError
的實如今http-errors
這個npm
包中。
下面是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.set
、this.type
等是哪裏來的?context
並無定義這些屬性。咱們知道, ctx
中實際上是代理了不少response
和resquest
的屬性和方法的,this.set
、this.type
其實就是response.set
和response.type
。那麼koa
中對象屬性和方法的代理是如何實現的呢,答案是delegate
,context
中代碼的最後就是使用delegate
來代理一些原本只存在於request
和response上
的屬性。接下來咱們看一下delegete
是如何實現代理的,delegete
的實現代碼在delegetes
這個npm包中。
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.method
、Delegator.prototype.accsess
以及Delegator.prototype.getter
,這些都是代理方法, 分別代理set
和get
方法。下面是代碼,其中get
和set
方法的代理主要使用了對象的__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;
};
複製代碼
到這裏,關於request
、response
和context
就聊的差很少了,接下來回到callback
繼續咱們的重點,前面說到的compose
纔是koa
的精華和核心所在,他的代碼在koa-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
函數,失敗後將執行ctx
的onerror
函數。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
是koa
內置的響應自動處理函數,代碼以下,它主要功能是判斷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);
}
複製代碼