KOA 与 REST API

REST API 规范

REST请求只是一种请求类型和响应类型均为JSON的HTTP请求。

编写REST API,实际上就是编写处理HTTP请求的async函数,不过,REST请求和普通的HTTP请求有几个特殊的地方:

REST请求仍然是标准的HTTP请求,但是,除了GET请求外,POST、PUT等请求的body是JSON数据格式,请求的Content-Type为application/json;
REST响应返回的结果是JSON数据格式,因此,响应的Content-Type也是application/json。
REST规范定义了资源的通用访问格式,虽然它不是一个强制要求,但遵守该规范可以让人易于理解。

例如,商品Product就是一种资源:

  • GET /api/products 获取所有Product;

  • GET /api/products/123 而获取某个指定的Product ,指定id为123;

  • POST /api/products 新建一个Product,JSON数据包含在body中;

  • PUT /api/products/123 更新一个Product,更新id为123;

  • DELETE /api/products/123 删除一个Product使用DELETE请求,删除id为123;

  • GET /api/products/123/reviews 资源还可以按层次组织。如,获取某个Product的所有评论;

  • GET /api/products/123/reviews?page=2&size=10&sort=time 也可通过参数限制返回的结果集。如,返回第2页评论,每页10项,按时间排序。

REST API 编写

使用REST虽然非常简单,但是,设计一套合理的REST框架却需要仔细考虑很多问题。

问题一:如何组织URL

在实际工程中,一个Web应用既有REST,还有MVC,可能还需要集成其他第三方系统。如何组织URL?
一个简单的方法是通过固定的前缀区分。例如,/static/ 开头的URL是静态资源文件,类似的,/api/ 开头的URL就是REST API,其他URL是普通的MVC请求。

问题二:如何统一输出REST
  • REST API的返回值全部是object对象
    为方便客户端处理结果,只要是请求成功,不管是查询错误还是查询异常,都返回JSON数据,而不是简单的 number、boolean、null或者数组;
  • REST API必须使用前缀/api/
    如果需要对请求做某些统一处理,就可以通过 /api/ 来判断当前请求是否是一个 REST 请求。
问题三:如何处理错误

这个问题实际上有两部分。

HTTP请求可能发生的错误
当REST API请求出错时,像403,404,500等错误,我们如何返回错误信息?
针对这种类型的错误,第一类的错误实际上客户端可以识别,并且我们也无法操控HTTP服务器的错误码。

业务逻辑的错误
当客户端收到 REST 响应后,如何判断是成功还是错误?
例如,输入了不合法的Email地址,试图删除一个不存在的Product,等等。这种类型的错误完全可以通过JSON返回给客户端,这样,客户端可以根据错误信息提示用户“Email不合法”等,以便用户修复后重新请求API。例如:

{
    "code": "0",
    "message": "Bad email address"
}

REST架构本身同样没有标准的错误码定义一说,因此,有的Web应用使用数字1000、1001……作为错误码。

不推荐混合其他HTTP错误码。例如,使用401响应“登录失败”,使用403响应“权限不够”。这会使客户端无法有效识别HTTP错误码和业务错误,其原因在于HTTP协议定义的错误码十分偏向底层,而REST API属于“高层”协议,不应该复用底层的错误码。

koa 处理 REST

使用koa作为Web框架处理HTTP请求,因此,我们可以在koa中响应并处理REST请求。

我来看下koa一个简单的路由请求案例:

const Koa = require('koa')
const router = require('koa-router')() // 处理路由映射
const bodyParser = require('koa-bodyparser') // 处理提交请求
const app = new Koa()

// add bodyparser
app.use(bodyParser())

// add url-route:
router.get('/', async (ctx, next) => {
  ctx.response.body = '<h1>Hello, koa2!!!</h1>'
});

router.get('/hello/:name', async (ctx, next) => {
  var name = ctx.params.name;
  ctx.response.body = `<h1>Hello, ${name}!</h1>`
});

