半小時掌握koa
<h3 id="koa分析與實現">koa分析與實現</h3>
<h4 id="example">1.從一個例子開始</h4>
'use strict';
let Application = require('koa');
let app = new Application();
app.use(function*(next) {
console.log('東方紅');
yield next;
console.log('太陽升');
console.log();
});
app.use(function*(next) {
console.log('-1979年,');
yield next;
console.log('-那是一個春天');
});
app.use(function*() {
let message = '--我愛北京,天安門';
console.log(message);
this.body = 'hello world';
});
const PORT = 8888;
app.listen(PORT);
console.log('start at ' + PORT);
上面代碼,先后添加了3個middleware。請求的結果如下:
<pre>
東方紅
-1979年,
--我愛北京,天安門
-那是一個春天
太陽升
</pre>
<h4 id="abstract">2.koa之流程</h4>

- 每一個請求到達服務器后,初始化請求上下文對象
- 將上下文對象按照順序,在每個中間件中運行一遍
- 執行中間件1,next前的邏輯
- next
- 執行中間件2的邏輯
- 執行中間件1,next后的邏輯
- 將body返回給客戶端
上面的過程就是創建http服務器的時候,callback參數應該做的事情。
<h4 id="appObjectAnalyse">3.app對象分析</h4>
- 屬性:由于這里只是簡單的山寨一下koa,所以只需要一個middleware數組,用來存儲中間件。如果感興趣,想完善的話可以添加其他的屬性,實現相關功能。
- 公共方法:從開始的例子可以輕松看出有如下2個方法。
- use
- listen
- 私有方法:
- callback:創建http服務器的時候需要提供的回調函數
- response:相應函數
<h4 id="callback">4.callback實現</h4>
先從callback下手。因為,解決了這個函數,其他函數的實現就是輕松加愉快。
不過,開始我們就會發現一個問題。回調函數是node底層調用的,我們沒有辦法把middleware參數傳遞進。
這個時候,閉包玩的比較熟練的同學也許就會心中竊喜了。
沒錯解決這個問題的常用手段就是使用高階函數,利用js的閉包這一特性。
function callback(middleware) {
return function (req, res) {
}
}
解決了上邊的問題,我們順著流程寫就好了。
首先構造上下文對象
function callback(middleware) {
return function (req, res) {
let ctx = {
req: req,
res: res
};
}
}
之后需要做的事情就是,順序調用每一個middleware。但是我們手中有的數據結構只是一個generator function array,顯然完成不了任務。因此,考慮將它map一下,讓它變成一個類似function鏈表一樣的東西。
代碼如下:
let next = function*(){};
var i = middleware.length;
while (i--) {
next = middleware[i].call(ctx, next);
}
ps: 這里有一點不知道大家發現了沒有!!在每一個請求到來的時候,都需要進行一次這種map。也就是說,掛載的中間件越多,性能就會越差。我覺得這也是,koa1在性能上比express差的一個原因吧。 性能測試
接著,調用一下鏈表頭的function,就可以實現middleware的調用。
function callback(middleware) {
return function (req, res) {
let ctx = {
req: req,
res: res
};
co(function *() {
let next = function*(){};
var i = middleware.length;
while (i--) {
next = middleware[i].call(ctx, next);
}
return yield next;
})
}
}
最后,就是將上下文的body返回給前端。
function callback(middleware) {
return function (req, res) {
let ctx = {
req: req,
res: res
};
co(function *() {
let next = function*(){};
var i = middleware.length;
while (i--) {
next = middleware[i].call(ctx, next);
}
return yield next;
}).then(() => response.call(ctx))
.catch(e => console.log(e.stack));
}
}
以上就是整個callback的代碼,不過錯誤處理比較粗糙。大家可以自己完善一下-。
<h4 id="otherFn">5.其他function</h4>
其他function就比較簡單了。分別寫在下面
app.listen = function () {
var server = http.createServer(callback(this.middleware));
return server.listen.apply(server, arguments);
};
app.use = function (fn) {
this.middleware.push(fn);
};
function response() {
this.res.writeHead(200, {
'Content-Type': 'text/plain'
});
this.res.end(this.body);
}
最后整個myKoa的代碼
'use strict';
let co = require('co');
let http = require('http');
function Application() {
this.middleware = [];
}
var app = Application.prototype;
app.listen = function () {
var server = http.createServer(callback(this.middleware));
return server.listen.apply(server, arguments);
};
app.use = function (fn) {
this.middleware.push(fn);
};
function callback(middleware) {
return function (req, res) {
let ctx = {
req: req,
res: res
};
co(function *() {
let next = function*(){};
var i = middleware.length;
while (i--) {
next = middleware[i].call(ctx, next);
}
return yield next;
}).then(() => response.call(ctx))
.catch(e => console.log(e.stack));
}
}
//------private function
function response() {
this.res.writeHead(200, {
'Content-Type': 'text/plain'
});
this.res.end(this.body);
}
module.exports = Application;
<h3 id="koaCompoment">koa與他的小伙伴</h3>
<h4 id="koa-router">koa-router</h4>
<h5 id="router-memory">1.內存模型</h5>

