8

Koa

次世代nodejs 的 web框架

简介

koa是由Express幕后团队打造的,目的是更小,更快,更稳定的web应用和apis。通过杠杆生成器(leveraging generators)Koa可以让你引导(ditch)回调函数,极大的提升错误处理。Koa核心不集成任何的中间件,其本身提供的优雅的功能套件就能够写出既快又nice的服务器。

安装

Koa需要node7.6.0或更高的版本,因为需要async function支持。
你可以使用自己的版本管理器很快的安装一个支持的版本。

nvm install 7
npm i koa
node my-koa-app.js

Async Function 结合 Babel

想要在较低版本的node中使用async函数,我们建议使用babel。

require('babel-core/register')
//然后在加载应用的主代码,这个必须在babel后面
const app = require('./app')

为了编译和转化async functions你需要在最后的压缩版本中使用'transform-async-to-generator'或者transform-async-to-module-method插件。例如,在你的.babelrc文件中,进行如下设置。

{
    "plugins":["transform-async-to-generator"]
}

你也可以使用stage-3 persent来代替。

应用 Application

一个Koa应用是一个对象,其包含一个数组,数组有很多函数组成的中间件,这些函数集合起来等待请求,并且执行时是按照类栈的方式。koa和很多其他中间件系统相似,你也许是用过RubyRack,Connect等。然而一个设计的决定行因素是提供高等级"sugar",与此同时低等级中间件层。因此提升了交互性,鲁棒性(软件设计术语,即稳定性)并且使得编写中间件更加的带劲!

这包括一些常用任务的方法——例如链接协调,缓存,代理支持,别用等。尽管提供了大量的有用的方法,但是koa仍然保持了一个较小的体积,因为没有绑定中间件。

怎么能偶少得了一个hello world应用。

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
    ctx.body = 'hello world';
})

app.listen(3000)

串联 Cascading

koa 的串联中间件使用了一个比较传统的方式,跟你通常用的工具很像。这是因为原先很难让用户有好的使用node的回调函数。然而使用异步函数我们能偶“真正得”是有中间件。相较于连接的实现,这个更容易提供一些列函数/功能来控制,在最后返回便可。koa调用"downstream",控制流返回"upstream".

下面的例子返回“hello world”,然而最开始请求先通过x-response-timelogging中间件来记录请求的开始。然后通过返回的中间件产出控制。当一个中间件执行next()函数,来延迟和传递控制给下一个声明的中间件。然后直到没有中间件需要执行downstream了,栈将会松开然后每个中间件复原去展现自己的“upstream”行为。

设置 Settings

应用设置即在实例app上的属性。当前支持如下:

  • app.env 默认是NODE_ENV或者“development”。

  • app.proxy 当设置为true时,porxy头部将被信任。

  • app.subdomainOffset 设置.subdomains的偏移量。替代[2]。

  • app.listen(...)
    一个Koa应用不是一对一的呈现一个htpp服务器。一个或者多个应用也许被添加到一块形成大的应用对应一个http服务器。

创建返回一个http服务器,传递给定的参数到Server#listen()。这些参数在nodejs.org都有说明。下面是一个无意义的Koa应用,绑定了端口3000

app.listen(...)方法是如下的一个语法糖。

const http = require('http')
const Koa = require('koa')
const app = new Koa()
http.createServer(app.callback()).listen(3000)

这说明你可以定义同一个应用为https和http或者多个地址。

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
http.createServer(app.callback()).listen(3001);
  • app.callback()
    返回一个回调函数,相当于http.createServer()方法,来出了请求。

你也可以使用这个方法在你的Connect/Express应用中添加koa应用。

  • app.use(function)

添加一个给定的中间件方法来实现它的功能。查看Middleware了解更多。

  • app.keys=
    设置cookie的键。

这些键被传递到KeyGrip,也许你想使用自己的KeyGrip,可以如下做。

app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');

这些键也许是循环的,并且可以设置{signed:true}来使用。

