koa 2.0 - Middleware

  1. 1. 官方 middlewares 列表
  2. 2. 基本用法
  3. 3. middleware 原理
    1. 3.1. U型 嵌套 (洋葱嵌套)
    2. 3.2. 原理解析
  4. 4. V1 的支持方案
  5. 5. Middleware 能做的事
  6. 6. REF::

koa 2.0 系列:

Middleware 是 koa 里很重要的点,那关于 Middleware 的基本用法,实现原理和最终如何写一个 koa2 的中间件,大致的讲一下。


官方 middlewares 列表

这里是所有的 middleware 列表,支持 koa2 的不多,但也够用了,其实很多 middleware 真的就只是几句话的事,自己写也很方便。


基本用法

这个例子是从官方上改造的,加了总执行时长(x-response-time),并用到了以下特性:

  • koa2 context
  • async functions
  • arrow functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app.use(async (ctx, next) => {
// 启动 middleware,记录当前时间
let start = new Date;
// 进入下一个 middleware,并等待返回。
try {
await next() // next is now a function
} catch (err) {
ctx.body = { message: err.message }
ctx.status = err.status || 500
}
// 内部中间件执行完成,一般意味着业务执行完成。
// 这时,记录结束时间并输出。
let ms = new Date - start;
ctx.set('X-Response-Time', ms + 'ms');
})
// response
app.use(async ctx => {
// 这里处理业务,也就是最后的一个 middleware,所以不再调用 next()
const user = await User.getById(1) // await instead of yield
ctx.body = user // ctx instead of this
})


middleware 原理

U型 嵌套 (洋葱嵌套)

koa 最有特点的就是 U 型 middleware 的实现了,有点像面向切面编程 AOP 从上图可以看出,它顺序执行 middleware,到达最后一个时,又按队列再次顺序执行。

代码上理解就是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
.middleware1 {
// (1) do some stuff
.middleware2 {
// (2) do some other stuff
.middleware3 {
// (3) NO more middleware !
// ctx.body = 'hello world'
}
// (4) do some other stuff later
}
// (5) do some stuff lastest and return
}

原理解析

其实原理并不难,它是通过一个 compose 的库来实现这种的。


V1 的支持方案

koa2 已经不支持 generator 时代的 middleware,但提供了一个库来做转换 koa-convert,到 v3 时将彻底放弃。

1
2
3
4
import convert from 'koa-convert';
const app = new Koa();
app.use(convert(cors()));


Middleware 能做的事

  • 在请求到达前,做用户和权限的判断,做 RateLimit,限制 某个可用的 IP …
  • 在请求处理后,可以记录服务器处理时长,对结果统一格式化 ….

Express 的 Middleware 是单向的,就没有它这么方便。


REF::

koa 2.0 - Context

koa 2.0 系列:

在介绍篇时,提到过 Web Framework 的最重要的作为就是处理 request & reponse。

koa 将 node 的 request & reponse 对象封装在一个新的对象中
context,并提供一些更简单的 API 方便调用。

Context 是在每一个 request

Koa1 在调用时,使用的是 this,而 koa2 则是 ctx。

1
2
3
4
5
// Koa2
app.use(async (ctx, next) => {
ctx.body = 'hello world'
await next()
})


API

Context 提供的一些方法和 accessors(访问器)。

ctx.req

node request 对象,被封装到了 ctx.req

ctx.res

node response 对象,被封装到了 ctx.res

ctx.request

koa 的 Request 对象。是对 node 的 request 进一步抽象和封装,提供了日常 HTTP 服务器开发中一些有用的功能。

ctx.response

koa 的 Response 对象,是对 node 的 response 进一步抽象和封装,提供了日常 HTTP 服务器开发中一些有用的功能。

ctx.cookies

1
2
ctx.cookies.get(name, [options])
ctx.cookies.set(name, value, [options])

ctx.state

可以通过 State 向前端 View 传递数据:

1
this.state.user = await User.find(id);

ctx.app

Application 实例的指针。


cxt 访问器

context 提供一些 alias & accessors 以方便更快操作 request & response。

Request aliases

  • ctx.header
  • ctx.method
  • ctx.method=
  • ctx.url
  • ctx.url=
  • ctx.originalUrl
  • ctx.path
  • 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.length=
  • ctx.length
  • ctx.type=
  • ctx.type
  • ctx.headerSent
  • ctx.redirect()
  • ctx.attachment()
  • ctx.set(field, value) // 设置 response header 字段 field 的值为 value。
  • ctx.remove()
  • ctx.lastModified=
  • ctx.etag=

REF::

https://segmentfault.com/a/1190000006085240
https://segmentfault.com/a/1190000006145114

koa 2.0

  1. 1. Introduction
  2. 2. Why Koa
    1. 2.1. Callback hell
    2. 2.2. ES 2017
    3. 2.3. middleware U型嵌套 机制
    4. 2.4. 轻量化内核
  3. 3. koa 2.0 Roadmap
  4. 4. koa 2.0 Boilerplate
  5. 5. REF::

