koa是什麼javascript
koa 2作了的事情:java
基於node原生req和res爲request和response對象賦能,並基於它們封裝成一個context對象。node
基於async/await 中間件洋蔥模型機制。json
koa1和koa2在源碼上的區別主要是於對異步中間件的支持方式的不一樣。設計模式
koa1是使用generator、yield)的模式。跨域
koa2使用的是async/await+Promise的模式。下文主要是針對koa2版本源碼上的講解。數組
koa源碼其實很簡單,共4個文件。promise
── lib
├── application.js
├── context.js
├── request.js
└── response.js
複製代碼
這4個文件其實也對應了koa的4個對象:bash
── lib
├── new Koa() || ctx.app
├── ctx
├── ctx.req || ctx.request
└── ctx.res || ctx.response
複製代碼
request.js
和 response.js
在實現邏輯上徹底一致,都是暴露出一個對象,對象屬性都經過getter
和 setter
來實現讀寫和控制。服務器
下面,咱們先初步瞭解koa的源碼內容,讀懂它們,能夠對koa有一個初步的瞭解。
application.js是koa的入口(從koa文件夾下的package.json的main字段(lib/application.js)中能夠得知此文件是入口文件),也是核心部分。
/** * 依賴模塊,包括但不止於下面的,只列出核心須要關注的內容 */
const response = require('./response');
const compose = require('koa-compose');
const context = require('./context');
const request = require('./request');
const Emitter = require('events');
const convert = require('koa-convert');
/** * 繼承Emitter,很重要,說明Application有異步事件的處理能力 */
module.exports = class Application extends Emitter {
constructor() {
super();
this.middleware = []; // 該數組存放全部經過use函數的引入的中間件函數
this.subdomainOffset = 2; // 須要忽略的域名個數
this.env = process.env.NODE_ENV || 'development';
// 經過context.js、request.js、response.js建立對應的context、request、response。爲何用Object.create下面會講解
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// 建立服務器
listen(...args) {
debug('listen');
const server = http.createServer(this.callback()); //this.callback()是須要重點關注的部分,其實對應了http.createServer的參數(req, res)=> {}
return server.listen(...args);
}
/* 經過調用koa應用實例的use函數,形如: app.use(async (ctx, next) => { await next(); }); 來加入中間件 */
use(fn) {
if (isGeneratorFunction(fn)) {
fn = convert(fn); // 兼容koa1的generator寫法,下文會講解轉換原理
}
this.middleware.push(fn); // 將傳入的函數存放到middleware數組中
return this;
}
// 返回一個相似(req, res) => {}的函數,該函數會做爲參數傳遞給上文的listen函數中的http.createServer函數,做爲請求處理的函數
callback() {
// 將全部傳入use的函數經過koa-compose組合一下
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 基於req、res封裝出更強大的ctx,下文會詳細講解
const ctx = this.createContext(req, res);
// 調用app實例上的handleRequest,注意區分本函數handleRequest
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
// 處理請求
handleRequest(ctx, fnMiddleware) {
// 省略,見下文
}
// 基於req、res封裝出更強大的ctx
createContext(req, res) {
// 省略,見下文
}
};
複製代碼
從上面代碼中,咱們能夠總結出application.js核心其實處理了這4個事情:
1. 啓動框架
2. 實現洋蔥模型中間件機制
3. 封裝高內聚的context
4. 實現異步函數的統一錯誤處理機制
const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const proto = module.exports = {
// 省略了一些不甚重要的函數
onerror(err) {
// 觸發application實例的error事件
this.app.emit('error', err, this);
},
};
/* 在application.createContext函數中, 被建立的context對象會掛載基於request.js實現的request對象和基於response.js實現的response對象。 下面2個delegate的做用是讓context對象代理request和response的部分屬性和方法 */
delegate(proto, 'response')
.method('attachment')
...
.access('status')
...
.getter('writable')
...;
delegate(proto, 'request')
.method('acceptsLanguages')
...
.access('querystring')
...
.getter('origin')
...;
複製代碼
從上面代碼中,咱們能夠總結出context.js核心其實處理了這2個事情
1. 錯誤事件處理
2. 代理response對象和request對象的部分屬性和方法
module.exports = {
// 在application.js的createContext函數中,會把node原生的req做爲request對象(即request.js封裝的對象)的屬性
// request對象會基於req封裝不少便利的屬性和方法
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
// 省略了大量相似的工具屬性和方法
};
複製代碼
request對象基於node原生req封裝了一系列便利屬性和方法,供處理請求時調用。
因此當你訪問ctx.request.xxx的時候,其實是在訪問request對象上的賦值器(setter)和取值器(getter)。
module.exports = {
// 在application.js的createContext函數中,會把node原生的res做爲response對象(即response.js封裝的對象)的屬性
// response對象與request對象相似,基於res封裝了一系列便利的屬性和方法
get body() {
return this._body;
},
set body(val) {
// 支持string
if ('string' == typeof val) {
}
// 支持buffer
if (Buffer.isBuffer(val)) {
}
// 支持stream
if ('function' == typeof val.pipe) {
}
// 支持json
this.remove('Content-Length');
this.type = 'json';
},
}
複製代碼
值得注意的是,返回的body支持Buffer、Stream、String以及最多見的json,如上示例所示。
下文會從初始化、啓動應用、處理請求等的角度,來對這過程當中比較重要的細節進行講解及延伸,若是完全弄懂,會對koa以及ES六、generator、async/await、co、異步中間件等有更深一步的瞭解
koa實例化:
const Koa = require('koa');
const app = new Koa();
複製代碼
koa執行源碼:
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context); //爲何要使用Object.create? 見下面緣由
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
}
複製代碼
當實例化koa的時候,koa作了如下2件事:
繼承Emitter,具有處理異步事件的能力。然而koa是如何處理,如今還不得而知,這裏打個問號。
在建立實例過程當中,有三個對象做爲實例的屬性被初始化,分別是context、request、response。還有咱們熟悉的存放中間件的數組mddleware。這裏須要注意,是使用Object.create(xxx)對this.xxx進行賦值。
在實例化koa以後,接下來,使用app.use傳入中間件函數:
app.use(async (ctx,next) => {
await next();
});
複製代碼
koa對應執行源碼:
use(fn) {
if (isGeneratorFunction(fn)) {
fn = convert(fn);
}
this.middleware.push(fn);
return this;
}
複製代碼
當咱們執行app.use的時候,koa作了這2件事情:
koa2處於對koa1版本的兼容,中間件函數若是是generator函數的話,會使用koa-convert進行轉換爲「類async函數」。(不過到第三個版本,該兼容會取消)。
那麼到底是怎麼轉換的呢?
咱們先來想一想generator和async有什麼區別?
惟一的區別就是async會自動執行,而generator每次都要調用next函數。
因此問題變爲,如何讓generator自動執行next函數?
回憶一下generator的知識:每次執行generator的next函數時,它會返回一個對象:
{ value: xxx, done: false }
複製代碼
返回這個對象後,若是能再次執行next,就能夠達到自動執行的目的了。
看下面的例子:
function * gen(){
yield new Promise((resolve,reject){
//異步函數1
if(成功){
resolve()
}else{
reject();
}
});
yield new Promise((resolve,reject){
//異步函數2
if(成功){
resolve()
}else{
reject();
}
})
}
let g = gen();
let ret = g.next();
複製代碼
此時ret = { value: Promise實例; done: false};value已經拿到了Promise對象,那就能夠本身定義成功/失敗的回調函數了。如:
ret.value.then(()=>{
g.next();
})
複製代碼
如今就大功告成啦。咱們只要找到一個合適的方法讓g.next()一直持續下去就能夠自動執行了。
因此問題的關鍵在於yield的value必須是一個Promise。那麼咱們來看看co是如何把這些都東西都轉化爲Promise的:
function co(gen) {
var ctx = this; // 把上下文轉換爲當前調用co的對象
var args = slice.call(arguments, 1) // 獲取參數
// we wrap everything in a promise to avoid promise chaining,
// 無論你的gen是什麼,都先用Promise包裹起來
return new Promise(function(resolve, reject) {
// 若是gen是函數,則修改gen的this爲co中的this對象並執行gen
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 由於執行了gen,因此gen如今是一個有next和value的對象,若是gen不存在、或者不是函數則直接返回gen
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// 執行相似上面示例g.next()的代碼
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 執行每個gen.next()
} catch (e) {
return reject(e);
}
next(ret); //把執行獲得的返回值傳入到next函數中,next函數是自動執行的關鍵
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/** * Get the next value in the generator, * return a promise. */
function next(ret) {
// 若是ret.done=true說明迭代已經完畢,返回最後一次迭代的value
if (ret.done) return resolve(ret.value);
// 不管ret.value是什麼,都轉換爲Promise,而且把上下文指向ctx
var value = toPromise.call(ctx, ret.value);
// 若是value是一個Promise,則繼續在then中調用onFulfilled。至關於從頭開始!!
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
複製代碼
請留意上面代碼的註釋。
從上面代碼能夠獲得這樣的結論,co的思想其實就是:
把一個generator封裝在一個Promise對象中,而後再這個Promise對象中再次把它的gen.next()也封裝出Promise對象,至關於這個子Promise對象完成的時候也重複調用gen.next()。當全部迭代完成時,把父Promise對象resolve掉。這就成了一個類async函數了。
以上就是如何把generator函數轉爲類async的內容。
好啦,咱們繼續回來看koa的源碼。
當執行完app.use時,服務還沒啓動,只有當執行到app.listen(3000)時,程序才真正啓動。
koa源碼:
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製代碼
這裏使用了node原生http.createServer建立服務器,並把this.callback()做爲參數傳遞進去。
若是是用原生的node來寫,是下面這種語法:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('Hello World!');
res.end();
}).listen(8080);
複製代碼
上面的代碼中,http.createServer
方法傳入了一個回調函數。
每一次接收到一個新的請求的時候會調用這個回調函數,參數req和res分別是請求的實體和返回的實體,操做req能夠獲取收到的請求,操做res對應的是將要返回的packet。
弊端:callback
函數很是容易隨着業務邏輯的複雜也變得臃腫,即便把callback
函數拆分紅各個小函數,也會在繁雜的異步回調中漸漸失去對整個流程的把控。
解決: koa 把這些業務邏輯,拆分到不一樣的中間件中去處理
經常使用的中間件:
koa router
koa logger 日誌打印
koa cors 跨域
能夠知道,this.callback()返回的必定是這種形式:(req, res) => {}。繼續看下this.callback代碼。
callback() {
// compose處理全部中間件函數。洋蔥模型實現核心
const fn = compose(this.middleware);
// 每次請求執行函數(req, res) => {}
const handleRequest = (req, res) => {
// 基於req和res封裝ctx
const ctx = this.createContext(req, res);
// 調用handleRequest處理請求
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
// 調用context.js的onerror函數
const onerror = err => ctx.onerror(err);
// 處理響應內容
const handleResponse = () => respond(ctx);
// 確保一個流在關閉、完成和報錯時都會執行響應的回調函數
onFinished(res, onerror);
// 中間件執行、統一錯誤處理機制的關鍵
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
從上面源碼能夠看到,有這幾個細節很關鍵:
compose(this.middleware)作了什麼事情(使用了koa-compose包)。
如何實現洋蔥式調用的?
context是如何處理的?createContext的做用是什麼?
koa的統一錯誤處理機制是如何實現的?
下面,來進行一一講解。
這裏能夠結合以前的文章一塊兒看
context是如何處理的?createContext的做用是什麼?
context使用node原生的http監聽回調函數中的req、res來進一步封裝,意味着對於每個http請求,koa都會建立一個context並共享給全部的全局中間件使用,當全部的中間件執行完後,會將全部的數據統一交給res進行返回。因此,在每一箇中間件中咱們才能取得req的數據進行處理,最後ctx再把要返回的body給res進行返回。
記住句話:每個請求都有惟一一個context對象,全部的關於請求和響應的東西都放在其裏面。
下面來看context(即ctx)是怎麼封裝的:
// 單一context原則
createContext(req, res) {
const context = Object.create(this.context); // 建立一個對象,使之擁有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的原則,context必須做爲一個臨時對象存在,全部的東西都必須放進一個對象,所以,從上面源碼能夠看到,app、req、res屬性就此誕生。
請留意以上代碼,爲何app、req、res、ctx也存放在了request、和response對象中呢?
使它們同時共享一個app、req、res、ctx,是爲了將處理職責進行轉移,當用戶訪問時,只須要ctx就能夠獲取koa提供的全部數據和方法,而koa會繼續將這些職責進行劃分,好比request是進一步封裝req的,response是進一步封裝res的,這樣職責獲得了分散,降低了耦合度,同時共享全部資源使context具備高內聚的性質,內部元素互相能訪問到。
在createContext中,還有這樣一行代碼:
context.state = {};
複製代碼
這裏的state是專門負責保存單個請求狀態的空對象,能夠根據須要來管理內部內容。
接下來,咱們再來看第四個問題:koa的統一錯誤處理機制是如何實現的?
回憶一下咱們如何在koa中統一處理錯誤,只須要讓koa實例監聽onerror事件就能夠了。則全部的中間件邏輯錯誤都會在這裏被捕獲並處理。以下所示:
app.on('error', err => { log.error('server error', err)});
複製代碼
這是怎麼作到的呢?核心代碼以下(在上面提到的application.js的handleRequest函數中):
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
// application.js也有onerror函數,但這裏使用了context的onerror,
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 這裏是中間件若是執行出錯的話,都能執行到onerror的關鍵!!!
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
這裏其實會有2個疑問:
請看下context.js的onerror:
onerror(err) { this.app.emit('error', err, this);}
複製代碼
這裏的this.app是對application的引用,當context.js調用onerror時,其實是觸發application實例的error事件。該事件是基於「Application類繼承自EventEmitter」這一事實。
function compose (middleware) {
return function (context, next) {
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)
}
}
}
}
複製代碼
還有外部處理:
// 這裏是中間件若是執行出錯的話,都能執行到onerror的關鍵!!!
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
複製代碼
主要涉及這幾個知識點:
async函數返回一個Promise對象
async函數內部拋出錯誤,會致使Promise對象變爲reject狀態。拋出的錯誤會被catch的回調函數(上面爲onerror)捕獲到。
await命令後面的Promise對象若是變爲reject狀態,reject的參數也能夠被catch的回調函數(上面爲onerror)捕獲到。
這樣就能夠理解爲何koa能實現異步函數的統一錯誤處理了。
最後講一下koa中使用的設計模式——委託模式。
當咱們在使用context對象時,每每會這樣使用:
ctx.header 獲取請求頭
ctx.method 獲取請求方法
ctx.url 獲取請求url
這些對請求參數的獲取都得益於context.request的許多屬性都被委託在context上了
delegate(proto, 'request')
.method('acceptsLanguages')
...
.access('method')
...
.getter('URL')
.getter('header')
...;
複製代碼
又好比,
ctx.body 設置響應體
ctx.status 設置響應狀態碼
ctx.redirect() 請求重定向
這些對響應參數的設置都得益於koa中的context.response的許多方法都被委託在context對象上了:
delegate(proto, 'response')
.method('redirect')
...
.access('status')
.access('body')
...;
複製代碼
至於delegate的使用和源碼就不展開了
爲何 ctx 這裏代理用 delegate,而 response 和 request 代理原生的res
和 req
對象經過 getter
和 setter
? 由於 getter
和 setter
中能夠寫邏輯,作一些處理,delegate 只是單純的代理,本質仍是基於getter
和 setter
實現的。