ctx.cookies.set('name','tobi',{signed:true})
  • app.context
    app.context是ctx的来源。你可以使用app.context添加额外的属性到ctx。这对于创建跨越整个app应用的属性或者方法来说是有用的,而且性能更好,在依赖上也跟简单,可以考虑做一个anti-pattern

例子,从ctx添加一个数据库引用。

add.context.db = db()

app.use(async (ctx)=>{
    console.log(ctx.db)
})

注意:

  • 通过getter和setter以及Object.difineProperty()设置的属性,你只能在app.context使用Object.defineProperty()来编辑他们。(不推荐)

  • 使用父级的ctx和设置来添加当前的应用。这样添加的app就能使用到那些中间件。

错误处理 Error Handling

除非设置app.silent是true,不然所有的出无输出都是标准输出。默认的错误输出不会处理像是err.sttus是404或者err.expose是true。为了自定义错误输出例如日志,你可以添加错误事件监听。

app.on('error', err =>
  log.error('server error', err)
);

当 req/res 周期中出现任何错误且无法响应客户端时,Koa 会把 Context(上下文) 实例作为第二个参数传递给 error 事件:

app.on('error', (err, ctx) =>
  log.error('server error', err, ctx)
);

如果有错误发生, 并且还能响应客户端(即没有数据被写入到 socket), Koa 会返回 500 "Internal Server Error". 这两种情况都会触发 app-level 的 error 事件, 用于 logging.

环境(Context)

一个Koa环境(实例)封装了node原生的请求和返回对象到一个单独的对象中,这个单独的对象提供了许多使用的方法,能够编写web应用和API。这些HTTP服务器开发中经常用到的操作被添加到当前等级,而不是高等级。他将强制中间件重新实现这些常用的功能。

一个环境Context在每次请求时被创建,并且被引用至中间件作为接收器,或者定义成this。如下所示。

app.use(function *(){
    this;//这里是koa环境context
    this.request;//是一个koa请求
    this.response;//是一个koa返回
})

很多context环境访问器和方法只是ctx.requestkoa请求或者ctx.responsekoa返回的代理,主要是为了方便。例如ctx.typectx.length代表response返回对象,ctx.pahtctx.methos代表请求。

API 接口。
环境(Context)定义的方法和访问器。

  • ctx.req Node的request对象。

  • ctx.res Node的response对象。
    绕开使用koa的response处理是不支持的。避免使用下面的node属性。

    • res.statusCode

    • res.writeHead()

    • res.write()

    • res.end()

  • ctx.request 一个Koa的request对象。

  • ctx.response 一个Koa的response对象。

  • ctx.state
    推荐的通过中间件传递信息给前端view(显示)的命名空间。

  • ctx.app 应用实例的引用。

  • ctx.cookies.get(name,[options])
    通过options获得cookie名字。

    • signed 要求cookie已经签名。
      koa使用cookie模块,这里只是传入选项即可。

  • ctx.coolies.set(name,value,[options])
    使用options设置name的值value

    • signed 签名cookie的值。

    • expires 使cookie的有效期到期。

    • path cookie路径,默认/

    • domain cookie域

    • secure 保护coolie

    • httpOnly 服务器端cookie,默认值true
      通过设置options来使用cookie模块。

  • ctx.throw([msg],[status],[properties])
    处理抛出错误的辅助方法,默认.status的值为500时抛出,koa在返回的信息中适当处理。限免的组合也是可疑的。

    • this.throw(403);

    • this.throw('name require', 400);

    • this.throw(400,'name require');

    • this.throw('something exploded');

例如:this.throw('name require', 400)等于

var err = new Error('name require');
err.status = 400;
throw err;

注意这些是用户自定义的错误,使用err.expose发出。因此只适合某些客户端的反馈。这些错误不同于内置的错误信息提醒,因为错误的详细信息不会泄露。

你也许传递一个properties选项对象,他和原来的错误信息进行了整合,对于人性化体验很有帮助,它报告个给请求者一个回溯流(upsteam)。

this.throw(401,'access_denied',{user:user});
this.throw('access_denied',{user:user});

koa使用http-errors来创建错误。

  • ctx.assert(value,[msg],[status],[properties])

    跑出错误辅助方法,类似`.throw()`,当`!value`是类似node的`assert()`方法。

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