koa 2.0 系列:

Introduction

在开始 koa 之前,先回想下一个 Web Framework 应该要做的事。

  • 监听请求
  • 处理 request 中的 header/body
  • 返回 response 中的 header/body

统称 request/response 为 context,一般处理 context 不会只有一件事,大多数时间,我们需要权限验证,判断用户来源,RateLimit,日志,自定义返回数据 等等,这些事件为一个链状,context 顺序通过这些事件并对其进行加工,这些的事件在 express/koa 中叫 middleware

当你看完 代码 时,你会发现和我们设想的一样简单:

1
2
3
4
5
lib
├── application.js
├── context.js
├── request.js
└── response.js

所以 koa 内部主要关注两个要点:

  • context(上下文)
  • middleware(中间件)


Why Koa

讲了上述的点后,并没有说明为什么是 koa,而不是 express 或其它。选择 koa 主要是以下几个重要的点:

  • Callback hell
  • ES 2017
  • Middleware U型嵌套
  • 轻量化内核

Callback hell

1.0 中,是通过 Generator 来实现了 coroutine-like,取消了 callback。

Callback 有两大硬伤,一是不可组合,二是异常不可捕获。

ES 2017

当然,选择它的主要原因是还有 支持 ES 的新语法

2.0 中用了 async functions 代替了 1.0 中的 co 的 Generator functions,使语法更优雅。

middleware U型嵌套 机制

这是 koa 很有特色的一个地方,另花笔墨去写。

轻量化内核

和 express 不同的是,它只做了 Web Framework 应该做的,Router,View 都不再提供,更轻量化。

当然也提供了多样性,可以用更适合的 middleware 来做事情。


koa 2.0 Roadmap

koa 2.0 目前并没有正式发布,主要是为了等 V8 的 async functions 集成。但并不意味着不能投入使用,相反很多公司都已经应用于生产了。


koa 2.0 Boilerplate

这是我写的一个 koa2 的最佳实践项目,其中包括:

  • 完整的项目结构
  • koa-router 集成
  • Sequlize & MySQL
  • DI Container 支持
  • Class 语法
  • Babel 配置
  • Multiple Environment 支持

重点说明下,为什么和其它的项目结构有所不同,这里采用的是类 rails 的结构,而不是将所有代码都放置在 src 目录下,原因是 node 很快就会支持所有的新语法,编译不再是必须的。

REF::

RESTful Status in Deep

在 RESTful 中,Status Code 是 Sever 和 Client 通讯中第一个用到的标准。业界有一些很好的总结,这里记下自己的实践。

0x01: 成功类:

成功类的请求就相对简单了,直接返回对象就可以了(Modern Web App 中其实是不需要外面包一层 Envelope)。

200 Success GET, PATCH 表示返回正常 返回对象 { ...}
201 Created POST 表创建成功 会返回创建对象 { ... }
204 No Content DELETE 表删除成功,且不返回数据

POST, PUT, PATCH 都会返回该操作对象,只是 POST 有特殊 Code 201,其它都用 200


1x10: 失败类:

4XX Client Error:

400 Bad Request The request is malformed, such as if the body does not parse。 无效的请求,语义有误,当前请求无法被服务器理解
401 Unauthorized Bad credentials 未验证
403 Forbidden 未授权
404 Not Found 内容不存在,无论请求一个错误的地址,还是指定 id 不存在
409 Conflict
410 410 Indicates that the resource at this end point is no longer available. Useful as a blanket response for old API versions
422 Unprocessable Entity Used for validation errors
429 Too many request Rate limit exceeded, throttling

具象化的使用 Status code:

The request could not be understood by the server due to malformed syntax

5XX Server Error:

500 Internal Server Error 服务器内部错误,不受程序管理
501 Not Implemented 该接口未实现


1x20: 错误的格式 (Error Object)

业界一般的做法是返回 code 和 message 这两个字段

  • code 用于显示出错信息,客户端可定义。
  • message 一般是给 API 调用者看的,对开发友好。

有些需求是会返回一个 Error 数组,好处是,如果 API Body 中有多个值,可以分别校验,返回具体每一个值的出错信息,更友好。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Github
{
"message": "Problems parsing JSON",
"documentation_url": "https://developer.github.com/v3"
}
# Twitter
{
"errors": [{
"code": 135,
"message": "Timestamp out of bounds."
}]
}


1x30: 案例分析

1x31: Client Data Error

服务器无法理解这个请求。如果 request 包括无法解析的数据,应该返回一个 HTTP 400:

  • request 中包含的是一个无效的 JSON
  • request 缺少有效的 Query Parameters
1
2
3
4
5
6
7
8
# Status: 400 Bad Request
{
"errors": [{
"code": 4003,
"message": "Problems parsing JSON"
}]
}

1x32: 无法通过验证

之前通常会用 HTTP 400 错误码来标识用户提交的错误信息,但这个码太通用了,有时无法更具象表述出错信息。

