Koa是一個類似于Express的Web開發框架,開發人員也是同一組人,但是使用了Generator函數,進行了架構的重新設計。也就是說,Koa的原理和內部結構很像Express,但是語法和內部結構進行了升級。
官方[faq](https://github.com/koajs/koa/blob/master/docs/faq.md#why-isnt-koa-just-express-40)有這樣一個問題:”為什么koa不是Express 4.0?“,回答是這樣的:”Koa與Express有很大差異,整個設計都是不同的,所以如果將Express 3.0按照這種寫法升級到4.0,就意味著重寫整個程序。所以,我們覺得創造一個新的庫,是更合適的做法。“
[TOC]
## Koa應用
一個Koa應用就是一個對象,包含了一個middleware數組,這個數組由一組Generator函數組成。這些函數負責對HTTP請求進行各種加工,比如生成緩存、指定代理、請求重定向等等。
~~~
var koa = require('koa');
var app = koa();
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
~~~
上面代碼中,變量app就是一個Koa應用。它監聽3000端口,返回一個內容為Hello World的網頁。
app.use方法用于向middleware數組添加Generator函數。
listen方法指定監聽端口,并啟動當前應用。它實際上等同于下面的代碼。
~~~
var http = require('http');
var koa = require('koa');
var app = koa();
http.createServer(app.callback()).listen(3000);
~~~
## 中間件
Koa的中間件很像Express的中間件,也是對HTTP請求進行處理的函數,但是必須是一個Generator函數。而且,Koa的中間件是一個級聯式(Cascading)的結構,也就是說,屬于是層層調用,第一個中間件調用第二個中間件,第二個調用第三個,以此類推。上游的中間件必須等到下游的中間件返回結果,才會繼續執行,這點很像遞歸。
中間件通過當前應用的use方法注冊。
~~~
app.use(function* (next){
var start = new Date; // (1)
yield next; // (2)
var ms = new Date - start; // (3)
console.log('%s %s - %s', this.method, this.url, ms); // (4)
});
~~~
上面代碼中,`app.use`方法的參數就是中間件,它是一個Generator函數,最大的特征就是function命令與參數之間,必須有一個星號。Generator函數的參數next,表示下一個中間件。
Generator函數內部使用yield命令,將程序的執行權轉交給下一個中間件,即`yield next`,要等到下一個中間件返回結果,才會繼續往下執行。上面代碼中,Generator函數體內部,第一行賦值語句首先執行,開始計時,第二行yield語句將執行權交給下一個中間件,當前中間件就暫停執行。等到后面的中間件全部執行完成,執行權就回到原來暫停的地方,繼續往下執行,這時才會執行第三行,計算這個過程一共花了多少時間,第四行將這個時間打印出來。
下面是一個兩個中間件級聯的例子。
~~~
app.use(function *() {
this.body = "header\n";
yield saveResults.call(this);
this.body += "footer\n";
});
function *saveResults() {
this.body += "Results Saved!\n";
}
~~~
上面代碼中,第一個中間件調用第二個中間件saveResults,它們都向`this.body`寫入內容。最后,`this.body`的輸出如下。
~~~
header
Results Saved!
footer
~~~
只要有一個中間件缺少`yield next`語句,后面的中間件都不會執行,這一點要引起注意。
~~~
app.use(function *(next){
console.log('>> one');
yield next;
console.log('<< one');
});
app.use(function *(next){
console.log('>> two');
this.body = 'two';
console.log('<< two');
});
app.use(function *(next){
console.log('>> three');
yield next;
console.log('<< three');
});
~~~
上面代碼中,因為第二個中間件少了`yield next`語句,第三個中間件并不會執行。
如果想跳過一個中間件,可以直接在該中間件的第一行語句寫上`return yield next`。
~~~
app.use(function* (next) {
if (skip) return yield next;
})
~~~
由于Koa要求中間件唯一的參數就是next,導致如果要傳入其他參數,必須另外寫一個返回Generator函數的函數。
~~~
function logger(format) {
return function *(next){
var str = format
.replace(':method', this.method)
.replace(':url', this.url);
console.log(str);
yield next;
}
}
app.use(logger(':method :url'));
~~~
上面代碼中,真正的中間件是logger函數的返回值,而logger函數是可以接受參數的。
### 多個中間件的合并
由于中間件的參數統一為next(意為下一個中間件),因此可以使用`.call(this, next)`,將多個中間件進行合并。
~~~
function *random(next) {
if ('/random' == this.path) {
this.body = Math.floor(Math.random()*10);
} else {
yield next;
}
};
function *backwards(next) {
if ('/backwards' == this.path) {
this.body = 'sdrawkcab';
} else {
yield next;
}
}
function *pi(next) {
if ('/pi' == this.path) {
this.body = String(Math.PI);
} else {
yield next;
}
}
function *all(next) {
yield random.call(this, backwards.call(this, pi.call(this, next)));
}
app.use(all);
~~~
上面代碼中,中間件all內部,就是依次調用random、backwards、pi,后一個中間件就是前一個中間件的參數。
Koa內部使用koa-compose模塊,進行同樣的操作,下面是它的源碼。
~~~
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
~~~
上面代碼中,middleware是中間件數組。前一個中間件的參數是后一個中間件,依次類推。如果最后一個中間件沒有next參數,則傳入一個空函數。
## 路由
可以通過`this.path`屬性,判斷用戶請求的路徑,從而起到路由作用。
~~~
app.use(function* (next) {
if (this.path === '/') {
this.body = 'we are at home!';
}
})
// 等同于
app.use(function* (next) {
if (this.path !== '/') return yield next;
this.body = 'we are at home!';
})
~~~
下面是多路徑的例子。
~~~
let koa = require('koa')
let app = koa()
// normal route
app.use(function* (next) {
if (this.path !== '/') {
return yield next
}
this.body = 'hello world'
});
// /404 route
app.use(function* (next) {
if (this.path !== '/404') {
return yield next;
}
this.body = 'page not found'
});
// /500 route
app.use(function* (next) {
if (this.path !== '/500') {
return yield next;
}
this.body = 'internal server error'
});
app.listen(8080)
~~~
上面代碼中,每一個中間件負責一個路徑,如果路徑不符合,就傳遞給下一個中間件。
復雜的路由需要安裝koa-router插件。
~~~
var app = require('koa')();
var Router = require('koa-router');
var myRouter = new Router();
myRouter.get('/', function *(next) {
this.response.body = 'Hello World!';
});
app.use(myRouter.routes());
app.listen(3000);
~~~
上面代碼對根路徑設置路由。
Koa-router實例提供一系列動詞方法,即一種HTTP動詞對應一種方法。典型的動詞方法有以下五種。
* router.get()
* router.post()
* router.put()
* router.del()
* router.patch()
這些動詞方法可以接受兩個參數,第一個是路徑模式,第二個是對應的控制器方法(中間件),定義用戶請求該路徑時服務器行為。
~~~
router.get('/', function *(next) {
this.body = 'Hello World!';
});
~~~
上面代碼中,`router.get`方法的第一個參數是根路徑,第二個參數是對應的函數方法。
注意,路徑匹配的時候,不會把查詢字符串考慮在內。比如,`/index?param=xyz`匹配路徑`/index`。
有些路徑模式比較復雜,Koa-router允許為路徑模式起別名。起名時,別名要添加為動詞方法的第一個參數,這時動詞方法變成接受三個參數。
~~~
router.get('user', '/users/:id', function *(next) {
// ...
});
~~~
上面代碼中,路徑模式`\users\:id`的名字就是`user`。路徑的名稱,可以用來引用對應的具體路徑,比如url方法可以根據路徑名稱,結合給定的參數,生成具體的路徑。
~~~
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
~~~
上面代碼中,user就是路徑模式的名稱,對應具體路徑`/users/:id`。url方法的第二個參數3,表示給定id的值是3,因此最后生成的路徑是`/users/3`。
Koa-router允許為路徑統一添加前綴。
~~~
var router = new Router({
prefix: '/users'
});
router.get('/', ...); // 等同于"/users"
router.get('/:id', ...); // 等同于"/users/:id"
~~~
路徑的參數通過`this.params`屬性獲取,該屬性返回一個對象,所有路徑參數都是該對象的成員。
~~~
// 訪問 /programming/how-to-node
router.get('/:category/:title', function *(next) {
console.log(this.params);
// => { category: 'programming', title: 'how-to-node' }
});
~~~
param方法可以針對命名參數,設置驗證條件。
~~~
router
.get('/users/:user', function *(next) {
this.body = this.user;
})
.param('user', function *(id, next) {
var users = [ '0號用戶', '1號用戶', '2號用戶'];
this.user = users[id];
if (!this.user) return this.status = 404;
yield next;
})
~~~
上面代碼中,如果`/users/:user`的參數user對應的不是有效用戶(比如訪問`/users/3`),param方法注冊的中間件會查到,就會返回404錯誤。
redirect方法會將某個路徑的請求,重定向到另一個路徑,并返回301狀態碼。
~~~
router.redirect('/login', 'sign-in');
// 等同于
router.all('/login', function *() {
this.redirect('/sign-in');
this.status = 301;
});
~~~
redirect方法的第一個參數是請求來源,第二個參數是目的地,兩者都可以用路徑模式的別名代替。
## context對象
中間件當中的this表示上下文對象context,代表一次HTTP請求和回應,即一次訪問/回應的所有信息,都可以從上下文對象獲得。context對象封裝了request和response對象,并且提供了一些輔助方法。每次HTTP請求,就會創建一個新的context對象。
~~~
app.use(function *(){
this; // is the Context
this.request; // is a koa Request
this.response; // is a koa Response
});
~~~
context對象的很多方法,其實是定義在ctx.request對象或ctx.response對象上面,比如,ctx.type和ctx.length對應于ctx.response.type和ctx.response.length,ctx.path和ctx.method對應于ctx.request.path和ctx.request.method。
context對象的全局屬性。
* request:指向Request對象
* response:指向Response對象
* req:指向Node的request對象
* req:指向Node的response對象
* app:指向App對象
* state:用于在中間件傳遞信息。
~~~
this.state.user = yield User.find(id);
~~~
上面代碼中,user屬性存放在`this.state`對象上面,可以被另一個中間件讀取。
context對象的全局方法。
* throw():拋出錯誤,直接決定了HTTP回應的狀態碼。
* assert():如果一個表達式為false,則拋出一個錯誤。
~~~
this.throw(403);
this.throw('name required', 400);
this.throw('something exploded');
this.throw(400, 'name required');
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
~~~
assert方法的例子。
~~~
// 格式
ctx.assert(value, [msg], [status], [properties])
// 例子
this.assert(this.user, 401, 'User not found. Please login!');
~~~
以下模塊解析POST請求的數據。
* co-body
* [https://github.com/koajs/body-parser](https://github.com/koajs/body-parser)
* [https://github.com/koajs/body-parsers](https://github.com/koajs/body-parsers)
~~~
var parse = require('co-body');
// in Koa handler
var body = yield parse(this);
~~~
## 錯誤處理機制
Koa提供內置的錯誤處理機制,任何中間件拋出的錯誤都會被捕捉到,引發向客戶端返回一個500錯誤,而不會導致進程停止,因此也就不需要forever這樣的模塊重啟進程。
~~~
app.use(function *() {
throw new Error();
});
~~~
上面代碼中,中間件內部拋出一個錯誤,并不會導致Koa應用掛掉。Koa內置的錯誤處理機制,會捕捉到這個錯誤。
當然,也可以額外部署自己的錯誤處理機制。
~~~
app.use(function *() {
try {
yield saveResults();
} catch (err) {
this.throw(400, '數據無效');
}
});
~~~
上面代碼自行部署了try...catch代碼塊,一旦產生錯誤,就用`this.throw`方法拋出。該方法可以將指定的狀態碼和錯誤信息,返回給客戶端。
對于未捕獲錯誤,可以設置error事件的監聽函數。
~~~
app.on('error', function(err){
log.error('server error', err);
});
~~~
error事件的監聽函數還可以接受上下文對象,作為第二個參數。
~~~
app.on('error', function(err, ctx){
log.error('server error', err, ctx);
});
~~~
如果一個錯誤沒有被捕獲,koa會向客戶端返回一個500錯誤“Internal Server Error”。
this.throw方法用于向客戶端拋出一個錯誤。
~~~
this.throw(403);
this.throw('name required', 400);
this.throw(400, 'name required');
this.throw('something exploded');
this.throw('name required', 400)
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
~~~
`this.throw`方法的兩個參數,一個是錯誤碼,另一個是報錯信息。如果省略狀態碼,默認是500錯誤。
`this.assert`方法用于在中間件之中斷言,用法類似于Node的assert模塊。
~~~
this.assert(this.user, 401, 'User not found. Please login!');
~~~
上面代碼中,如果this.user屬性不存在,會拋出一個401錯誤。
由于中間件是層級式調用,所以可以把`try { yield next }`當成第一個中間件。
~~~
app.use(function *(next) {
try {
yield next;
} catch (err) {
this.status = err.status || 500;
this.body = err.message;
this.app.emit('error', err, this);
}
});
app.use(function *(next) {
throw new Error('some error');
})
~~~
## cookie
cookie的讀取和設置。
~~~
this.cookies.get('view');
this.cookies.set('view', n);
~~~
get和set方法都可以接受第三個參數,表示配置參數。其中的signed參數,用于指定cookie是否加密。如果指定加密的話,必須用`app.keys`指定加密短語。
~~~
app.keys = ['secret1', 'secret2'];
this.cookies.set('name', '張三', { signed: true });
~~~
this.cookie的配置對象的屬性如下。
* signed:cookie是否加密。
* expires:cookie何時過期
* path:cookie的路徑,默認是“/”。
* domain:cookie的域名。
* secure:cookie是否只有https請求下才發送。
* httpOnly:是否只有服務器可以取到cookie,默認為true。
## session
~~~
var session = require('koa-session');
var koa = require('koa');
var app = koa();
app.keys = ['some secret hurr'];
app.use(session(app));
app.use(function *(){
var n = this.session.views || 0;
this.session.views = ++n;
this.body = n + ' views';
})
app.listen(3000);
console.log('listening on port 3000');
~~~
## Request對象
Request對象表示HTTP請求。
(1)this.request.header
返回一個對象,包含所有HTTP請求的頭信息。它也可以寫成`this.request.headers`。
(2)this.request.method
返回HTTP請求的方法,該屬性可讀寫。
(3)this.request.length
返回HTTP請求的Content-Length屬性,取不到值,則返回undefined。
(4)this.request.path
返回HTTP請求的路徑,該屬性可讀寫。
(5)this.request.href
返回HTTP請求的完整路徑,包括協議、端口和url。
~~~
this.request.href
// http://example.com/foo/bar?q=1
~~~
(6)this.request.querystring
返回HTTP請求的查詢字符串,不含問號。該屬性可讀寫。
(7)this.request.search
返回HTTP請求的查詢字符串,含問號。該屬性可讀寫。
(8)this.request.host
返回HTTP請求的主機(含端口號)。
(9)this.request.hostname
返回HTTP的主機名(不含端口號)。
(10)this.request.type
返回HTTP請求的Content-Type屬性。
~~~
var ct = this.request.type;
// "image/png"
~~~
(11)this.request.charset
返回HTTP請求的字符集。
~~~
this.request.charset
// "utf-8"
~~~
(12)this.request.query
返回一個對象,包含了HTTP請求的查詢字符串。如果沒有查詢字符串,則返回一個空對象。該屬性可讀寫。
比如,查詢字符串`color=blue&size=small`,會得到以下的對象。
~~~
{
color: 'blue',
size: 'small'
}
~~~
(13)this.request.fresh
返回一個布爾值,表示緩存是否代表了最新內容。通常與If-None-Match、ETag、If-Modified-Since、Last-Modified等緩存頭,配合使用。
~~~
this.response.set('ETag', '123');
// 檢查客戶端請求的內容是否有變化
if (this.request.fresh) {
this.response.status = 304;
return;
}
// 否則就表示客戶端的內容陳舊了,
// 需要取出新內容
this.response.body = yield db.find('something');
~~~
(14)this.request.stale
返回`this.request.fresh`的相反值。
(15)this.request.protocol
返回HTTP請求的協議,https或者http。
(16)this.request.secure
返回一個布爾值,表示當前協議是否為https。
(17)this.request.ip
返回發出HTTP請求的IP地址。
(18)this.request.subdomains
返回一個數組,表示HTTP請求的子域名。該屬性必須與app.subdomainOffset屬性搭配使用。app.subdomainOffset屬性默認為2,則域名“tobi.ferrets.example.com”返回["ferrets", "tobi"],如果app.subdomainOffset設為3,則返回["tobi"]。
(19)this.request.is(types...)
返回指定的類型字符串,表示HTTP請求的Content-Type屬性是否為指定類型。
~~~
// Content-Type為 text/html; charset=utf-8
this.request.is('html'); // 'html'
this.request.is('text/html'); // 'text/html'
this.request.is('text/*', 'text/html'); // 'text/html'
// Content-Type為s application/json
this.request.is('json', 'urlencoded'); // 'json'
this.request.is('application/json'); // 'application/json'
this.request.is('html', 'application/*'); // 'application/json'
~~~
如果不滿足條件,返回false;如果HTTP請求不含數據,則返回undefined。
~~~
this.is('html'); // false
~~~
它可以用于過濾HTTP請求,比如只允許請求下載圖片。
~~~
if (this.is('image/*')) {
// process
} else {
this.throw(415, 'images only!');
}
~~~
(20)this.request.accepts(types)
檢查HTTP請求的Accept屬性是否可接受,如果可接受,則返回指定的媒體類型,否則返回false。
~~~
// Accept: text/html
this.request.accepts('html');
// "html"
// Accept: text/*, application/json
this.request.accepts('html');
// "html"
this.request.accepts('text/html');
// "text/html"
this.request.accepts('json', 'text');
// => "json"
this.request.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
this.request.accepts('image/png');
this.request.accepts('png');
// false
// Accept: text/*;q=.5, application/json
this.request.accepts(['html', 'json']);
this.request.accepts('html', 'json');
// "json"
// No Accept header
this.request.accepts('html', 'json');
// "html"
this.request.accepts('json', 'html');
// => "json"
~~~
如果accepts方法沒有參數,則返回所有支持的類型(text/html,application/xhtml+xml,image/webp,application/xml,_/_)。
如果accepts方法的參數有多個參數,則返回最佳匹配。如果都不匹配則返回false,并向客戶端拋出一個406”Not Acceptable“錯誤。
如果HTTP請求沒有Accept字段,那么accepts方法返回它的第一個參數。
accepts方法可以根據不同Accept字段,向客戶端返回不同的字段。
~~~
switch (this.request.accepts('json', 'html', 'text')) {
case 'json': break;
case 'html': break;
case 'text': break;
default: this.throw(406, 'json, html, or text only');
}
~~~
(21)this.request.acceptsEncodings(encodings)
該方法根據HTTP請求的Accept-Encoding字段,返回最佳匹配,如果沒有合適的匹配,則返回false。
~~~
// Accept-Encoding: gzip
this.request.acceptsEncodings('gzip', 'deflate', 'identity');
// "gzip"
this.request.acceptsEncodings(['gzip', 'deflate', 'identity']);
// "gzip"
~~~
注意,acceptEncodings方法的參數必須包括identity(意為不編碼)。
如果HTTP請求沒有Accept-Encoding字段,acceptEncodings方法返回所有可以提供的編碼方法。
~~~
// Accept-Encoding: gzip, deflate
this.request.acceptsEncodings();
// ["gzip", "deflate", "identity"]
~~~
如果都不匹配,acceptsEncodings方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。
(22)this.request.acceptsCharsets(charsets)
該方法根據HTTP請求的Accept-Charset字段,返回最佳匹配,如果沒有合適的匹配,則返回false。
~~~
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"
this.request.acceptsCharsets(['utf-7', 'utf-8']);
// => "utf-8"
~~~
如果acceptsCharsets方法沒有參數,則返回所有可接受的匹配。
~~~
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets();
// ["utf-8", "utf-7", "iso-8859-1"]
~~~
如果都不匹配,acceptsCharsets方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。
(23)this.request.acceptsLanguages(langs)
該方法根據HTTP請求的Accept-Language字段,返回最佳匹配,如果沒有合適的匹配,則返回false。
~~~
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages('es', 'en');
// "es"
this.request.acceptsLanguages(['en', 'es']);
// "es"
~~~
如果acceptsCharsets方法沒有參數,則返回所有可接受的匹配。
~~~
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages();
// ["es", "pt", "en"]
~~~
如果都不匹配,acceptsLanguages方法返回false,并向客戶端拋出一個406“Not Acceptable”錯誤。
(24)this.request.socket
返回HTTP請求的socket。
(25)this.request.get(field)
返回HTTP請求指定的字段。
## Response對象
Response對象表示HTTP回應。
(1)this.response.header
返回HTTP回應的頭信息。
(2)this.response.socket
返回HTTP回應的socket。
(3)this.response.status
返回HTTP回應的狀態碼。默認情況下,該屬性沒有值。該屬性可讀寫,設置時等于一個整數。
(4)this.response.message
返回HTTP回應的狀態信息。該屬性與`this.response.message`是配對的。該屬性可讀寫。
(5)this.response.length
返回HTTP回應的Content-Length字段。該屬性可讀寫,如果沒有設置它的值,koa會自動從this.request.body推斷。
(6)this.response.body
返回HTTP回應的信息體。該屬性可讀寫,它的值可能有以下幾種類型。
* 字符串:Content-Type字段默認為text/html或text/plain,字符集默認為utf-8,Content-Length字段同時設定。
* 二進制Buffer:Content-Type字段默認為application/octet-stream,Content-Length字段同時設定。
* Stream:Content-Type字段默認為application/octet-stream。
* JSON對象:Content-Type字段默認為application/json。
* null(表示沒有信息體)
如果`this.response.status`沒設置,Koa會自動將其設為200或204。
(7)this.response.get(field)
返回HTTP回應的指定字段。
~~~
var etag = this.get('ETag');
~~~
注意,get方法的參數是區分大小寫的。
(8)this.response.set()
設置HTTP回應的指定字段。
~~~
this.set('Cache-Control', 'no-cache');
~~~
set方法也可以接受一個對象作為參數,同時為多個字段指定值。
~~~
this.set({
'Etag': '1234',
'Last-Modified': date
});
~~~
(9)this.response.remove(field)
移除HTTP回應的指定字段。
(10)this.response.type
返回HTTP回應的Content-Type字段,不包括“charset”參數的部分。
~~~
var ct = this.reponse.type;
// "image/png"
~~~
該屬性是可寫的。
~~~
this.reponse.type = 'text/plain; charset=utf-8';
this.reponse.type = 'image/png';
this.reponse.type = '.png';
this.reponse.type = 'png';
~~~
設置type屬性的時候,如果沒有提供charset參數,Koa會判斷是否自動設置。如果`this.response.type`設為html,charset默認設為utf-8;但如果`this.response.type`設為text/html,就不會提供charset的默認值。
(10)this.response.is(types...)
該方法類似于`this.request.is()`,用于檢查HTTP回應的類型是否為支持的類型。
它可以在中間件中起到處理不同格式內容的作用。
~~~
var minify = require('html-minifier');
app.use(function *minifyHTML(next){
yield next;
if (!this.response.is('html')) return;
var body = this.response.body;
if (!body || body.pipe) return;
if (Buffer.isBuffer(body)) body = body.toString();
this.response.body = minify(body);
});
~~~
上面代碼是一個中間件,如果輸出的內容類型為HTML,就會進行最小化處理。
(11)this.response.redirect(url, [alt])
該方法執行302跳轉到指定網址。
~~~
this.redirect('back');
this.redirect('back', '/index.html');
this.redirect('/login');
this.redirect('http://google.com');
~~~
如果redirect方法的第一個參數是back,將重定向到HTTP請求的Referrer字段指定的網址,如果沒有該字段,則重定向到第二個參數或“/”網址。
如果想修改302狀態碼,或者修改body文字,可以采用下面的寫法。
~~~
this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
~~~
(12)this.response.attachment([filename])
該方法將HTTP回應的Content-Disposition字段,設為“attachment”,提示瀏覽器下載指定文件。
(13)this.response.headerSent
該方法返回一個布爾值,檢查是否HTTP回應已經發出。
(14)this.response.lastModified
該屬性以Date對象的形式,返回HTTP回應的Last-Modified字段(如果該字段存在)。該屬性可寫。
~~~
this.response.lastModified = new Date();
~~~
(15)this.response.etag
該屬性設置HTTP回應的ETag字段。
~~~
this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');
~~~
注意,不能用該屬性讀取ETag字段。
(16)this.response.vary(field)
該方法將參數添加到HTTP回應的Vary字段。
## CSRF攻擊
CSRF攻擊是指用戶的session被劫持,用來冒充用戶的攻擊。
koa-csrf插件用來防止CSRF攻擊。原理是在session之中寫入一個秘密的token,用戶每次使用POST方法提交數據的時候,必須含有這個token,否則就會拋出錯誤。
~~~
var koa = require('koa');
var session = require('koa-session');
var csrf = require('koa-csrf');
var route = require('koa-route');
var app = module.exports = koa();
app.keys = ['session key', 'csrf example'];
app.use(session(app));
app.use(csrf());
app.use(route.get('/token', token));
app.use(route.post('/post', post));
function* token () {
this.body = this.csrf;
}
function* post() {
this.body = {ok: true};
}
app.listen(3000);
~~~
POST請求含有token,可以是以下幾種方式之一,koa-csrf插件就能獲得token。
* 表單的_csrf字段
* 查詢字符串的_csrf字段
* HTTP請求頭信息的x-csrf-token字段
* HTTP請求頭信息的x-xsrf-token字段
## 數據壓縮
koa-compress模塊可以實現數據壓縮。
~~~
app.use(require('koa-compress')())
app.use(function* () {
this.type = 'text/plain'
this.body = fs.createReadStream('filename.txt')
})
~~~
## 源碼解讀
每一個網站就是一個app,它由`lib/application`定義。
~~~
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
var app = Application.prototype;
exports = module.exports = Application;
~~~
`app.use()`用于注冊中間件,即將Generator函數放入中間件數組。
~~~
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are allowed
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
~~~
`app.listen()`就是`http.createServer(app.callback()).listen(...)`的縮寫。
~~~
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
var mw = [respond].concat(this.middleware);
var fn = this.experimental
? compose_es7(mw)
: co.wrap(compose(mw));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
}
};
~~~
上面代碼中,`app.callback()`會返回一個函數,用來處理HTTP請求。它的第一行`mw = [respond].concat(this.middleware)`,表示將respond函數(這也是一個Generator函數)放入`this.middleware`,現在mw就變成了`[respond, S1, S2, S3]`。
`compose(mw)`將中間件數組轉為一個層層調用的Generator函數。
~~~
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
~~~
上面代碼中,下一個generator函數總是上一個Generator函數的參數,從而保證了層層調用。
`var fn = co.wrap(gen)`則是將Generator函數包裝成一個自動執行的函數,并且返回一個Promise。
~~~
//co package
co.wrap = function (fn) {
return function () {
return co.call(this, fn.apply(this, arguments));
};
};
~~~
由于`co.wrap(compose(mw))`執行后,返回的是一個Promise,所以可以對其使用catch方法指定捕捉錯誤的回調函數`fn.call(ctx).catch(ctx.onerror)`。
將所有的上下文變量都放進context對象。
~~~
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var 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.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, this.keys);
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
~~~
真正處理HTTP請求的是下面這個Generator函數。
~~~
function *respond(next) {
yield *next;
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
res.end(body);
}
~~~
## 參考鏈接
* [Koa Guide](https://github.com/koajs/koa/blob/master/docs/guide.md)
* William XING,?[Is Koa.js right for me?](http://william.xingyp.com/is-koa-js-right-for-me/)
- 第一章 導論
- 1.1 前言
- 1.2 為什么學習JavaScript?
- 1.3 JavaScript的歷史
- 第二章 基本語法
- 2.1 語法概述
- 2.2 數值
- 2.3 字符串
- 2.4 對象
- 2.5 數組
- 2.6 函數
- 2.7 運算符
- 2.8 數據類型轉換
- 2.9 錯誤處理機制
- 2.10 JavaScript 編程風格
- 第三章 標準庫
- 3.1 Object對象
- 3.2 Array 對象
- 3.3 包裝對象和Boolean對象
- 3.4 Number對象
- 3.5 String對象
- 3.6 Math對象
- 3.7 Date對象
- 3.8 RegExp對象
- 3.9 JSON對象
- 3.10 ArrayBuffer:類型化數組
- 第四章 面向對象編程
- 4.1 概述
- 4.2 封裝
- 4.3 繼承
- 4.4 模塊化編程
- 第五章 DOM
- 5.1 Node節點
- 5.2 document節點
- 5.3 Element對象
- 5.4 Text節點和DocumentFragment節點
- 5.5 Event對象
- 5.6 CSS操作
- 5.7 Mutation Observer
- 第六章 瀏覽器對象
- 6.1 瀏覽器的JavaScript引擎
- 6.2 定時器
- 6.3 window對象
- 6.4 history對象
- 6.5 Ajax
- 6.6 同域限制和window.postMessage方法
- 6.7 Web Storage:瀏覽器端數據儲存機制
- 6.8 IndexedDB:瀏覽器端數據庫
- 6.9 Web Notifications API
- 6.10 Performance API
- 6.11 移動設備API
- 第七章 HTML網頁的API
- 7.1 HTML網頁元素
- 7.2 Canvas API
- 7.3 SVG 圖像
- 7.4 表單
- 7.5 文件和二進制數據的操作
- 7.6 Web Worker
- 7.7 SSE:服務器發送事件
- 7.8 Page Visibility API
- 7.9 Fullscreen API:全屏操作
- 7.10 Web Speech
- 7.11 requestAnimationFrame
- 7.12 WebSocket
- 7.13 WebRTC
- 7.14 Web Components
- 第八章 開發工具
- 8.1 console對象
- 8.2 PhantomJS
- 8.3 Bower:客戶端庫管理工具
- 8.4 Grunt:任務自動管理工具
- 8.5 Gulp:任務自動管理工具
- 8.6 Browserify:瀏覽器加載Node.js模塊
- 8.7 RequireJS和AMD規范
- 8.8 Source Map
- 8.9 JavaScript 程序測試
- 第九章 JavaScript高級語法
- 9.1 Promise對象
- 9.2 有限狀態機
- 9.3 MVC框架與Backbone.js
- 9.4 嚴格模式
- 9.5 ECMAScript 6 介紹
- 附錄
- 10.1 JavaScript API列表
- 草稿一:函數庫
- 11.1 Underscore.js
- 11.2 Modernizr
- 11.3 Datejs
- 11.4 D3.js
- 11.5 設計模式
- 11.6 排序算法
- 草稿二:jQuery
- 12.1 jQuery概述
- 12.2 jQuery工具方法
- 12.3 jQuery插件開發
- 12.4 jQuery.Deferred對象
- 12.5 如何做到 jQuery-free?
- 草稿三:Node.js
- 13.1 Node.js 概述
- 13.2 CommonJS規范
- 13.3 package.json文件
- 13.4 npm模塊管理器
- 13.5 fs 模塊
- 13.6 Path模塊
- 13.7 process對象
- 13.8 Buffer對象
- 13.9 Events模塊
- 13.10 stream接口
- 13.11 Child Process模塊
- 13.12 Http模塊
- 13.13 assert 模塊
- 13.14 Cluster模塊
- 13.15 os模塊
- 13.16 Net模塊和DNS模塊
- 13.17 Express框架
- 13.18 Koa 框架