Node.js Koa 框架

2021-09-15 16:33 更新

Koa是一個(gè)類似于Express的Web開(kāi)發(fā)框架,開(kāi)發(fā)人員也是同一組人,但是使用了Generator函數(shù),進(jìn)行了架構(gòu)的重新設(shè)計(jì)。也就是說(shuō),Koa的原理和內(nèi)部結(jié)構(gòu)很像Express,但是語(yǔ)法和內(nèi)部結(jié)構(gòu)進(jìn)行了升級(jí)。

官方faq有這樣一個(gè)問(wèn)題:”為什么koa不是Express 4.0?“,回答是這樣的:”Koa與Express有很大差異,整個(gè)設(shè)計(jì)都是不同的,所以如果將Express 3.0按照這種寫法升級(jí)到4.0,就意味著重寫整個(gè)程序。所以,我們覺(jué)得創(chuàng)造一個(gè)新的庫(kù),是更合適的做法?!?/p>

Koa應(yīng)用

一個(gè)Koa應(yīng)用就是一個(gè)對(duì)象,包含了一個(gè)middleware數(shù)組,這個(gè)數(shù)組由一組Generator函數(shù)組成。這些函數(shù)負(fù)責(zé)對(duì)HTTP請(qǐng)求進(jìn)行各種加工,比如生成緩存、指定代理、請(qǐng)求重定向等等。

var koa = require('koa');
var app = koa();

app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

上面代碼中,變量app就是一個(gè)Koa應(yīng)用。它監(jiān)聽(tīng)3000端口,返回一個(gè)內(nèi)容為Hello World的網(wǎng)頁(yè)。

app.use方法用于向middleware數(shù)組添加Generator函數(shù)。

listen方法指定監(jiān)聽(tīng)端口,并啟動(dòng)當(dāng)前應(yīng)用。它實(shí)際上等同于下面的代碼。

var http = require('http');
var koa = require('koa');
var app = koa();
http.createServer(app.callback()).listen(3000);

中間件

Koa的中間件很像Express的中間件,也是對(duì)HTTP請(qǐng)求進(jìn)行處理的函數(shù),但是必須是一個(gè)Generator函數(shù)。而且,Koa的中間件是一個(gè)級(jí)聯(lián)式(Cascading)的結(jié)構(gòu),也就是說(shuō),屬于是層層調(diào)用,第一個(gè)中間件調(diào)用第二個(gè)中間件,第二個(gè)調(diào)用第三個(gè),以此類推。上游的中間件必須等到下游的中間件返回結(jié)果,才會(huì)繼續(xù)執(zhí)行,這點(diǎn)很像遞歸。

中間件通過(guò)當(dāng)前應(yīng)用的use方法注冊(cè)。

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方法的參數(shù)就是中間件,它是一個(gè)Generator函數(shù),最大的特征就是function命令與參數(shù)之間,必須有一個(gè)星號(hào)。Generator函數(shù)的參數(shù)next,表示下一個(gè)中間件。

Generator函數(shù)內(nèi)部使用yield命令,將程序的執(zhí)行權(quán)轉(zhuǎn)交給下一個(gè)中間件,即yield next,要等到下一個(gè)中間件返回結(jié)果,才會(huì)繼續(xù)往下執(zhí)行。上面代碼中,Generator函數(shù)體內(nèi)部,第一行賦值語(yǔ)句首先執(zhí)行,開(kāi)始計(jì)時(shí),第二行yield語(yǔ)句將執(zhí)行權(quán)交給下一個(gè)中間件,當(dāng)前中間件就暫停執(zhí)行。等到后面的中間件全部執(zhí)行完成,執(zhí)行權(quán)就回到原來(lái)暫停的地方,繼續(xù)往下執(zhí)行,這時(shí)才會(huì)執(zhí)行第三行,計(jì)算這個(gè)過(guò)程一共花了多少時(shí)間,第四行將這個(gè)時(shí)間打印出來(lái)。

下面是一個(gè)兩個(gè)中間件級(jí)聯(lián)的例子。

