閱讀目錄
|-- lib | |--- application.js | |--- context.js | |--- request.js | |--- response.js |__ package.json
application.js 是Koa2的入口檔案,它封裝了 context, request, response, 及 中介軟體處理的流程, 及 它向外匯出了class的實列,並且它繼承了Event, 因此該框架支援事件監聽和觸發的能力,比如程式碼: module.exports = class Application extends Emitter {}.
context.js 是處理應用的上下文ctx。它封裝了 request.js 和 response.js 的方法。
request.js 它封裝了處理http的請求。
response.js 它封裝了處理http響應。
因此實現koa2框架需要封裝和實現如下四個模組:
1. 封裝node http server. 建立koa類建構函式。
2. 構造request、response、及 context 物件。
3. 中介軟體機制的實現。
4. 錯誤捕獲和錯誤處理。
一:封裝node http server. 建立koa類建構函式
首先,如果我們使用node的原生模組實現一個簡單的伺服器,並且列印 hello world,程式碼一般是如下所示:
const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200); res.end('hello world....'); }); server.listen(3000, () => { console.log('listening on 3000'); });
因此實現koa的第一步是,我們需要對該原生模組進行封裝一下,我們首先要建立application.js實現一個Application物件。
基本程式碼封裝成如下(假如我們把程式碼放到 application.js裡面):
const Emitter = require('events'); const http = require('http'); class Application extends Emitter { /* 建構函式 */ constructor() { super(); this.callbackFunc = null; } // 開啟 http server 並且傳入引數 callback listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } use(fn) { this.callbackFunc = fn; } callback() { return (req, res) => { this.callbackFunc(req, res); } } } module.exports = Application;
然後我們在該目錄下新建一個 test.js 檔案,使用如下程式碼進行初始化如下:
const testKoa = require('./application'); const app = new testKoa(); app.use((req, res) => { res.writeHead(200); res.end('hello world....'); }); app.listen(3000, () => { console.log('listening on 3000'); });
如上基本程式碼我們可以看到,在application.js 我們簡單的封裝了一個 http server,使用app.use註冊回撥函式,app.listen監聽server,並傳入回撥函式。
但是如上程式碼有個缺點,app.use 傳入的回撥函式引數還是req,res, 也就是node原生的request和response物件,使用該物件還是不夠方便,它不符合框架的設計的易用性,我們需要封裝成如下的樣子:
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { console.log(11111); await next(); console.log(22222); }); app.listen(3000, () => { console.log('listening on 3000'); });
基於以上的原因,我們需要構造 request, response, 及 context物件了。
二:構造request、response、及 context 物件。
2.1 request.js
該模組的作用是對原生的http模組的request物件進行封裝,對request物件的某些屬性或方法通過重寫 getter/setter函式進行代理。
因此我們需要在我們專案中根目錄下新建一個request.js, 該檔案只有獲取和設定url的方法,最後匯出該檔案,程式碼如下:
const request = { get url() { return this.req.url; }, set url(val) { this.req.url = val; } }; module.exports = request;
如需要理解get/set對物件的監聽可以看我這篇文章
如上程式碼很簡單,匯出一個物件,該檔案中包含了獲取和設定url的方法,程式碼中this.req是node原生中的request物件,this.req.url則是node原生request中獲取url的方法。
2. response.js
response.js 也是對http模組的response物件進行封裝,通過對response物件的某些屬性或方法通過getter/setter函式進行代理。
同理我們需要在我們專案的根目錄下新建一個response.js。基本程式碼像如下所示:
const response = { get body() { return this._body; }, set body(data) { this._body = data; }, get status() { return this.res.statusCode; }, set status(statusCode) { if (typeof statusCode !== 'number') { throw new Error('statusCode 必須為一個數字'); } this.res.statusCode = statusCode; } }; module.exports = response;
程式碼也是如上一些簡單的程式碼,該檔案中有四個方法,分別是 body讀取和設定方法。讀取一個名為 this._body 的屬性。
status方法分別是設定或讀取 this.res.statusCode。同理:this.res是node原生中的response物件。
3. context.js
如上是簡單的 request.js 和 response.js ,那麼context的核心是將 request, response物件上的屬性方法代理到context物件上。也就是說 將會把 this.res.statusCode 就會變成 this.ctx.statusCode 類似於這樣的程式碼。request.js和response.js 中所有的方法和屬性都能在ctx物件上找到。
因此我們需要在專案中的根目錄下新建 context.js, 基本程式碼如下:
const context = { get url() { return this.request.url; }, set url(val) { this.request.url = val; }, get body() { return this.response.body; }, set body(data) { this.response.body = data; }, get status() { return this.response.statusCode; }, set status(statusCode) { if (typeof statusCode !== 'number') { throw new Error('statusCode 必須為一個數字'); } this.response.statusCode = statusCode; } }; module.exports = context;
如上程式碼可以看到context.js 是做一些常用方法或屬性的代理,比如通過 context.url 直接代理了 context.request.url.
context.body 代理了 context.response.body, context.status 代理了 context.response.status. 但是 context.request、context.response會在application.js中掛載的。
注意:想要瞭解 getter/setter 的代理原理可以看這篇文章.
如上是簡單的代理,但是當有很多代理的時候,我們一個個編寫有點繁瑣,因此我們可以通過 __defineSetter__ 和 __defineGetter__來實現,該兩個方法目前不建議使用,我們也可以通過Object.defineProperty這個來監聽物件。
但是目前在koa2中還是使用 delegates模組中的 __defineSetter__ 和 __defineGetter來實現的。delegates模組它的作用是將內部物件上的變數或函式委託到外部物件上。具體想要瞭解 delegates模組 請看我這篇文章。
因此我們的context.js 程式碼可以改成如下(當然我們需要引入delegates模組中的程式碼引入進來);
const delegates = require('./delegates'); const context = { // ..... 其他很多程式碼 }; // 代理request物件 delegates(context, 'request').access('url'); // 代理response物件 delegates(context, 'response').access('body').access('status'); /* const context = { get url() { return this.request.url; }, set url(val) { this.request.url = val; }, get body() { return this.response.body; }, set body(data) { this.response.body = data; }, get status() { return this.response.statusCode; }, set status(statusCode) { if (typeof statusCode !== 'number') { throw new Error('statusCode 必須為一個數字'); } this.response.statusCode = statusCode; } }; */ module.exports = context;
如上程式碼引入了 delegates.js 模組,然後使用該模組下的access的方法,該方法既擁有setter方法,也擁有getter方法,因此代理了request物件中的url方法,同時代理了context物件中的response屬性中的 body 和 status方法。
最後我們需要來修改application.js程式碼,引入request,response,context物件。如下程式碼:
const Emitter = require('events'); const http = require('http'); // 引入 context request, response 模組 const context = require('./context'); const request = require('./request'); const response = require('./response'); class Application extends Emitter { /* 建構函式 */ constructor() { super(); this.callbackFunc = null; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } // 開啟 http server 並且傳入引數 callback listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } use(fn) { this.callbackFunc = fn; } callback() { return (req, res) => { // this.callbackFunc(req, res); // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); this.callbackFunc(ctx).then(response); } } /* 構造ctx @param {Object} req實列 @param {Object} res 實列 @return {Object} ctx實列 */ createContext(req, res) { // 每個實列都要建立一個ctx物件 const ctx = Object.create(this.context); // 把request和response物件掛載到ctx上去 ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; } /* 響應訊息 @param {Object} ctx 實列 */ responseBody(ctx) { const content = ctx.body; if (typeof content === 'string') { ctx.res.end(content); } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)); } } } module.exports = Application;
如上程式碼可以看到在callback()函式內部,我們把之前的這句程式碼 this.callbackFunc(req, res); 註釋掉了,改成如下程式碼:
// 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); this.callbackFunc(ctx).then(response);
1. 首先是使用 createContext() 方法來建立ctx。然後把request對和response物件都直接掛載到了 ctx.request 和 ctx.response上,並且還將node原生的req/res物件掛載到了 ctx.request.req/ctx.req 和 ctx.response.res/ctx.res上了。
我們再來看下 request.js 的程式碼:
const request = { get url() { return this.req.url; }, set url(val) { this.req.url = val; } }; module.exports = request;
我們之前request.js 程式碼是如上寫的,比如 get url() 方法,返回的是 this.req.url, this.req是從什麼地方來的?之前我們並不理解,現在我們知道了。
1. 首先我們把request掛載到ctx實列上了,如程式碼:ctx.request = Object.create(this.request);然後node中的原生的req也掛載到ctx.req中了,如程式碼:ctx.req = ctx.request.req = req; 因此request.js 中的this指向了createContext方法中掛載到了對應的例項上。因此 this.req.url 實際上就是 ctx.req.url了。同理 this.res 也是一樣的道理的。
2. 其次,我們使用 const response = () => this.responseBody(ctx); 該方法把ctx實列作用引數傳入 responseBody方法內作為
響應內容。程式碼如下:
responseBody(ctx) { const content = ctx.body; if (typeof content === 'string') { ctx.res.end(content); } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)); } }
如上我們建立了 responseBody方法,該方法的作用是通過ctx.body讀取資訊,判斷該 ctx.body是否是字串或物件,如果是物件的話,也會把它轉為字串,最後呼叫 ctx.res.end() 方法返回資訊並關閉連線。
3. 最後我們呼叫該程式碼:this.callbackFunc(ctx).then(response); this.callbackFunc()函式就是我們使用koa中傳入的方法,比如如下koa程式碼:
app.use(async ctx => { console.log(ctx.status); // 列印狀態碼為200 ctx.body = 'hello world'; });
該回撥函式是一個async函式,然後返回給我們的引數是ctx物件,async函式返回的是一個promise物件,因此在原始碼中我們繼續呼叫then方法,把返回的內容掛載到ctx上。因此我們可以拿著ctx物件做我們自己想要做的事情了。
三:中介軟體機制的實現。
koa中的中介軟體是洋蔥型模型。具體的洋蔥模型的機制可以看這篇文章。
koa2中使用了async/await作為執行方式,具體理解 async/await的含義可以看我這篇文章介紹。
koa2中的中介軟體demo如下:
const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) => { console.log(11111); await next(); console.log(22222); }); app.use(async (ctx, next) => { console.log(33333); await next(); console.log(44444); }); app.use(async (ctx, next) => { console.log(55555); await next(); console.log(66666); }); app.listen(3001); console.log('app started at port 3000...'); // 執行結果為 11111 33333 55555 66666 44444 22222
如上執行結果為 11111 33333 55555 66666 44444 22222,koa2中的中介軟體模型為洋蔥型模型,當物件請求過來的時候,會依次經過各個中介軟體進行處理,當碰到 await next()時候就會跳到下一個中介軟體,當中介軟體沒有 await next執行的時候,就會逆序執行前面的中介軟體剩餘的程式碼,因此,先列印出 11111,然後碰到await next()函式,所以跳到下一個中介軟體去,就接著列印33333, 然後又碰到 await next(),因此又跳到下一個中介軟體,因此會列印55555, 列印完成後,繼續碰到 await next() 函式,但是後面就沒有中介軟體了,因此執行列印66666,然後逆序列印後面的資料了,先列印44444,執行完成後,就往上列印22222.
逆序如果我們不好理解的話,我們繼續來看下如下demo就能明白了。
function test1() { console.log(1) test2(); console.log(5) return Promise.resolve(); } function test2() { console.log(2) test3(); console.log(4) } function test3() { console.log(3) return; } test1();
如上程式碼列印的順序分別為 1, 2, 3, 4, 5; 上面的程式碼就是和koa2中的中介軟體逆序順序是一樣的哦。可以自己理解一下。
那麼現在我們想要實現這麼一個類似koa2中介軟體的這麼一個機制,我們該如何做呢?
我們都知道koa2中是使用了async/await來做的,假如我們現在有如下三個簡單的async函式:
// 假如下面是三個測試函式,想要實現 koa中的中介軟體機制 async function fun1(next) { console.log(1111); await next(); console.log('aaaaaa'); } async function fun2(next) { console.log(22222); await next(); console.log('bbbbb'); } async function fun3() { console.log(3333); }
如上三個簡單的函式,我現在想構造出一個函式,讓這三個函式依次執行,先執行fun1函式,列印1111,然後碰到 await next() 後,執行下一個函式 fun2, 列印22222, 再碰到 await next() 就執行fun3函式,列印3333,然後繼續列印 bbbbb, 再列印 aaaaa。
因此我們需要從第一個函式入手,因為首先列印的是 11111, 因此我們需要構造一個呼叫 fun1函式了。fun1函式的next引數需要能呼叫 fun2函式了,fun2函式中的next引數需要能呼叫到fun3函式了。因此程式碼改成如下:
// 假如下面是三個測試函式,想要實現 koa中的中介軟體機制 async function fun1(next) { console.log(1111); await next(); console.log('aaaaaa'); } async function fun2(next) { console.log(22222); await next(); console.log('bbbbb'); } async function fun3() { console.log(3333); } let next1 = async function () { await fun2(next2); } let next2 = async function() { await fun3(); } fun1(next1);
然後我們執行一下,就可以看到函式會依次執行,結果為:1111,22222,3333,bbbbb, aaaaaa;
如上就可以讓函式依次執行了,但是假如頁面有n箇中介軟體函式,我們需要依次執行怎麼辦呢?因此我們需要抽象成一個公用的函式出來,據koa2中application.js 原始碼中,首先會把所有的中介軟體函式放入一個陣列裡面去,比如原始碼中這樣的:
this.middleware.push(fn); 因此我們這邊首先也可以把上面的三個函式放入陣列裡面去,然後使用for迴圈依次迴圈呼叫即可:
如下程式碼:
async function fun1(next) { console.log(1111); await next(); console.log('aaaaaa'); } async function fun2(next) { console.log(22222); await next(); console.log('bbbbb'); } async function fun3() { console.log(3333); } function compose(middleware, oldNext) { return async function() { await middleware(oldNext); } } const middlewares = [fun1, fun2, fun3]; // 最後一箇中介軟體返回一個promise物件 let next = async function() { return Promise.resolve(); }; for (let i = middlewares.length - 1; i >= 0; i--) { next = compose(middlewares[i], next); } next();
最後依次會列印 1111 22222 3333 bbbbb aaaaaa了。
如上程式碼是怎麼執行的呢?首先我們會使用一個陣列 middlewares 儲存所有的函式,就像和koa2中一樣使用 app.use 後,會傳入async函式進去,然後會依次通過 this.middlewares 把對應的函式儲存到陣列裡面去。然後我們從陣列末尾依次迴圈該陣列最後把返回的值儲存到 next 變數裡面去。如上程式碼:
因此for迴圈第一次列印 middlewares[i], 返回的是 fun3函式,next傳進來的是 async function { return Promise.resolve()} 這樣的函式,最後返回該next,那麼此時該next儲存的值就是:
next = async function() { await func3(async function(){ return Promise.resolve(); }); }
for 迴圈第二次的時候,返回的是 fun2函式,next傳進來的是 上一次返回的函式,最後返回next, 那麼此時next儲存的值就是
next = async function() { await func2(async function() { await func3(async function(){ return Promise.resolve(); }); }); }
for迴圈第三次的時候,返回的是 fun1 函式,next傳進來的又是上一次返回的async函式,最後也返回next,那麼此時next的值就變為:
next = async function(){ await fun1(async function() { await fun2(async function() { await fun3(async function(){ return Promise.resolve(); }); }); }); };
因此我們下面呼叫 next() 函式的時候,會依次執行 fun1 函式,執行完成後,就會呼叫 fun2 函式,再執行完成後,接著呼叫fun3函式,依次類推..... 最後一個函式返回 Promise.resolve() 中Promise成功狀態。
如果上面的async 函式依次呼叫不好理解的話,我們可以繼續看如下demo;程式碼如下:
async function fun1(next) { console.log(1111); await next(); console.log('aaaaaa'); } async function fun2(next) { console.log(22222); await next(); console.log('bbbbb'); } async function fun3() { console.log(3333); } const next = async function(){ await fun1(async function() { await fun2(async function() { await fun3(async function(){ return Promise.resolve(); }); }); }); }; next();
最後結果也會依次列印 1111, 22222, 3333, bbbbb, aaaaaa;
因此上面就是我們的koa2中介軟體機制了。我們現在把我們總結的機制運用到我們application.js中了。因此application.js程式碼變成如下:
const Emitter = require('events'); const http = require('http'); // 引入 context request, response 模組 const context = require('./context'); const request = require('./request'); const response = require('./response'); class Application extends Emitter { /* 建構函式 */ constructor() { super(); // this.callbackFunc = null; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); // 儲存所有的中介軟體函式 this.middlewares = []; } // 開啟 http server 並且傳入引數 callback listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } use(fn) { // this.callbackFunc = fn; // 把所有的中介軟體函式存放到陣列裡面去 this.middlewares.push(fn); return this; } callback() { return (req, res) => { // this.callbackFunc(req, res); // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); //呼叫 compose 函式,把所有的函式合併 const fn = this.compose(); return fn(ctx).then(response); } } /* 構造ctx @param {Object} req實列 @param {Object} res 實列 @return {Object} ctx實列 */ createContext(req, res) { // 每個實列都要建立一個ctx物件 const ctx = Object.create(this.context); // 把request和response物件掛載到ctx上去 ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; } /* 響應訊息 @param {Object} ctx 實列 */ responseBody(ctx) { const content = ctx.body; if (typeof content === 'string') { ctx.res.end(content); } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)); } } /* 把傳進來的所有的中介軟體函式合併為一箇中介軟體 @return {function} */ compose() { // 該函式接收一個引數 ctx return async ctx => { function nextCompose(middleware, oldNext) { return async function() { await middleware(ctx, oldNext); } } // 獲取中介軟體的長度 let len = this.middlewares.length; // 最後一箇中介軟體返回一個promise物件 let next = async function() { return Promise.resolve(); }; for (let i = len; i >= 0; i--) { next = nextCompose(this.middlewares[i], next); } await next(); }; } } module.exports = Application;
1. 如上程式碼在建構函式內部 constructor 定義了一個變數 this.middlewares = []; 目的是儲存app.use(fn)所有的中介軟體函式,
2. 然後我們在use函式內部,不是把fn賦值,而是把fn放到一個陣列裡面去,如下程式碼:
use(fn) { // this.callbackFunc = fn; // 把所有的中介軟體函式存放到陣列裡面去 this.middlewares.push(fn); return this; }
3. 最後把所有的中介軟體函式合併為一箇中介軟體函式;如下compose函式的程式碼如下:
compose() { // 該函式接收一個引數 ctx return async ctx => { function nextCompose(middleware, oldNext) { return async function() { await middleware(ctx, oldNext); } } // 獲取中介軟體的長度 let len = this.middlewares.length; // 最後一箇中介軟體返回一個promise物件 let next = async function() { return Promise.resolve(); }; for (let i = len; i >= 0; i--) { next = nextCompose(this.middlewares[i], next); } await next(); }; }
該compose函式程式碼和我們之前的demo程式碼是一樣的。這裡就不多做解析哦。
4. 在callback函式內部改成如下程式碼:
callback() { return (req, res) => { /* // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); this.callbackFunc(ctx).then(response); */ // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); //呼叫 compose 函式,把所有的函式合併 const fn = this.compose(); return fn(ctx).then(response); } }
如上程式碼和之前版本的程式碼,最主要的區別是 最後兩句程式碼,之前的是直接把fn函式傳入到 this.callbackFunc函式內。現在是使用 this.compose()函式呼叫,把所有的async的中介軟體函式合併成一箇中介軟體函式後,把返回的合併後的中介軟體函式fn再去呼叫,這樣就會依次呼叫和初始化各個中介軟體函式,具體的原理機制我們上面的demo已經講過了,這裡就不再多描述了。
最後我們需要一個測試檔案,來測試該程式碼:如下在test.js 程式碼如下:
const testKoa = require('./application'); const app = new testKoa(); const obj = {}; app.use(async (ctx, next) => { obj.name = 'kongzhi'; console.log(1111); await next(); console.log('aaaaa'); }); app.use(async (ctx, next) => { obj.age = 30; console.log(2222); await next(); console.log('bbbbb') }); app.use(async (ctx, next) => { console.log(3333); console.log(obj); }); app.listen(3001, () => { console.log('listening on 3001'); });
我們執行下即可看到,在命令列中會依次列印如下所示:
如上是先列印1111,2222,3333,{'name': 'kongzhi', 'age': 30}, bbbbb, aaaaa.
因此如上就是koa2中的中介軟體機制了。
四:錯誤捕獲和錯誤處理。
一個非常不錯的框架,當異常的時候,都希望能捕獲到該異常,並且希望把該異常返回給客戶端,讓開發者知道異常的一些資訊。
比如koa2中的異常情況下,會報錯如下資訊:demo如下:
const Koa = require('koa'); const app = new Koa(); app.use((ctx) => { str += 'hello world'; // 沒有宣告該變數, 所以直接拼接字串會報錯 ctx.body = str; }); app.on('error', (err, ctx) => { // 捕獲異常記錄錯誤日誌 console.log(err); }); app.listen(3000, () => { console.log('listening on 3000'); });
如上程式碼,由於str是一個未定義的變數,因此和字串拼接的時候會報錯,但是koa2中我們可以使用 app.on('error', (err, ctx) => {}) 這樣的error方法來進行監聽的。因此在命令列中會報如下錯誤提示:
因此我們現在也是一樣,我們需要有對於某個中介軟體發生錯誤的時候,我們需要監聽error這個事件進行監聽。
因此我們需要定義一個onerror函式,當發生錯誤的時候,我們可以使用Promise中的catch方法來捕獲該錯誤了。
因此我們可以讓我們的Application繼承於Event這個物件,在koa2原始碼中的application.js 中有 onerror函式,我們把它複製到我們的Application.js 中,程式碼如下:
onerror(err) { if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)); if (404 == err.status || err.expose) return; if (this.silent) return; const msg = err.stack || err.toString(); console.error(); console.error(msg.replace(/^/gm, ' ')); console.error(); }
然後在我們我們的callback()函式中最後一句程式碼使用catch去捕獲這個異常即可:程式碼如下:
callback() { return (req, res) => { /* // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); this.callbackFunc(ctx).then(response); */ // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); // 響應時 呼叫error函式 const onerror = (err) => this.onerror(err, ctx); //呼叫 compose 函式,把所有的函式合併 const fn = this.compose(); return fn(ctx).then(response).catch(onerror); } }
因此Application.js 所有程式碼如下:
const Emitter = require('events'); const http = require('http'); // 引入 context request, response 模組 const context = require('./context'); const request = require('./request'); const response = require('./response'); class Application extends Emitter { /* 建構函式 */ constructor() { super(); // this.callbackFunc = null; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); // 儲存所有的中介軟體函式 this.middlewares = []; } // 開啟 http server 並且傳入引數 callback listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } use(fn) { // this.callbackFunc = fn; // 把所有的中介軟體函式存放到陣列裡面去 this.middlewares.push(fn); return this; } callback() { return (req, res) => { /* // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); this.callbackFunc(ctx).then(response); */ // 建立ctx const ctx = this.createContext(req, res); // 響應內容 const response = () => this.responseBody(ctx); // 響應時 呼叫error函式 const onerror = (err) => this.onerror(err, ctx); //呼叫 compose 函式,把所有的函式合併 const fn = this.compose(); return fn(ctx).then(response).catch(onerror); } } /** * Default error handler. * * @param {Error} err * @api private */ onerror(err) { if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err)); if (404 == err.status || err.expose) return; if (this.silent) return; const msg = err.stack || err.toString(); console.error(); console.error(msg.replace(/^/gm, ' ')); console.error(); } /* 構造ctx @param {Object} req實列 @param {Object} res 實列 @return {Object} ctx實列 */ createContext(req, res) { // 每個實列都要建立一個ctx物件 const ctx = Object.create(this.context); // 把request和response物件掛載到ctx上去 ctx.request = Object.create(this.request); ctx.response = Object.create(this.response); ctx.req = ctx.request.req = req; ctx.res = ctx.response.res = res; return ctx; } /* 響應訊息 @param {Object} ctx 實列 */ responseBody(ctx) { const content = ctx.body; if (typeof content === 'string') { ctx.res.end(content); } else if (typeof content === 'object') { ctx.res.end(JSON.stringify(content)); } } /* 把傳進來的所有的中介軟體函式合併為一箇中介軟體 @return {function} */ compose() { // 該函式接收一個引數 ctx return async ctx => { function nextCompose(middleware, oldNext) { return async function() { await middleware(ctx, oldNext); } } // 獲取中介軟體的長度 let len = this.middlewares.length; // 最後一箇中介軟體返回一個promise物件 let next = async function() { return Promise.resolve(); }; for (let i = len; i >= 0; i--) { next = nextCompose(this.middlewares[i], next); } await next(); }; } } module.exports = Application;
然後我們使用test.js 編寫測試程式碼如下:
const testKoa = require('./application'); const app = new testKoa(); app.use((ctx) => { str += 'hello world'; // 沒有宣告該變數, 所以直接拼接字串會報錯 ctx.body = str; }); app.on('error', (err, ctx) => { // 捕獲異常記錄錯誤日誌 console.log(err); }); app.listen(3000, () => { console.log('listening on 3000'); });
當我們在瀏覽器訪問的 http://localhost:3000/ 的時候,我們可以在命令列中看到如下報錯資訊了:
總結:如上就是實現一個簡單的koa2框架的基本原理,本來想把koa2原始碼也分析下,但是篇幅有限,所以下篇文章繼續把koa2所有的原始碼簡單的解讀下,其實看懂這篇文章後,已經可以理解95%左右的koa2原始碼了,只是說koa2原始碼中,比如request.js 會包含更多的方法,及 response.js 包含更多有用的方法等。