Validation Errors 通过 HTTP 422 (Unprocessable Entity) 会比 HTTP 400 更具象,更合理。

Request matched and met syntactic contract but validation failed

1
2
3
4
5
6
7
8
# Github
# Status: 404 Unprocessable Entity
{
"message": "Problems parsing JSON",
"documentation_url": "https://developer.github.com/v3"
}

1x33: 用户相关的几个实例:

  • 密码错误: 这也是用户上传的数据也合格,但不能够通过 validation ,属于 HTTP 422 的范畴。
  • 但如果去访问一个资源,该资源需要 token,这时就是 HTTP 401。
  • 还有,如果有了 token ,但该资源需要特定的权限,这就是 HTTP 403。

1x34: 案例分析1:优惠券次数

举例:抢优惠券的业务,每个人有 3 次机会,如果用户前 3 次内点,和第 4 次请求,应如何处理。

但在这件事的最后定论是:这是一个业务问题,不应该在 HTTP 协议中来体现,所以最终请求会是这样:

1
2
3
4
5
6
{
"data": {
"success": true,
"prize": 1
}
}

事过情迁,我再去写 RESTful 时,发现这样并不好。首先先谈下好的方案:

Rate Limit Solution

HTTP 是有这种情况的方案的,Rate Limit,在 Header 中显示该接口的可调用情况:

X-RateLimit-Limit -
X-RateLimit-Remaining -
X-RateLimit-Reset -

超过三次后的请求

返回 403 Forbidden

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 403 Forbidden
Date: Tue, 20 Aug 2013 14:50:41 GMT
Status: 403 Forbidden
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1377013266
{
"code": 88,
"message": "API rate limit exceeded for xxx.xxx.xxx.xxx or username."
}

1x35: 案例分析1:兑奖

兑奖码无效

HTTP 404

未中奖

理论上未中奖是不能进入的,前端会限制的,但如果被绕开了,还是会有请求到达。

HTTP 400 Bad Request
HTTP 422 Unprocessable Entity

该码已兑换过

跟上一个一样

  • HTTP 409 Gone 忆兑换过,所以是用过了,Gone
  • HTTP 400 Bad Request 客户端为什么还要发这样的请求?所以是 Bad request
  • HTTP 422 Unprocessable Entity 因为后端验证出了问题。

用户输入兑奖信息不完整 (unpassed validation) ( HTTP 409)

更多 Rate Limit 参见:


REF::

Modern JavaScript - Class

  1. 1. 世界是属于对象的。
  2. 2. REF::

世界是属于对象的。

所有接触的语言都是面向对象的。遇到 js 这种异类,基于 prototype 原型链继承,就崩溃了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Circle {
constructor(radius) {
this.radius = radius;
Circle.circlesMade++;
};
static draw(circle, canvas) {
// Canvas绘制代码
};
static get circlesMade() {
return !this._count ? 0 : this._count;
};
static set circlesMade(val) {
this._count = val;
};
area() {
return Math.pow(this.radius, 2) * Math.PI;
};
get radius() {
return this._radius;
};
set radius(radius) {
if (!Number.isInteger(radius))
throw new Error("圆的半径必须为整数。");
this._radius = radius;
};
}

REF::

EventSource

Polling

轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
优点:后端程序编写比较容易。
缺点:请求中有大半是无用,浪费带宽和服务器资源。
实例:适于小型应用。

Long Polling

https://www.pubnub.com/blog/2014-12-01-http-long-polling/

长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。
实例:WebQQ、Hi网页版、Facebook IM。

WebSocket

HTML5 的 WebSocket 是一種建立在單一 TCP 連線上的全雙工(full-duplex)通訊管道,可以讓網頁應用程式與伺服器之間做即時性、雙向的資料傳遞。

WebSocket 跟以往實作全雙工的技術比起來,改進了非常多,不但減低網路頻寬的使用,有降低了網路延遲的時間。(關於網路的頻寬與延遲可參考這裡)

非HTTP协议,无法自动穿越防火墙

EF::

https://developer.mozilla.org/en-US/docs/Web/API/EventSource
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
SSE:服务器发送事件

http://stackoverflow.com/questions/12555043/my-understanding-of-http-polling-long-polling-http-streaming-and-websockets

Modern JavaScript - Arrow Functions

ES6中引入了一种编写函数的新语法

简单函数

1
2
3
4
5
6
7
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());

当你只需要一个只有一个参数的简单函数时,可以使用新标准中的箭头函数,它的语法非常简单:标识符 => 表达式。你无需输入 function 和 return,一些小括号、大括号以及分号也可以省略。

1
2
3
4
5
6
7
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);

命名函数定义

1
2
3
4
5
// ES5
const getExtensives = async({}, start, limit) => {};
// ES2015
async function getExtensives({}, start, limit) {};

this

箭头函数没有它自己的this值,箭头函数内的this值继承自外围作用域。