app.use(function *() {
  this.body = "header\n";
  yield saveResults.call(this);
  this.body += "footer\n";
});

function *saveResults() {
  this.body += "Results Saved!\n";
}

上面代碼中,第一個(gè)中間件調(diào)用第二個(gè)中間件saveResults,它們都向this.body寫入內(nèi)容。最后,this.body的輸出如下。

header
Results Saved!
footer

只要有一個(gè)中間件缺少yield next語(yǔ)句,后面的中間件都不會(huì)執(zhí)行,這一點(diǎn)要引起注意。

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

上面代碼中,因?yàn)榈诙€(gè)中間件少了yield next語(yǔ)句,第三個(gè)中間件并不會(huì)執(zhí)行。

如果想跳過(guò)一個(gè)中間件,可以直接在該中間件的第一行語(yǔ)句寫上return yield next。

app.use(function* (next) {
  if (skip) return yield next;
})

由于Koa要求中間件唯一的參數(shù)就是next,導(dǎo)致如果要傳入其他參數(shù),必須另外寫一個(gè)返回Generator函數(shù)的函數(shù)。

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函數(shù)的返回值,而logger函數(shù)是可以接受參數(shù)的。

多個(gè)中間件的合并

由于中間件的參數(shù)統(tǒng)一為next(意為下一個(gè)中間件),因此可以使用.call(this, next),將多個(gè)中間件進(jìn)行合并。

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內(nèi)部,就是依次調(diào)用random、backwards、pi,后一個(gè)中間件就是前一個(gè)中間件的參數(shù)。

Koa內(nèi)部使用koa-compose模塊,進(jìn)行同樣的操作,下面是它的源碼。

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是中間件數(shù)組。前一個(gè)中間件的參數(shù)是后一個(gè)中間件,依次類推。如果最后一個(gè)中間件沒(méi)有next參數(shù),則傳入一個(gè)空函數(shù)。

路由

可以通過(guò)this.path屬性,判斷用戶請(qǐng)求的路徑,從而起到路由作用。

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)

上面代碼中,每一個(gè)中間件負(fù)責(zé)一個(gè)路徑,如果路徑不符合,就傳遞給下一個(gè)中間件。

復(fù)雜的路由需要安裝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);

上面代碼對(duì)根路徑設(shè)置路由。

Koa-router實(shí)例提供一系列動(dòng)詞方法,即一種HTTP動(dòng)詞對(duì)應(yīng)一種方法。典型的動(dòng)詞方法有以下五種。

  • router.get()
  • router.post()
  • router.put()
  • router.del()
  • router.patch()

這些動(dòng)詞方法可以接受兩個(gè)參數(shù),第一個(gè)是路徑模式,第二個(gè)是對(duì)應(yīng)的控制器方法(中間件),定義用戶請(qǐng)求該路徑時(shí)服務(wù)器行為。

router.get('/', function *(next) {
  this.body = 'Hello World!';
});

上面代碼中,router.get方法的第一個(gè)參數(shù)是根路徑,第二個(gè)參數(shù)是對(duì)應(yīng)的函數(shù)方法。

注意,路徑匹配的時(shí)候,不會(huì)把查詢字符串考慮在內(nèi)。比如,/index?param=xyz匹配路徑/index。

有些路徑模式比較復(fù)雜,Koa-router允許為路徑模式起別名。起名時(shí),別名要添加為動(dòng)詞方法的第一個(gè)參數(shù),這時(shí)動(dòng)詞方法變成接受三個(gè)參數(shù)。

router.get('user', '/users/:id', function *(next) {
 // ...
});

上面代碼中,路徑模式\users\:id的名字就是user。路徑的名稱,可以用來(lái)引用對(duì)應(yīng)的具體路徑,比如url方法可以根據(jù)路徑名稱,結(jié)合給定的參數(shù),生成具體的路徑。

router.url('user', 3);
// => "/users/3"

router.url('user', { id: 3 });
// => "/users/3"

