Koa源碼十分精簡,只有不到2k行的代碼,總共由4個模塊文件組成,很是適合咱們來學習。
javascript
參考代碼: learn-koa2 java
咱們先來看段原生Node實現Server服務器的代碼:node
const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200); res.end('hello world'); }); server.listen(3000, () => { console.log('server start at 3000'); });
很是簡單的幾行代碼,就實現了一個服務器Server。createServer
方法接收的callback
回調函數,能夠對每次請求的req
res
對象進行各類操做,最後返回結果。不過弊端也很明顯,callback
函數很是容易隨着業務邏輯的複雜也變得臃腫,即便把callback
函數拆分紅各個小函數,也會在繁雜的異步回調中漸漸失去對整個流程的把控。git
另外,Node原生提供的一些API,有時也會讓開發者疑惑:github
res.statusCode = 200; res.writeHead(200);
修改res
的屬性或者調用res
的方法均可以改變http
狀態碼,這在多人協做的項目中,很容易產生不一樣的代碼風格。編程
咱們再來看段Koa實現Server:segmentfault
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { console.log('1-start'); await next(); console.log('1-end'); }); app.use(async (ctx, next) => { console.log('2-start'); ctx.status = 200; ctx.body = 'Hello World'; console.log('2-end'); }); app.listen(3000); // 最後輸出內容: // 1-start // 2-start // 2-end // 1-end
Koa使用了中間件的概念來完成對一個http請求的處理,同時,Koa採用了async和await的語法使得異步流程能夠更好的控制。ctx
執行上下文代理了原生的res
和req
,這讓開發者避免接觸底層,而是經過代理訪問和設置屬性。api
看完二者的對比後,咱們應該會有幾個疑惑:數組
ctx.status
爲何就能夠直接設置狀態碼了,不是根本沒看到res
對象嗎?next
究竟是啥?爲何執行next
就進入了下一個中間件?如今讓咱們帶着疑惑,進行源碼解讀,同時本身實現一個簡易版的Koa吧!promise
參考代碼: step-1
// Koa的使用方法 const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
咱們首先模仿koa的使用方法,搭建一個最簡易的骨架:
新建kao/application.js
(特地使用了Kao,區別Koa
,並不是筆誤!!!)
const http = require('http'); class Application { constructor() { this.callbackFn = null; } use(fn) { this.callbackFn = fn; } callback() { return (req, res) => this.callbackFn(req, res) } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } } module.exports = Application;
新建測試文件kao/index.js
const Kao = require('./application'); const app = new Kao(); app.use(async (req, res) => { res.writeHead(200); res.end('hello world'); }); app.listen(3001, () => { console.log('server start at 3001'); });
咱們已經初步封裝好http server:經過new
實例一個對象,use
註冊回調函數,listen
啓動server並傳入回調。
注意的是:調用new
時,其實沒有開啓server服務器,真正開啓是在listen
調用時。
不過這段代碼有明顯的不足:
req
和res
咱們先來解決第一個問題
參考代碼: step-2
先來介紹下ES6中的get和set 參考
基於普通對象的get和set
const demo = { _name: '', get name() { return this._name; }, set name(val) { this._name = val; } }; demo.name = 'deepred'; console.log(demo.name);
基於Class
的get和set
class Demo { constructor() { this._name = ''; } get name() { return this._name; } set name(val) { this._name = val; } } const demo = new Demo(); demo.name = 'deepred'; console.log(demo.name);
基於Object.defineProperty
的get和set
const demo = { _name: '' }; Object.defineProperty(demo, 'name', { get: function() { return this._name }, set: function(val) { this._name = val; } });
基於Proxy
的get和set
const demo = { _name: '' }; const proxy = new Proxy(demo, { get: function(target, name) { return name === 'name' ? target['_name'] : undefined; }, set: function(target, name, val) { name === 'name' && (target['_name'] = val) } });
還有__defineSetter__
和__defineGetter__
的實現,不過現已廢棄。
const demo = { _name: '' }; demo.__defineGetter__('name', function() { return this._name; }); demo.__defineSetter__('name', function(val) { this._name = val; });
主要區別是,Object.defineProperty
__defineSetter__
Proxy
能夠動態設置屬性,而其餘方式只能在定義時設置。
Koa源碼中 request.js
和response.js
就使用了大量的get
和set
來代理
新建kao/request.js
module.exports = { get header() { return this.req.headers; }, set header(val) { this.req.headers = val; }, get url() { return this.req.url; }, set url(val) { this.req.url = val; }, }
當訪問request.url
時,其實就是在訪問原生的req.url
。須要注意的是,this.req
原生對象此時尚未注入!
同理新建kao/response.js
module.exports = { get status() { return this.res.statusCode; }, set status(code) { this.res.statusCode = code; }, get body() { return this._body; }, set body(val) { // 源碼裏有對val類型的各類判斷,這裏省略 /* 可能的類型 1. string 2. Buffer 3. Stream 4. Object */ this._body = val; } }
這裏對body進行操做並無使用原生的this.res.end,由於在咱們編寫koa代碼的時候,會對body進行屢次的讀取和修改,因此真正返回瀏覽器信息的操做是在application.js
裏進行封裝和操做
一樣須要注意的是,this.res
原生對象此時尚未注入!
新建kao/context.js
const delegate = require('delegates'); const proto = module.exports = { // context自身的方法 toJSON() { return { request: this.request.toJSON(), response: this.response.toJSON(), app: this.app.toJSON(), originalUrl: this.originalUrl, req: '<original node req>', res: '<original node res>', socket: '<original node socket>' }; }, } // delegates 原理就是__defineGetter__和__defineSetter__ // method是委託方法,getter委託getter,access委託getter和setter。 // proto.status => proto.response.status delegate(proto, 'response') .access('status') .access('body') // proto.url = proto.request.url delegate(proto, 'request') .access('url') .getter('header')
context.js
代理了request
和response
。ctx.body
指向ctx.response.body
。可是此時ctx.response
ctx.request
還沒注入!
可能會有疑問,爲何response.js
和request.js
使用get set
代理,而context.js
使用delegate
代理? 緣由主要是: set
和get
方法裏面還能夠加入一些本身的邏輯處理。而delegate
就比較純粹了,只代理屬性。
{ get length() { // 本身的邏輯 const len = this.get('Content-Length'); if (len == '') return; return ~~len; }, } // 僅僅代理屬性 delegate(proto, 'response') .access('length')
所以context.js
比較適合使用delegate
,僅僅是代理request
和response
的屬性和方法。
真正注入原生對象,是在application.js
裏的createContext
方法中注入的!!!
const http = require('http'); const context = require('./context'); const request = require('./request'); const response = require('./response'); class Application { constructor() { this.callbackFn = null; // 每一個Kao實例的context request respones this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } use(fn) { this.callbackFn = fn; } callback() { const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx) }; return handleRequest; } handleRequest(ctx) { const handleResponse = () => respond(ctx); // callbackFn是個async函數,最後返回promise對象 return this.callbackFn(ctx).then(handleResponse); } createContext(req, res) { // 針對每一個請求,都要建立ctx對象 // 每一個請求的ctx request response // ctx代理原生的req res就是在這裏代理的 let ctx = Object.create(this.context); ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; ctx.app = ctx.request.app = ctx.response.app = this; return ctx; } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } } module.exports = Application; function respond(ctx) { // 根據ctx.body的類型,返回最後的數據 /* 可能的類型,代碼刪減了部分判斷 1. string 2. Buffer 3. Stream 4. Object */ let content = ctx.body; if (typeof content === 'string') { ctx.res.end(content); } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)); } }
代碼中使用了Object.create
的方法建立一個全新的對象,經過原型鏈繼承原來的屬性。這樣能夠有效的防止污染原來的對象。
createContext
在每次http請求時都會調用,每次調用都新生成一個ctx
對象,而且代理了此次http請求的原生的對象。
respond
纔是最後返回http響應的方法。根據執行完全部中間件後ctx.body
的類型,調用res.end
結束這次http請求。
如今咱們再來測試一下:kao/index.js
const Kao = require('./application'); const app = new Kao(); // 使用ctx修改狀態碼和響應內容 app.use(async (ctx) => { ctx.status = 200; ctx.body = { code: 1, message: 'ok', url: ctx.url }; }); app.listen(3001, () => { console.log('server start at 3001'); });
參考代碼: step-3
const greeting = (firstName, lastName) => firstName + ' ' + lastName const toUpper = str => str.toUpperCase() const fn = compose([toUpper, greeting]); const result = fn('jack', 'smith'); console.log(result);
函數式編程有個compose
的概念。好比把greeting
和toUpper
組合成一個複合函數。調用這個複合函數,會先調用greeting
,而後把返回值傳給toUpper
繼續執行。
實現方式:
// 命令式編程(面向過程) function compose(fns) { let length = fns.length; let count = length - 1; let result = null; return function fn1(...args) { result = fns[count].apply(null, args); if (count <= 0) { return result } count--; return fn1(result); } } // 聲明式編程(函數式) function compose(funcs) { return funcs.reduce((a, b) => (...args) => a(b(...args))) }
Koa的中間件機制相似上面的compose
,一樣是把多個函數包裝成一個,可是koa的中間件相似洋蔥模型,也就是從A中間件執行到B中間件,B中間件執行完成之後,仍然能夠再次回到A中間件。
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!') } /** * @param {Object} context * @return {Promise} * @api public */ return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { // 一箇中間件裏屢次調用next if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // fn就是當前的中間件 let fn = middleware[i] if (i === middleware.length) fn = next // 最後一箇中間件若是也next時進入(通常最後一箇中間件是直接操做ctx.body,並不須要next了) if (!fn) return Promise.resolve() // 沒有中間件,直接返回成功 try { /* * 使用了bind函數返回新的函數,相似下面的代碼 return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) */ // dispatch.bind(null, i + 1)就是中間件裏的next參數,調用它就能夠進入下一個中間件 // fn若是返回的是Promise對象,Promise.resolve直接把這個對象返回 // fn若是返回的是普通對象,Promise.resovle把它Promise化 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { // 中間件是async的函數,報錯不會走這裏,直接在fnMiddleware的catch中捕獲 // 捕獲中間件是普通函數時的報錯,Promise化,這樣才能走到fnMiddleware的catch方法 return Promise.reject(err) } } } }
const context = {}; const sleep = (time) => new Promise(resolve => setTimeout(resolve, time)); const test1 = async (context, next) => { console.log('1-start'); context.age = 11; await next(); console.log('1-end'); }; const test2 = async (context, next) => { console.log('2-start'); context.name = 'deepred'; await sleep(2000); console.log('2-end'); }; const fn = compose([test1, test2]); fn(context).then(() => { console.log('end'); console.log(context); });
遞歸調用棧的執行狀況:
弄懂了中間件機制,咱們應該能夠回答以前的問題:
next
究竟是啥?洋蔥模型是怎麼實現的?
next就是一個包裹了dispatch的函數
在第n箇中間件中執行next,就是執行dispatch(n+1),也就是進入第n+1箇中間件
由於dispatch返回的都是Promise,因此在第n箇中間件await next(); 進入第n+1箇中間件。當第n+1箇中間件執行完成後,能夠返回第n箇中間件
若是在某個中間件中再也不調用next,那麼它以後的全部中間件都不會再調用了
修改kao/application.js
class Application { constructor() { this.middleware = []; // 存儲中間件 this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } use(fn) { this.middleware.push(fn); // 存儲中間件 } 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!') } /** * @param {Object} context * @return {Promise} * @api public */ 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) } } } } callback() { // 合成全部中間件 const fn = this.compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn) }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const handleResponse = () => respond(ctx); // 執行中間件並把最後的結果交給respond return fnMiddleware(ctx).then(handleResponse); } createContext(req, res) { // 針對每一個請求,都要建立ctx對象 let ctx = Object.create(this.context); ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; ctx.app = ctx.request.app = ctx.response.app = this; return ctx; } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } } module.exports = Application; function respond(ctx) { let content = ctx.body; if (typeof content === 'string') { ctx.res.end(content); } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)); } }
測試一下
const Kao = require('./application'); const app = new Kao(); app.use(async (ctx, next) => { console.log('1-start'); await next(); console.log('1-end'); }) app.use(async (ctx) => { console.log('2-start'); ctx.body = 'hello tc'; console.log('2-end'); }); app.listen(3001, () => { console.log('server start at 3001'); }); // 1-start 2-start 2-end 1-end
參考代碼: step-4
由於compose
組合以後的函數返回的仍然是Promise對象,因此咱們能夠在catch
捕獲異常
kao/application.js
handleRequest(ctx, fnMiddleware) { const handleResponse = () => respond(ctx); const onerror = err => ctx.onerror(err); // catch捕獲,觸發ctx的onerror方法 return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
kao/context.js
const proto = module.exports = { // context自身的方法 onerror(err) { // 中間件報錯捕獲 const { res } = this; if ('ENOENT' == err.code) { err.status = 404; } else { err.status = 500; } this.status = err.status; res.end(err.message || 'Internal error'); } }
const Kao = require('./application'); const app = new Kao(); app.use(async (ctx) => { // 報錯能夠捕獲 a.b.c = 1; ctx.body = 'hello tc'; }); app.listen(3001, () => { console.log('server start at 3001'); });
如今咱們已經實現了中間件的錯誤異常捕獲,可是咱們還缺乏框架層發生錯誤的捕獲機制。咱們可讓Application
繼承原生的Emitter
,從而實現error
監聽
kao/application.js
const Emitter = require('events'); // 繼承Emitter class Application extends Emitter { constructor() { // 調用super super(); this.middleware = []; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } }
kao/context.js
const proto = module.exports = { onerror(err) { const { res } = this; if ('ENOENT' == err.code) { err.status = 404; } else { err.status = 500; } this.status = err.status; // 觸發error事件 this.app.emit('error', err, this); res.end(err.message || 'Internal error'); } }
const Kao = require('./application'); const app = new Kao(); app.use(async (ctx) => { // 報錯能夠捕獲 a.b.c = 1; ctx.body = 'hello tc'; }); app.listen(3001, () => { console.log('server start at 3001'); }); // 監聽error事件 app.on('error', (err) => { console.log(err.stack); });
至此咱們能夠了解到Koa異常捕獲的兩種方式:
// 捕獲全局異常的中間件 app.use(async (ctx, next) => { try { await next() } catch (error) { return ctx.body = 'error' } } )
// 事件監聽 app.on('error', err => { console.log('error happends: ', err.stack); });
Koa整個流程能夠分紅三步:
初始化階段:
const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
new
初始化一個實例,use
蒐集中間件到middleware數組,listen
合成中間件fnMiddleware
,返回一個callback函數給http.createServer
,開啓服務器,等待http請求。
請求階段:
每次請求,createContext
生成一個新的ctx
,傳給fnMiddleware
,觸發中間件的整個流程
響應階段:
整個中間件完成後,調用respond
方法,對請求作最後的處理,返回響應給客戶端。
參考下面的流程圖: