深刻閱讀 koa 源碼

koa是什麼javascript

koa 2作了的事情:java

  1. 基於node原生req和res爲request和response對象賦能,並基於它們封裝成一個context對象。node

  2. 基於async/await 中間件洋蔥模型機制。json

koa1和koa2在源碼上的區別主要是於對異步中間件的支持方式的不一樣。設計模式

koa1是使用generator、yield)的模式。跨域

koa2使用的是async/await+Promise的模式。下文主要是針對koa2版本源碼上的講解。數組

初讀koa源碼

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.jsresponse.js 在實現邏輯上徹底一致,都是暴露出一個對象,對象屬性都經過gettersetter 來實現讀寫和控制。服務器

下面,咱們先初步瞭解koa的源碼內容,讀懂它們,能夠對koa有一個初步的瞭解。

application.js

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. 實現異步函數的統一錯誤處理機制

2.2 context.js

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對象的部分屬性和方法

request.js

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)。

response.js

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 源碼

下文會從初始化、啓動應用、處理請求等的角度,來對這過程當中比較重要的細節進行講解及延伸,若是完全弄懂,會對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件事

  1. 繼承Emitter,具有處理異步事件的能力。然而koa是如何處理,如今還不得而知,這裏打個問號。

  2. 在建立實例過程當中,有三個對象做爲實例的屬性被初始化,分別是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件事情

  1. 判斷是不是generator函數,若是是,使用koa-convert作轉換(koa3將再也不支持generator)。
  2. 全部傳入use方法會被push到middleware中。

如何將generator函數轉爲類async函數

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);
 }

複製代碼

從上面源碼能夠看到,有這幾個細節很關鍵:

  1. compose(this.middleware)作了什麼事情(使用了koa-compose包)。

  2. 如何實現洋蔥式調用的?

  3. context是如何處理的?createContext的做用是什麼?

  4. koa的統一錯誤處理機制是如何實現的?

下面,來進行一一講解。

koa-compose和洋蔥式調用

這裏能夠結合以前的文章一塊兒看

單一context原則

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個疑問:

  1. 出錯執行的回調函數是context.js的onerror函數,爲何在app上監聽onerror事件,就能處理全部中間件的錯誤呢?*

請看下context.js的onerror:

onerror(err) {    this.app.emit('error', err, this);}
複製代碼

這裏的this.app是對application的引用,當context.js調用onerror時,其實是觸發application實例的error事件。該事件是基於「Application類繼承自EventEmitter」這一事實。

  1. 如何作到集中處理全部中間件的錯誤?
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);
複製代碼

主要涉及這幾個知識點:

  1. async函數返回一個Promise對象

  2. async函數內部拋出錯誤,會致使Promise對象變爲reject狀態。拋出的錯誤會被catch的回調函數(上面爲onerror)捕獲到。

  3. 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 代理原生的resreq 對象經過 gettersetter ? 由於 gettersetter 中能夠寫邏輯,作一些處理,delegate 只是單純的代理,本質仍是基於gettersetter 實現的。

相關文章
相關標籤/搜索