router.get('/login', async (ctx, next) => {
  ctx.response.body = `
      <h1>Index</h1>
      <form action="/signin" method="post">
          <p>Name: <input name="name" value="koa"></p>
          <p>Password: <input name="password" type="password"></p>
          <p><input type="submit" value="Submit"></p>
      </form>`
});

router.post('/signin', async (ctx, next) => {
  const { name, password } = ctx.request.body
  if (name === 'koa' && password === '123456') {
    const result = {
      message: 'login success',
      username: name
    }
    ctx.response.body = result
  } else {
    ctx.response.body = `<h1>Login failed!</h1>`
  }
})

// add router middleware:
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

上面代码中有两个关键的 middleware:koa-routerkoa-bodyparser

koa-router

为处理URL,我们需要引入koa-router这个middleware,让它负责处理URL映射,根据不同的URL调用不同的处理函数。

安装、引入。

然后就使用 router.get('/path', async fn) 来注册一个GET请求。如果API路径带有参数,参数必须用 : 表示,参数通过 ctx.params.来访问。
例如上面例子中的 /hello/:name,参数通过 ctx.params.name 访问。
客户端传递的URL可能就是 /hello/007,那么参数 name 对应的值就是 007,用 ctx.params.name 来获取。

类似的,如果API路径有多个参数,例如,/api/products/:pid/reviews/:rid,则这两个参数分别用
ctx.params.pidctx.params.rid 获取。

这个功能由 koa-router 这个middleware提供。

注意:API路径的参数永远是字符串!

koa-bodyparser

如果要处理post请求,可以用 router.post('/path', async fn)
用post请求处理URL时,我们会遇到一个问题:post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!

所以,我们需要引入另一个middleware来解析原始request请求,然后,把解析后的参数,绑定到 ctx.request.body 中。
koa-bodyparser就是用来干这个活的。

注意,由于middleware的顺序很重要,这个 koa-bodyparser 必须在router之前被注册到app对象上。如果 ctx.request.body 为 undefined,说明缺少middleware,或者middleware没有正确配置。

koa-bodyparser 给 koa 安装了一个解析HTTP请求body的处理函数。
如果客户端传递了JSON格式的数据(例如,POST、PUT等请求),就可以通过 ctx.request.body 直接访问已经反序列化的 JavaScript 对象。
如果要返回JSON格式的数据到客户端,只需要给 ctx.response.body 赋值一个JavaScript对象,koa会自动把该对象序列化为JSON并输出到客户端。

响应 response.type

在 koa内部,koa 会根据 ctx.response.body 响应体容格式的不同设置默认的Content-Type。
将响应体设置为以下之一 并 赋 Content-Type 默认值:

  • string 写入
    Content-Type 默认为 text/html 或 text/plain, 同时默认字符集是 utf-8。Content-Length 字段也是如此。

  • Buffer 写入
    Content-Type 默认为 application/octet-stream, 并且 Content-Length 字段也是如此。

  • Stream 管道
    Content-Type 默认为 application/octet-stream

  • Object || Array JSON 字符串化
    Content-Type 默认为 application/json. 这包括普通的对象 { foo: 'bar' } 和数组 ['foo', 'bar']

  • null 无内容响应
    Content-Type ,无

我们也可以通过 response.type 自行设置 Content-Type:
【 获取】
获取响应 Content-Type, 获取到的值不含 "charset" 等参数:
const ct = ctx.type // => "image/png"
【 设置】
设置响应 Content-Type :
ctx.type = 'text/plain; charset=utf-8';
ctx.type = 'image/png';
ctx.type = '.png';
ctx.type = 'png';

如果不设置字符串charset,将默认是 "utf-8"。
如果你想覆盖 charset, 使用 ctx.set('Content-Type', 'text/html') 将响应头字段设置为直接值。

例如上面例中的get请求:

router.get('/', async (ctx, next) => {
  ctx.response.type = 'text/html'; // 'text/html; charset=utf-8'

  /*或*/
  ctx.set('Content-Type', 'text/html') //  'text/html'

  ctx.response.body = '<h1>Hello, koa2!!!</h1>'
});
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容