上面代碼中,user就是路徑模式的名稱,對(duì)應(yīng)具體路徑/users/:id。url方法的第二個(gè)參數(shù)3,表示給定id的值是3,因此最后生成的路徑是/users/3。

Koa-router允許為路徑統(tǒng)一添加前綴。

var router = new Router({
  prefix: '/users'
});

router.get('/', ...); // 等同于"/users"
router.get('/:id', ...); // 等同于"/users/:id"

路徑的參數(shù)通過(guò)this.params屬性獲取,該屬性返回一個(gè)對(duì)象,所有路徑參數(shù)都是該對(duì)象的成員。

// 訪問(wèn) /programming/how-to-node
router.get('/:category/:title', function *(next) {
  console.log(this.params);
  // => { category: 'programming', title: 'how-to-node' }
});

param方法可以針對(duì)命名參數(shù),設(shè)置驗(yàn)證條件。

router
  .get('/users/:user', function *(next) {
    this.body = this.user;
  })
  .param('user', function *(id, next) {
    var users = [ '0號(hào)用戶', '1號(hào)用戶', '2號(hào)用戶'];
    this.user = users[id];
    if (!this.user) return this.status = 404;
    yield next;
  })

上面代碼中,如果/users/:user的參數(shù)user對(duì)應(yīng)的不是有效用戶(比如訪問(wèn)/users/3),param方法注冊(cè)的中間件會(huì)查到,就會(huì)返回404錯(cuò)誤。

redirect方法會(huì)將某個(gè)路徑的請(qǐng)求,重定向到另一個(gè)路徑,并返回301狀態(tài)碼。

router.redirect('/login', 'sign-in');

// 等同于
router.all('/login', function *() {
  this.redirect('/sign-in');
  this.status = 301;
});

redirect方法的第一個(gè)參數(shù)是請(qǐng)求來(lái)源,第二個(gè)參數(shù)是目的地,兩者都可以用路徑模式的別名代替。

context對(duì)象

中間件當(dāng)中的this表示上下文對(duì)象context,代表一次HTTP請(qǐng)求和回應(yīng),即一次訪問(wèn)/回應(yīng)的所有信息,都可以從上下文對(duì)象獲得。context對(duì)象封裝了request和response對(duì)象,并且提供了一些輔助方法。每次HTTP請(qǐng)求,就會(huì)創(chuàng)建一個(gè)新的context對(duì)象。

app.use(function *(){
  this; // is the Context
  this.request; // is a koa Request
  this.response; // is a koa Response
});

context對(duì)象的很多方法,其實(shí)是定義在ctx.request對(duì)象或ctx.response對(duì)象上面,比如,ctx.type和ctx.length對(duì)應(yīng)于ctx.response.type和ctx.response.length,ctx.path和ctx.method對(duì)應(yīng)于ctx.request.path和ctx.request.method。

context對(duì)象的全局屬性。

  • request:指向Request對(duì)象
  • response:指向Response對(duì)象
  • req:指向Node的request對(duì)象
  • req:指向Node的response對(duì)象
  • app:指向App對(duì)象
  • state:用于在中間件傳遞信息。
this.state.user = yield User.find(id);

上面代碼中,user屬性存放在this.state對(duì)象上面,可以被另一個(gè)中間件讀取。

context對(duì)象的全局方法。

  • throw():拋出錯(cuò)誤,直接決定了HTTP回應(yīng)的狀態(tài)碼。
  • assert():如果一個(gè)表達(dá)式為false,則拋出一個(gè)錯(cuò)誤。
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請(qǐng)求的數(shù)據(jù)。

var parse = require('co-body');

// in Koa handler
var body = yield parse(this);

錯(cuò)誤處理機(jī)制

Koa提供內(nèi)置的錯(cuò)誤處理機(jī)制,任何中間件拋出的錯(cuò)誤都會(huì)被捕捉到,引發(fā)向客戶端返回一個(gè)500錯(cuò)誤,而不會(huì)導(dǎo)致進(jìn)程停止,因此也就不需要forever這樣的模塊重啟進(jìn)程。

app.use(function *() {
  throw new Error();
});