上圖是一個簡化的koa-router的內存快照。
1個router最主要的屬性就是stack,他是一個layer的數組。每當我們調用一次router.verb或者router.use就會有一個layer被創造出來,放到這個數組中。
如上圖所示,每個layer主要有4個屬性。
請大家,務必記住這個內存模型,后文的討論都會圍繞這張圖進行。
<h5 id="router-flow">2.請求處理流程</h5>
在上一部分曾經說過,koa的每一個中間件都是一個generator function,koa-router也不能免俗。別看他代碼比koa要多,但是他就是一個函數而已。
接下來我們就看一下當請求到達服務器后這個函數都干了些什么。
- 首先便利router的stack數組,通過每一個layer的正則版本的路徑檢查當前請求的path是否符合這則表達式。
- 通過了第一步的檢查的layer,進入了第二階段的篩選。這個階段的標準是layer的methods屬性,methods為空數組或者methods數組中有當前請求的verb便通過篩選。
- 通過篩選的layer會被放入一個叫做pathAndMethod的數組中
- 我們下載可以看下下我們又有的數據結構-->一個fn*的二維數組。是否有種似曾相識的感覺囊。那么,下一步就是講數組變成fn鏈表,然后調用一下開頭的fn。
以上就是koa-router的整個流程。
<h5 id="router.verb">3.verb</h5>
相信大家對于這個方法的用法應該很熟悉了吧,我就在這里不多說了。
這里,我想強調的是。每次使用這個方法的時候就會增加一個layer。也就是說篩選合適的layer的操作消耗的時間就會變多。在設計跟優化程序的時候應該注意這個特征。
另外,推薦一種性能更好的使用方法
//method1
router.get('/blabla',fn1,fn2,...)
//method2
router.use('/blabla',fn1)
router.get('/blabla',fn2)
method1和2可以實現相同的功能,但是method1只會創建一個layer,這個layer會有兩個middleware。不難發現method1的效率會更高一些。
好了verb就說到這了吧。
<h5 id="router.use">3.use</h5>
關于這個方法,我想說的是,use不但可以use一個自己寫的中間件,而且還可以use一個router。這種方法可以讓我們實現用文件夾定義url的效果,我個人覺得這是一種有沒得方法。
var forums = new Router();
var posts = new Router();
posts.get('/', function *(next) {...});
posts.get('/:pid', function *(next) {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
// responds to "/forums/123/posts" and "/forums/123/posts/123"
app.use(forums.routes());
我想,也許有人會好奇這個功能是怎么實現的吧,當時看到api文檔的時候我也很好奇。下面我就來說一下吧。
直接上源代碼:
middleware = middleware.filter(function (fn) {
if (fn.router) {
fn.router.stack.forEach(function (layer) {
if (path) layer.setPrefix(path);
if (router.opts.prefix) layer.setPrefix(router.opts.prefix);
router.stack.push(layer);
});
if (router.params) {
Object.keys(router.params).forEach(function (key) {
fn.router.param(key, router.params[key]);
});
}
return false;
}
return true;
});
從上面可以很輕易的看出,作者是先檢測了一下這個fn,發現如果是router,那么讀取這個router的所有layer,把每一個layer設置一下前綴,然后直接放到父router的stack中。機智吧-.
<h5 id="router.param">3.param </h5>
最后要說的是param這個方法。
router
.param('user', function *(id, next) {
this.user = users[id];
if (!this.user) return this.status = 404;
yield next;
})
.get('/users/:user', function *(next) {
this.body = this.user;
})
.get('/users/:user/friends', function *(next) {
this.body = yield this.user.getFriends();
})
// /users/3 => {"id": 3, "name": "Alex"}
// /users/3/friends => [{"id": 4, "name": "TJ"}]
當時,看到這段代碼的時候,確實激動了一把。如作者說的,這個功能可以很方便的實現auto-loading作者是validation。
但是,但是,但是!!!!!!!同志們一定要注意,這里面有一個小小的坑。
閑言碎語不要說,我們直接上代碼
var app = require('koa')();
var router = require('koa-router')();
router
.get('/test/:id', function *() {
console.log('get', this.test);
this.body = 'hello world';
})
.use('/test/:id', function*(next) {
console.log('use', this.test)
yield next;
})
.param('id', function * (id, next) {
this.test = this.test || 0;
this.test = this.test + id;
yield next;
});
app.use(router.routes())
.use(router.allowedMethods());
app.listen(8888);
/**
輸出結果
use 01
get 011
**/
相信眼尖的通知們已經知道發生了什么!沒錯,param對應的fn執行了兩遍,這是因為param會把中間件放到layer的middleware數組中。
好了,雖然有點小小的瑕疵,但是,用的時候注意就好了。這個功能還是很好用的,吼吼吼。
以上就是koa-router我想說的全部內容,至于怎么用的話,看一下api文檔就好了,我只能說作者設計的很簡潔,一切說明都是廢話。
希望我也能早日設計出這么有沒得api。
<h4 id="koa-body">koa-body</h4>
這個組件使用起來也是非常方便的,在這里說一下的目的是,由于前一陣子公司使用的body parse用起來難用的不要不要的。所以在這里推薦一下吧。
下面的內容基本就是readme的翻譯,沒興趣的可以直接不看。
<h5>1.簡介</h5>
koa-body支持multipart, urlencoded 和 json 請求體,提供跟express的multer一樣的功能。他是對co-body和formidable的封裝。
<h5>2.使用方法</h5>
koa-body可以向multer一樣使用,非常簡單,因為你可以從ctx.request.body 或者 ctx.req.body 中獲得fields和files
var app = require('koa')(),
koaBody = require('koa-body');
app.use(koaBody({formidable:{uploadDir: __dirname}}));
app.use(function *(next) {
if (this.request.method == 'POST') {
console.log(this.request.body);
// => POST body
this.body = JSON.stringify(this.request.body);
}
yield next;
});
app.listen(3131)
koa-body還可以跟koa-router一起使用
var app = require('koa')(),
router = require('koa-router')(),
koaBody = require('koa-body')();
router.post('/users', koaBody,
function *(next) {
console.log(this.request.body);
// => POST body
this.body = JSON.stringify(this.request.body);
}
);
app.use(router.routes());
<h5>3.參數</h5>
- patchNode bool 把request的body給node的ctx.req,默認為false。
- patchKoa bool 把request的body給koa的ctx.req,默認為false。
- jsonLimit String|Integer json body的最大byte數,默認為1mb
- formLimit String|Integer form body的最大byte數,默認為56kb
- textLimit String|Integer text body的最大byte數,默認為56kb
- encoding String 設置field的編碼,默認為utf-8
- multipart Boolean 是否解析multipart body,默認為false
- formidable Object 床底給formidable的設置項
- strict bool 如果激活,koa-body不會解析GET,HEAD,DELETE請求,默認為true