koa使用http-assert实现断言(assertions)

  • ctx.response
    通过绕开koa内置的返回处理(response handling),你可以明确的设置this.response = false;如果你想使用原生的res对象处理而不是koa的response处理,那么就使用它。

注意那种用法koa不支持。这也许会打断koa中间件本来的功能,或者koa也被打断。使用这个属性最好考虑一下hack,这是使用传统的fn(req,res)方法和koa中间件的唯一方便的方法。

请求别名Request aliases
下面的访问起和Request别名相等。

  • ctx.header

  • ctx.headers

  • ctx.method

  • ctx.method=

  • ctx.url

  • ctx.url=

  • ctx.originalUrl

  • ctx.origin

  • ctx.href

  • ctx.path

  • ctx.query

  • ctx.query=

  • ctx.querystring

  • ctx.querystring=

  • ctx.host

  • ctx.hostname

  • ctx.fresh

  • ctx.stale

  • ctx.socket

  • ctx.protocol

  • ctx.secure

  • ctx.ip

  • ctx.ips

  • ctx.subdomains

  • ctx.is()

  • ctx.accepts()

  • ctx.acceptsEncodings()

  • ctx.acceptsCharsets()

  • ctx.acceptsLanguages()

  • ctx.get()

返回别名Response aliases
下面的访问起和返回别名相等

  • ctx.body

  • ctx.body=

  • ctx.status

  • ctx.status=

  • ctx.message

  • ctx.message=

  • ctx.length=

  • ctx.length

  • ctx.type

  • ctx.type=

  • ctx.handerSent

  • ctx.redirect()

  • ctx.attachment()

  • ctx.set()

  • ctx.append()

  • ctx.remove()

  • ctx.lastModified=

  • ctx.etag=

请求 Request

一个koa请求Request对象是个建立在node请求request之上的抽象。提供了一些额外的功能,这对每个http服务器开发者来说非常有用。

API

  • request.header

    Request header 对象
  • request.headers

    Requests header 对象,别名`request.header`。
  • request.method

    request.method
  • request.method=

    设置request method,实现中间件很有用,例如`methodoverride()`。
  • request.length

    返回request Content-lenght值是数字或者undefined。
  • request.url

    返回rquest URL
  • request.url=

    设置rquest URL,重写url时有用。
  • request.originalUrl

    返回request 原始 URL
  • request.orgin

    得到URL的域,包括协议和host(主机号)。
this.request.origin
//=>http://example.com
  • request.href

    返回全部request URL,包括协议,主机号,和url。
this.request.href
//=>http://example.com/foo/bar?q=1
  • request.path

    返回路径名(pathname)。
  • request.path=

    设置请求路径名字,保存查询参数
  • rquest.querystring

    得到原始的查询参数,不包含`?`。
  • request.querystring=

    设置原始的查询参数。
  • request.search

    得到原始的查询字符,带`?`。
  • request.search=

    设置原始的查询字符。
  • rquest.host

    得到主机号(hostname:port)当呈现时。支持`X-Forwarded-Host`当`app.proxy`是true,否则是常用的`host`。
  • request.hostname

    当有时返回hostname,支持`X-Frowarded-Host`当`app.proxy`是true,否则是常用的。
  • request.type

    返回request的`Content-type`,无效的一些参数,如`charset`。
var ct = this.request.type.
//=>'image/png'
  • request.charset

    当有时返回request的charset,或者`undefined`。
this.request.charset
//=>'utf-8'
  • request.query

    返回解析过的查询字符query-string,如果没有则返回一个空对象。注意,这个getter不支持嵌套的解析nested parsing。例如:`color=blue&size=small`。
    
{
    color:'blue',
    size:'small'
}
  • request.query=
    设置查询字符query-string到给定的对象。注意给设置setter不支持嵌套对象。

this.query = {next:'/login'};
  • request.fresh

    检查请求缓存是否“刷新fresh”,或者内容是否发生改变。这个方法是为了`if-None-Match`和`if-Modified-Since`以及`last-modified`之间的缓存沟通。他必须能够引用到更改之后的返回头部response headers