上面代碼中,中間件內(nèi)部拋出一個(gè)錯(cuò)誤,并不會(huì)導(dǎo)致Koa應(yīng)用掛掉。Koa內(nèi)置的錯(cuò)誤處理機(jī)制,會(huì)捕捉到這個(gè)錯(cuò)誤。

當(dāng)然,也可以額外部署自己的錯(cuò)誤處理機(jī)制。

app.use(function *() {
  try {
    yield saveResults();
  } catch (err) {
    this.throw(400, '數(shù)據(jù)無(wú)效');
  }
});

上面代碼自行部署了try...catch代碼塊,一旦產(chǎn)生錯(cuò)誤,就用this.throw方法拋出。該方法可以將指定的狀態(tài)碼和錯(cuò)誤信息,返回給客戶端。

對(duì)于未捕獲錯(cuò)誤,可以設(shè)置error事件的監(jiān)聽(tīng)函數(shù)。

app.on('error', function(err){
  log.error('server error', err);
});

error事件的監(jiān)聽(tīng)函數(shù)還可以接受上下文對(duì)象,作為第二個(gè)參數(shù)。

app.on('error', function(err, ctx){
  log.error('server error', err, ctx);
});

如果一個(gè)錯(cuò)誤沒(méi)有被捕獲,koa會(huì)向客戶端返回一個(gè)500錯(cuò)誤“Internal Server Error”。

this.throw方法用于向客戶端拋出一個(gè)錯(cuò)誤。

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方法的兩個(gè)參數(shù),一個(gè)是錯(cuò)誤碼,另一個(gè)是報(bào)錯(cuò)信息。如果省略狀態(tài)碼,默認(rèn)是500錯(cuò)誤。

this.assert方法用于在中間件之中斷言,用法類似于Node的assert模塊。

this.assert(this.user, 401, 'User not found. Please login!');

上面代碼中,如果this.user屬性不存在,會(huì)拋出一個(gè)401錯(cuò)誤。

由于中間件是層級(jí)式調(diào)用,所以可以把try { yield next }當(dāng)成第一個(gè)中間件。

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的讀取和設(shè)置。

this.cookies.get('view');
this.cookies.set('view', n);

get和set方法都可以接受第三個(gè)參數(shù),表示配置參數(shù)。其中的signed參數(shù),用于指定cookie是否加密。如果指定加密的話,必須用app.keys指定加密短語(yǔ)。

app.keys = ['secret1', 'secret2'];
this.cookies.set('name', '張三', { signed: true });

this.cookie的配置對(duì)象的屬性如下。

  • signed:cookie是否加密。
  • expires:cookie何時(shí)過(guò)期
  • path:cookie的路徑,默認(rèn)是“/”。
  • domain:cookie的域名。
  • secure:cookie是否只有https請(qǐng)求下才發(fā)送。
  • httpOnly:是否只有服務(wù)器可以取到cookie,默認(rèn)為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對(duì)象

Request對(duì)象表示HTTP請(qǐng)求。

(1)this.request.header

返回一個(gè)對(duì)象,包含所有HTTP請(qǐng)求的頭信息。它也可以寫成this.request.headers

(2)this.request.method

返回HTTP請(qǐng)求的方法,該屬性可讀寫。

(3)this.request.length

返回HTTP請(qǐng)求的Content-Length屬性,取不到值,則返回undefined。

(4)this.request.path

返回HTTP請(qǐng)求的路徑,該屬性可讀寫。

(5)this.request.href

返回HTTP請(qǐng)求的完整路徑,包括協(xié)議、端口和url。

this.request.href
// http://example.com/foo/bar?q=1

(6)this.request.querystring

返回HTTP請(qǐng)求的查詢字符串,不含問(wèn)號(hào)。該屬性可讀寫。

(7)this.request.search

返回HTTP請(qǐng)求的查詢字符串,含問(wèn)號(hào)。該屬性可讀寫。

(8)this.request.host

返回HTTP請(qǐng)求的主機(jī)(含端口號(hào))。

(9)this.request.hostname

返回HTTP的主機(jī)名(不含端口號(hào))。

(10)this.request.type

返回HTTP請(qǐng)求的Content-Type屬性。

var ct = this.request.type;
// "image/png"

(11)this.request.charset

返回HTTP請(qǐng)求的字符集。

this.request.charset
// "utf-8"

(12)this.request.query

返回一個(gè)對(duì)象,包含了HTTP請(qǐng)求的查詢字符串。如果沒(méi)有查詢字符串,則返回一個(gè)空對(duì)象。該屬性可讀寫。

比如,查詢字符串color=blue&size=small,會(huì)得到以下的對(duì)象。

{
  color: 'blue',
  size: 'small'
}

(13)this.request.fresh

返回一個(gè)布爾值,表示緩存是否代表了最新內(nèi)容。通常與If-None-Match、ETag、If-Modified-Since、Last-Modified等緩存頭,配合使用。

this.response.set('ETag', '123');

// 檢查客戶端請(qǐng)求的內(nèi)容是否有變化
if (this.request.fresh) {
  this.response.status = 304;
  return;
}

// 否則就表示客戶端的內(nèi)容陳舊了,
// 需要取出新內(nèi)容
this.response.body = yield db.find('something');

(14)this.request.stale

返回this.request.fresh的相反值。

(15)this.request.protocol

返回HTTP請(qǐng)求的協(xié)議,https或者h(yuǎn)ttp。

(16)this.request.secure

返回一個(gè)布爾值,表示當(dāng)前協(xié)議是否為https。

(17)this.request.ip

返回發(fā)出HTTP請(qǐng)求的IP地址。

(18)this.request.subdomains

返回一個(gè)數(shù)組,表示HTTP請(qǐng)求的子域名。該屬性必須與app.subdomainOffset屬性搭配使用。app.subdomainOffset屬性默認(rèn)為2,則域名“tobi.ferrets.example.com”返回["ferrets", "tobi"],如果app.subdomainOffset設(shè)為3,則返回["tobi"]。

(19)this.request.is(types...)

返回指定的類型字符串,表示HTTP請(qǐng)求的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請(qǐng)求不含數(shù)據(jù),則返回undefined。

this.is('html'); // false

它可以用于過(guò)濾HTTP請(qǐng)求,比如只允許請(qǐng)求下載圖片。

if (this.is('image/*')) {
  // process
} else {
  this.throw(415, 'images only!');
}

(20)this.request.accepts(types)

檢查HTTP請(qǐng)求的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方法沒(méi)有參數(shù),則返回所有支持的類型(text/html,application/xhtml+xml,image/webp,application/xml,/)。

如果accepts方法的參數(shù)有多個(gè)參數(shù),則返回最佳匹配。如果都不匹配則返回false,并向客戶端拋出一個(gè)406”Not Acceptable“錯(cuò)誤。

如果HTTP請(qǐng)求沒(méi)有Accept字段,那么accepts方法返回它的第一個(gè)參數(shù)。

accepts方法可以根據(jù)不同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)

該方法根據(jù)HTTP請(qǐng)求的Accept-Encoding字段,返回最佳匹配,如果沒(méi)有合適的匹配,則返回false。

// Accept-Encoding: gzip
this.request.acceptsEncodings('gzip', 'deflate', 'identity');
// "gzip"
this.request.acceptsEncodings(['gzip', 'deflate', 'identity']);
// "gzip"

注意,acceptEncodings方法的參數(shù)必須包括identity(意為不編碼)。

如果HTTP請(qǐng)求沒(méi)有Accept-Encoding字段,acceptEncodings方法返回所有可以提供的編碼方法。

// Accept-Encoding: gzip, deflate
this.request.acceptsEncodings();
// ["gzip", "deflate", "identity"]

如果都不匹配,acceptsEncodings方法返回false,并向客戶端拋出一個(gè)406“Not Acceptable”錯(cuò)誤。

(22)this.request.acceptsCharsets(charsets)