//freshness check requeire stats 20x or 304
this.status = 200;
this.set('ETag','123');

//cache is ok
if(this.fresh) {
    this.status = 304;
    return;
}

//cache is stale
//fetch new data
shis.body = yield db.find('something');
  • request.stale

    与`request.fresh`相反
  • request.protocol

    返回请求协议,`https`或者`http`。支持`X-Forwarded-Proto`当`app.proxy`是true。
  • request.secure

    `this.protocol == "https"`的速记,用以检查一个求情能否通过安全传输层。
  • request.ip

    请求的远程地址。支持`X-Forwarded-For`当`app.proxy`为true。
  • request.ips

    当有`X-Forwarded-For`并且`app.proxy`可用,那么返回这些的ip的一个数组。
    从回溯upstream——>downstream预定,当上述不可用则返回一个空数组。
  • request.subdomains

    返回子域数组。
    子域是在主域之前的部分,由点`.`分开。默认情况下,应用的主域都被假设成倒数的那两个。可以通过`app.subdomainOffset`来改变。
    例如,如果域是`tobi.ferrest.example.com`,并且`app.subdomainOffset`没有设置,那么这个子域是['ferrets','tobi']。如果设置`app.subdomainOffset`为3,那么子域是['tobi']。
  • request.is(type...)

    检查接下来的请求是否包含`Content-Type`头部内容,它包含任何的mime类型。如果这里没有请求体,返回undefined。如果没有内容类型,或者匹配失败,返回false。其他的直接返回内容类型(mime)。
    
//Contetn-type:text/html;charset=utf-8
this.is('html');//=>'html'
this.is('text/html');//=>'text/html'
this.is('text/*', 'test/html');//=>'test/html'

//when Content-type is application/json
this.is('json','urlencoded');//=>'json'
this.is('application/json',);//=>'application/json'
this.is('html','application/*',);//=>'application/json'

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

例子:你只想只有图片能够发送到路由

if(this.is('image/*')) {
    //process
}else{
    this.throw(415,'image only!');
}
  • 内容协商 Content Negotiation
    koa请求request包含有用的内容写上工具,由acceptsnegotaitor支持实现,这些工具是:

    • request accepts(types)

    • rquest acceptsEncoding(types)

    • rquest acceptsCharsets(charsets)

    • rquest acceptsLanguages(langs)
      如果没有提供类型,那么所有可接受的类型将被返回。

如果提供了多个类型,最优匹配奖杯返回。如果没有匹配到,返回false,并且你应该发送406 "Not Acceptable"返回response给客户端。

在可以接受任何类型的地方丢失了accept头部。第一个匹配到的将被返回。因此提供科技收的类型是很重要的。

  • request.accepts(types)

    检查给定的类型是否是可接受的。当为true则返回最佳匹配,否则false。类型`type`的值也许是一个或者多个mime类型字符,例如'application/json',扩展名是'josn',或者一个数组`['josn','html','text/plain']`。
//Accept:text/html
this.accepts('html')
//=>'html'

//Accept:text/*, application/json
this.accepts('html')
//=>'html'
this.accepts('json', 'text')
//=>'json'
this.accepts('application/json')
//=>'application/json'

//Accept.text/*, application/json
this.accepts('image/png')
this.accepts('png')
//=>false

//Accept:text/*,q=.5, application/json
this.accepts(['html', 'json'])
this.accepts('html', 'json')
//=>json

//No Accepts header
this.accpts('html', 'json')
//=>html
this.accepts('json','html')
//=> json

你也许调用this.accepts()很多次,或者使用switch语句。

switch(this.accepts('json', 'html', 'text')) {
    case 'json': bareak;
    case 'html': bareak;
    case 'text': bareak;
    default: this.throw(406, 'json , html or text only');
}
  • request.acceptsEncodings(encodings)

    检查编码`encodings`是否可接受,true时返回最优匹配,否则返回false。
    注意,你应该包含一个`indentity`作为编码`encodings`之一。