該方法根據(jù)HTTP請(qǐng)求的Accept-Charset字段,返回最佳匹配,如果沒(méi)有合適的匹配,則返回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方法沒(méi)有參數(shù),則返回所有可接受的匹配。

// 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,并向客戶端拋出一個(gè)406“Not Acceptable”錯(cuò)誤。

(23)this.request.acceptsLanguages(langs)

該方法根據(jù)HTTP請(qǐng)求的Accept-Language字段,返回最佳匹配,如果沒(méi)有合適的匹配,則返回false。

// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages('es', 'en');
// "es"
this.request.acceptsLanguages(['en', 'es']);
// "es"

如果acceptsCharsets方法沒(méi)有參數(shù),則返回所有可接受的匹配。

// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages();
// ["es", "pt", "en"]

如果都不匹配,acceptsLanguages方法返回false,并向客戶端拋出一個(gè)406“Not Acceptable”錯(cuò)誤。

(24)this.request.socket

返回HTTP請(qǐng)求的socket。

(25)this.request.get(field)

返回HTTP請(qǐng)求指定的字段。

Response對(duì)象

Response對(duì)象表示HTTP回應(yīng)。

(1)this.response.header

返回HTTP回應(yīng)的頭信息。

(2)this.response.socket

返回HTTP回應(yīng)的socket。

(3)this.response.status

返回HTTP回應(yīng)的狀態(tài)碼。默認(rèn)情況下,該屬性沒(méi)有值。該屬性可讀寫,設(shè)置時(shí)等于一個(gè)整數(shù)。

(4)this.response.message

返回HTTP回應(yīng)的狀態(tài)信息。該屬性與this.response.message是配對(duì)的。該屬性可讀寫。

(5)this.response.length

返回HTTP回應(yīng)的Content-Length字段。該屬性可讀寫,如果沒(méi)有設(shè)置它的值,koa會(huì)自動(dòng)從this.request.body推斷。

(6)this.response.body

返回HTTP回應(yīng)的信息體。該屬性可讀寫,它的值可能有以下幾種類型。

  • 字符串:Content-Type字段默認(rèn)為text/html或text/plain,字符集默認(rèn)為utf-8,Content-Length字段同時(shí)設(shè)定。
  • 二進(jìn)制Buffer:Content-Type字段默認(rèn)為application/octet-stream,Content-Length字段同時(shí)設(shè)定。
  • Stream:Content-Type字段默認(rèn)為application/octet-stream。
  • JSON對(duì)象:Content-Type字段默認(rèn)為application/json。
  • null(表示沒(méi)有信息體)

如果this.response.status沒(méi)設(shè)置,Koa會(huì)自動(dòng)將其設(shè)為200或204。

(7)this.response.get(field)

返回HTTP回應(yīng)的指定字段。

var etag = this.get('ETag');

注意,get方法的參數(shù)是區(qū)分大小寫的。

(8)this.response.set()

設(shè)置HTTP回應(yīng)的指定字段。

this.set('Cache-Control', 'no-cache');

set方法也可以接受一個(gè)對(duì)象作為參數(shù),同時(shí)為多個(gè)字段指定值。

this.set({
  'Etag': '1234',
  'Last-Modified': date
});

(9)this.response.remove(field)

移除HTTP回應(yīng)的指定字段。

(10)this.response.type

返回HTTP回應(yīng)的Content-Type字段,不包括“charset”參數(shù)的部分。

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';

設(shè)置type屬性的時(shí)候,如果沒(méi)有提供charset參數(shù),Koa會(huì)判斷是否自動(dòng)設(shè)置。如果this.response.type設(shè)為html,charset默認(rèn)設(shè)為utf-8;但如果this.response.type設(shè)為text/html,就不會(huì)提供charset的默認(rèn)值。

(10)this.response.is(types...)

該方法類似于this.request.is(),用于檢查HTTP回應(yīng)的類型是否為支持的類型。

它可以在中間件中起到處理不同格式內(nèi)容的作用。

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

上面代碼是一個(gè)中間件,如果輸出的內(nèi)容類型為HTML,就會(huì)進(jìn)行最小化處理。

(11)this.response.redirect(url, [alt])

該方法執(zhí)行302跳轉(zhuǎn)到指定網(wǎng)址。

this.redirect('back');
this.redirect('back', '/index.html');
this.redirect('/login');
this.redirect('http://google.com');

如果redirect方法的第一個(gè)參數(shù)是back,將重定向到HTTP請(qǐng)求的Referrer字段指定的網(wǎng)址,如果沒(méi)有該字段,則重定向到第二個(gè)參數(shù)或“/”網(wǎng)址。

如果想修改302狀態(tài)碼,或者修改body文字,可以采用下面的寫法。

this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';

(12)this.response.attachment([filename])

該方法將HTTP回應(yīng)的Content-Disposition字段,設(shè)為“attachment”,提示瀏覽器下載指定文件。

(13)this.response.headerSent

該方法返回一個(gè)布爾值,檢查是否HTTP回應(yīng)已經(jīng)發(fā)出。

(14)this.response.lastModified

該屬性以Date對(duì)象的形式,返回HTTP回應(yīng)的Last-Modified字段(如果該字段存在)。該屬性可寫。

this.response.lastModified = new Date();

(15)this.response.etag

該屬性設(shè)置HTTP回應(yīng)的ETag字段。

this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');

注意,不能用該屬性讀取ETag字段。

(16)this.response.vary(field)

該方法將參數(shù)添加到HTTP回應(yīng)的Vary字段。

CSRF攻擊

CSRF攻擊是指用戶的session被劫持,用來(lái)冒充用戶的攻擊。

koa-csrf插件用來(lái)防止CSRF攻擊。原理是在session之中寫入一個(gè)秘密的token,用戶每次使用POST方法提交數(shù)據(jù)的時(shí)候,必須含有這個(gè)token,否則就會(huì)拋出錯(cuò)誤。

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請(qǐng)求含有token,可以是以下幾種方式之一,koa-csrf插件就能獲得token。

  • 表單的_csrf字段
  • 查詢字符串的_csrf字段
  • HTTP請(qǐng)求頭信息的x-csrf-token字段
  • HTTP請(qǐng)求頭信息的x-xsrf-token字段

數(shù)據(jù)壓縮

koa-compress模塊可以實(shí)現(xiàn)數(shù)據(jù)壓縮。

app.use(require('koa-compress')())
app.use(function* () {
  this.type = 'text/plain'
  this.body = fs.createReadStream('filename.txt')
})

源碼解讀

每一個(gè)網(wǎng)站就是一個(gè)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()用于注冊(cè)中間件,即將Generator函數(shù)放入中間件數(shù)組。

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()會(huì)返回一個(gè)函數(shù),用來(lái)處理HTTP請(qǐng)求。它的第一行mw = [respond].concat(this.middleware),表示將respond函數(shù)(這也是一個(gè)Generator函數(shù))放入this.middleware,現(xiàn)在mw就變成了[respond, S1, S2, S3]。

compose(mw)將中間件數(shù)組轉(zhuǎn)為一個(gè)層層調(diào)用的Generator函數(shù)。

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(){}

上面代碼中,下一個(gè)generator函數(shù)總是上一個(gè)Generator函數(shù)的參數(shù),從而保證了層層調(diào)用。

var fn = co.wrap(gen)則是將Generator函數(shù)包裝成一個(gè)自動(dòng)執(zhí)行的函數(shù),并且返回一個(gè)Promise。

//co package
co.wrap = function (fn) {
  return function () {
    return co.call(this, fn.apply(this, arguments));
  };
};

由于co.wrap(compose(mw))執(zhí)行后,返回的是一個(gè)Promise,所以可以對(duì)其使用catch方法指定捕捉錯(cuò)誤的回調(diào)函數(shù)fn.call(ctx).catch(ctx.onerror)。

將所有的上下文變量都放進(jìn)context對(duì)象。

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請(qǐng)求的是下面這個(gè)Generator函數(shù)。

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

參考鏈接

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)