//Accept-Encoding:gzip
this.acceptsEncodings('gzip', 'deflate', 'identify');
//=>gzip

this.acceptsEncodings(['gzip', 'deflate', 'identify'])
//=>gzip

当没有参数时,所有可接受的编码作为数组元素返回

//Accept-Encoding:gzip, deflate
this.acceptsEncodings();
//=>['gzip','deflate','identify']

注意如果用户明确发送identify为identify,q=0。虽然这是个特殊例子,你仍然需要处理这个情况,当方法返回false时。

  • request.acceptsCharsets(charsets)

    检查charset是否可接受,为true时返回最优匹配,否则返回false。

    //Accept-Charset:utf-8, iso-8859-1;q=0.2,utf-7;q=0.5

this.acceptsCharsets('utf-8','utf-7')
//=>utf-8

this.acceptsCharsets(['utf-7','utf-8']);
//=>utf-8

如果没有参数是则返回所有可接受的编码到一个数组。

//Accept-Charset:utf-8,iso-8859-1;q=0.2,utf-7;q=0.5
this.acceptsCharsets();
//=>['utf-8','utf-7','iso-8859-7']
  • request.acceptLanguages(langs)

    检查langs是否可接受,如果为true则返回最有匹配,否则返回false。
//Accept-Language:en;q=0.8,es,pt
this.acceptsLanguages('es','en');
//=>'es'

this.acceptsLanguages(['en','es']);
//=>'es'

当没有传入参数则返回所有的语言。

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

    价差请求是否idempotent(幂等)
  • request.socket

    返回请求的socket
  • request.get(field)

    返回请求头header

返回 Response

一个koa返回Response对象是个建立在node请求request之上的抽象。提供了一些额外的功能,这对每个http服务器开发者来说非常有用。

API

  • response.header
    返回header对象

  • response。headers
    返回header对象。response.header的别名

  • response.status
    返回response的状态,默认情况下response.status没有默认值,而res.statusCode的默认值是200。

  • response.status =
    通过数字设置状态值

    • 100 'continue'继续

    • 101 'switch protocols'换协议

    • 102 'processing'处理中

    • 200 'ok' ok

    • 201 'created'已创建

    • 202 'accepted' 已接受

    • 203 'non-authoritative information'无作者信息

    • 204 'no content' 无内容

    • 205 'reset content' 重置内容

    • 206 "partial content" 部分内容

    • 207 "multi-status" 多状态

    • 300 "multiple choices" 多选择

    • 301 "moved permanently" 移动到永久

    • 302 "moved temporarily" 移动到暂时

    • 303 "see other" 看其他

    • 304 "not modified" 没有改动

    • 305 "use proxy" 使用代理

    • 307 "temporary redirect" 暂时改向

    • 400 "bad request" 坏请求

    • 401 "unauthorized" 未经授权

    • 402 "payment required" 要求付款

    • 403 "forbidden" 禁止

    • 404 "not found" 没有发现

    • 405 "method not allowed" 方法不允许

    • 406 "not acceptable" 不接受

    • 407 "proxy authentication required" 要求代理授权

    • 408 "request time-out" 请求超时

    • 409 "conflict" 冲突

    • 410 "gone" 消失

    • 411 "length required" 要求长度

    • 412 "precondition failed" 预处理失败

    • 413 "request entity too large" 请求量太大

    • 414 "request-uri too large" 请求同意资源太大

    • 415 "unsupported media type" 不支持的媒体类型

    • 416 "requested range not satisfiable" 不满足请求范围

    • 417 "expectation failed" 不是期望值

    • 418 "i'm a teapot" 我是个茶壶???

    • 422 "unprocessable entity" 错误实体

    • 423 "locked" 已锁定

    • 424 "failed dependency" 依赖错误

    • 425 "unordered collection" 未预定集合

    • 426 "upgrade required" 要求更新

    • 428 "precondition required" 要求前提

    • 429 "too many requests" 过多请求

    • 431 "request header fields too large" 请求头的域太大

    • 500 "internal server error" 服务器内部错误

    • 501 "not implemented" 没有实现

    • 502 "bad gateway" 网关错误

    • 503 "service unavailable" 不可服务

    • 504 "gateway time-out" 网关超时

    • 505 "http version not supported" http版本不支持

    • 506 "variant also negotiates" 多样协商

    • 507 "insufficient storage" 存储不足

    • 509 "bandwidth limit exceeded" 超过带宽

    • 510 "not extended" 扩展错误

    • 511 "network authentication required" 要求网路授权证明
      注意:不要担心要记太多东西,你可以随时查看。

  • response.message
    得到返回状态的信息。默认情况下,response.message是和response.status匹配的。

  • response.message=
    设置返回状态信息。

  • response.length=
    设置内容的长度

  • response.length
    返回内容的长度,或者计算出的this.body的大小。值为数字。或者undifined

  • response.body
    得到response的body。

  • response.body=
    设置返回体(response.body)为如下之一:

    • String written

    • Buffer written

    • Stream piped

    • Object json-stringified

    • null no content response

    String
    Content-type是text/html或者text/plain,charset是utf-8.Content-length也需要设置。
    Buffer
    Content-type是application/octet-stream,Content-length也要设置。
    Stream
    Content-type是application/octet-stream.
    Object
    Content-type是application/json.

  • response.get(field)
    得到response头部的field的值,不区分大小写。

var etag = this.get('ETag');
  • response.set(field, value)

    设置response头部field的值。
this.set('Cache-control', 'no-cache');
  • response.append(field, value)

    给头部添加额为的域和值。
this.append('Link', '<http://127.0.0.1/>');
  • response.set(fields)

    使用对象设置头部的fields
this.set({
    'Etag':'1234',
    'Last-modified':date
});
  • response.remove(field)

    移除头部的某个域。
  • resposne.type

    返回Content-type的类型,没有其他参数——如‘charset’。
var ct = this.type;
//=>image/png
  • response.type

    通过名字或者扩展名设置Content-type
    
this.type = 'text/plain;charset=utf-8';
this.type = 'image/png';
this.type='.png';
this.type='png';

注意,每个字符编码charset都是为你选的最合适的,例如response.type='html',那么默认的字符编码是utf-8,然而明确定义一个完整的类型,如response.type='text/html',将不会有指定的字符编码。

  • response.is(type...)

    很类似于`this.request.is()`。检查response的类型是否是被支持的类型。这在创建那些对返回进行操作的中间件是非常有用。
    
    示例:这是一个压缩html返回response的中间件,除了stream不被压缩。
var minify = require('html-minifier');

app.use(function *minifyHtml(next){
    yield next;

    if(!this.response.is('html')) return;

    var body = this.body;
    if(!body||body.pipe) return;

    if(Buffer.isBuffer(body)) body = body.toString();
    this.body = minify(body);
})
  • response.redirect(url, [alt])

    把[302]状态码重导向至`url`。
    字符串`back`是一个特殊的例子,提供了引用这支持,当引用者不存在或者`/`没有使用。
    
this.redirect('back');
this.redirect('back','/index.html');
this.redirect('login');
this.redirect('http://google.com');

为了改变默认的状态302,只需在这个状态吗出现之前或者出现之后进行重导向即可。为了改变body,在其调用之后进行重定向。

this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
  • response.attachment([filename])

    设置`Content-disposition`为"attachment"为客户端发出下载的信号。
    文件的名字是可以指定的。
  • response.headerSent

    检查返回头response header是否早已发送。查看客户端是否通知错误信号非常有用。
  • response.lastModified

    返回`Last-Modified`最后修改头部的数据(如果存在)。
  • response.LastModified=

    设置`Last-Modified`头部为一个合适的UTC(国际标准时间)字符串。你也可以设置其为一个日期或者日期字符串。
this.response.lastModified = new Date();
  • response.etag=

    设置ETag到一个返回中,包括外面的双引号。注意,这里没有相应的response.etag的getter。
this.response.etag = crypto.createHash('md5'),update(this.body).digest('hex');
  • response.vary(field)
    激活field。


farmerz
1.4k 声望93 粉丝

可可西里,可可西里,我只是想去看一看。