Skip to content

Latest commit

 

History

History
663 lines (479 loc) · 25.2 KB

File metadata and controls

663 lines (479 loc) · 25.2 KB

二十一、REST API

在本教程中,我们将构建一个 RESTful API 。除了 Express.js,我们还将通过 Mongoskin 库使用 MongoDB。我们还将使用 Mocha 和 SuperAgent 来编写功能测试。

本教程将指导您使用 Mocha 和 SuperAgent 库编写测试,然后向您展示如何以测试驱动的开发方式使用测试,利用 Express.js 框架和 MongoDB 的 Mongoskin 库构建 Node.js free-JSON REST API 服务器。

Image 注意为了方便起见,测试和应用文件的完整源代码都在https://github.com/azat-co/rest-api-express中。如果您想跳过教程,只运行代码,可以使用:

$ git clone https://github.com/azat-co/rest-api-express.git
$ cd rest-api-express
$ npm install
$ node express.jsIn a new terminal window, enter:$ ./node_modules/mocha/bin/mocha express.test.jsThe source code might be an enhanced version of the code in this chapter because of the ongoing contributions from readers. I encourage you to submit your own pull request!

在这个 REST API 服务器中,我们将执行创建、更新、移除和删除(CRUD)操作,并用app.param()app.use()方法利用 Express.js 中间件 1 概念。本章分为以下几个主题:

  • RESTful API 基础知识:RESTful API 初级读本
  • 测试覆盖范围:我们将使用测试驱动开发(TDD)方法,首先编写测试
  • 服务器依赖关系:我们将安装所需的模块
  • 服务器实现:我们将为 Express.js 应用编写代码

Image 注意在本章中,我们的 REST API 和测试示例使用了无分号的风格。JavaScript 中的分号绝对是可选的,除了两种情况:1)在 for 循环中,2)在以括号开头的表达式/语句之前(例如,立即调用的函数表达式或 IIFE)。使用这种风格给你一个不同的视角。键入更少的分号可以提高速度,而且看起来更好,更一致,因为开发人员往往会时不时地错过分号(完美运行的代码允许这样的草率)。此外,一些程序员发现不带分号的代码可读性更好。

RESTful API 基础

RESTful APIs 之所以流行,是因为分布式系统中的每个事务都需要包含足够的客户端状态信息。从某种意义上说,这个标准是无状态的,因为服务器上没有存储任何关于客户机状态的信息,这使得不同的系统为每个请求提供服务成为可能。

RESTful API 的独特特征(即,如果一个 API 是 RESTful 的,它通常遵循这些原则)如下:

  • 它具有更好的可伸缩性支持,因为不同的组件可以独立部署到不同的服务器上。
  • 它取代了简单对象访问协议(SOAP ),因为 REST 中的动词和名词结构更简单。
  • 它使用 HTTP 方法,比如 GET、POST、DELETE、PUT、OPTIONS 等等。
  • 它支持 JSON 以外的格式(尽管 JSON 是最流行的)。与 SOAP(一种协议)不同,REST 方法在选择格式方面非常灵活。例如,替代格式可能是可扩展标记语言(XML)或逗号分隔值(CSV)格式。

表 21-1 概述了一个用于消息收集的简单 CRUD REST API 的例子。

表 21-1 。CRUD REST API 结构示例

|

方法

|

统一资源定位器

|

意义

| | --- | --- | --- | | 得到 | /messages.json | 以 JSON 格式返回消息列表。 | | 放 | /messages.json | 更新/替换所有消息,并在 JSON 中返回状态/错误。 | | 邮政 | /messages.json | 创建新消息并以 JSON 格式返回其 ID。 | | 得到 | /messages/{id}.json | 以 JSON 格式返回 ID 为{id}的消息。 | | 放 | /messages/{id}.json | 更新/替换 ID 等于{id}值的消息;如果{id}消息不存在,则创建它。 | | 删除 | /messages/{id}.json | 删除 ID 为{id}的消息,并以 JSON 格式返回状态/错误。 |

休息不是一个协议;它是一种架构,从某种意义上说,它比协议(如 SOAP)更灵活。因此,如果我们想支持这些格式,REST API URLs 可能看起来像/messages/list.html/messages/list.xml

PUT 和 DELETE 是幂等方法,这意味着,如果服务器收到两个或更多类似的请求,最终结果是相同的。POST 不是等幂的,可能会影响状态并导致副作用(例如,创建多个重复记录)。GET 是无效,这意味着多次调用它是安全的,因为结果不会改变。

Image 你可以在维基百科(http://en.wikipedia.org/wiki/Representational_state_transfer)和 Stefan Tilkov 的 InfoQ 文章《REST 简介》(www.infoq.com/articles/rest-introduction)中找到更多关于 REST 的信息。

正如“简介”一章中提到的,在我们的 REST API 服务器中,我们将执行 CRUD 操作,并通过app.param()app.use()方法利用 Express.js 中间件概念。因此,我们的应用应该能够使用 JSON 格式处理以下命令(collectionName是集合的名称,通常是复数名词,例如,消息、评论、用户等)。):

  • POST /collections/{collectionName}:请求创建一个对象;应用使用新创建的对象 ID 进行响应。
  • GET /collections/{collectionName}/{id}:用 URL 中的 ID 值请求;应用检索具有该 ID 的对象。
  • GET /collections/{collectionName}/:请求从集合中检索任意项目(items);在我们的例子中,我们有以下查询选项:最多 10 个条目,按 ID 排序。
  • PUT /collections/{collectionName}/{id}:用 ID 请求更新一个对象。
  • 删除/collections/{collectionName}/{id}:ID 为的请求删除一个对象。

因此,这个服务器可以处理任何数量的集合,而不仅仅是单个集合,只需要六个端点(例如messages,如表 21-1 所示)。

测试覆盖率

在我们做任何其他事情之前,让我们编写向我们即将创建的 REST API 服务器发出 HTTP 请求的功能测试。如果您知道如何使用 Mocha,或者只是想直接跳到 Express.js 应用实现,请随意。您也可以使用 CURL 终端命令进行测试。

假设我们已经安装了 Node.js、NPM 和 MongoDB,让我们创建一个新的文件夹(或者如果您编写了测试,使用那个文件夹):

$ mkdir rest-api-express
$ cd rest-api-express

我们将使用 Mocha、Expect.js ( https://github.com/Automattic/expect.js)和 SuperAgent ( http://visionmedia.github.io/superagent/)库。要安装它们,从项目文件夹运行这些命令:

$ npm install -g mocha@1.18.2 --save-dev
$ npm install expect.js@0.3.1 --save-dev
$ npm install superagent@0.17.0 --save-dev

Image 提示您可以在全球范围内安装 Mocha,因为它是一个命令行工具,但是在本地安装 Mocha 将使您能够同时使用不同版本的 Mocha——一个项目一个版本。要用本地摩卡运行测试,只需指向./node_modules/mocha/bin/mocha。你可以把它复制到 Makefile 中,如第 22 章中的所述,或者复制到package.json"scripts": {"test": "..."}中。持续集成(CI)的配置也需要本地 Mocha。

现在让我们创建一个express.test.js文件,它在同一个文件夹中有六个测试套件:

  • 创建新对象
  • 按 ID 检索对象
  • 检索整个收藏
  • 按 ID 更新对象
  • 通过 ID 检查更新的对象
  • 按 ID 删除对象

通过 SuperAgent 的链式函数,HTTP 请求变得轻而易举,我们将把这些函数放在每个测试套件中。

所以,我们从依赖关系开始:

var superagent = require('superagent')
var expect = require('expect.js')

接下来,我们编写包装在测试用例中的第一个测试用例(describe及其回调)。这个想法很简单。我们向服务器的本地实例发出一个 HTTP 请求。当我们发送请求时,我们传递一些数据,当然,还有 URL 路径,它随着测试用例的不同而变化。主要操作发生在请求(由 SuperAgent 发出)回调中。在那里,我们放置了多个断言,这是 TDD 的主要部分。严格来说,这个测试套件使用了行为驱动开发(BDD)语言,但是这个差异对于我们的项目来说并不重要。T3】

describe('express rest api server', function(){
  var id

  it('posts an object', function(done){
    superagent.post('http://localhost:3000/collections/test')
      .send({ name: 'John'
        , email: 'john@rpjs.co'
      })
      .end(function(e,res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.eql(1)
        expect(res.body[0]._id.length).to.eql(24)
        id = res.body[0]._id
        done()
      })
  })

您可能已经注意到,我们正在检查以下内容:

  • 错误对象应该为空(eql(null))。
  • 响应体数组应该有一项(to.eql(1))。
  • 第一个响应主体项应该具有_id属性,该属性的长度为 24 个字符(即标准 MongoDB ObjectId类型的十六进制字符串表示)。

最后,我们将新创建的对象 ID 保存在id全局变量中,这样我们可以在以后使用它进行检索、更新和删除。说到对象检索,我们将在下一个测试用例中测试它们。注意,superagent方法已经变成了get(),URL 路径包含了对象 ID。您可以“取消注释”console.log来检查完整的 HTTP 响应体:

it('retrieves an object', function(done){
  superagent.get('http://localhost:3000/collections/test/'+id)
    .end(function(e, res){
      // console.log(res.body)
      expect(e).to.eql(null)
      expect(typeof res.body).to.eql('object')
      expect(res.body._id.length).to.eql(24)
      expect(res.body._id).to.eql(id)
      done()
    })
})

done() 回调允许我们测试异步代码。如果没有它,Mocha 测试用例会突然结束,远远早于缓慢的服务器有时间响应。

下一个测试用例的断言更有趣一些,因为我们对响应结果使用了map()函数来返回一个 id 数组。在这个数组中,我们用contain()方法找到我们的 ID(保存在id变量中),这是一个比原生indexOf()更优雅的替代方法。它之所以有效,是因为结果(限于 10 条记录)是按 id 排序的,还因为我们的对象是刚刚创建的。

it('retrieves a collection', function(done){
  superagent.get('http://localhost:3000/collections/test')
    .end(function(e, res){
      // console.log(res.body)
      expect(e).to.eql(null)
      expect(res.body.length).to.be.above(0)
      expect(res.body.map(function (item){return item._id})).to.contain(id)
      done()
    })
})

当需要更新对象时,我们实际上需要发送一些数据。我们通过将对象传递给 SuperAgent 的函数来实现这一点。然后,我们断言该操作在(msg=success)完成:

it('updates an object', function(done){
    superagent.put('http://localhost:3000/collections/test/'+id)
      .send({name: 'Peter'
        , email: 'peter@yahoo.com'})
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')
        done()
      })
  })

最后两个测试用例断言检索更新的对象及其删除,使用的方法与我们以前使用的方法类似。下面是rest-api-express/express.test.js文件的完整源代码:

var superagent = require('superagent')
var expect = require('expect.js')

describe('express rest api server', function(){
  var id

  it('posts an object', function(done){
    superagent.post('http://localhost:3000/collections/test')
      .send({ name: 'John'
        , email: 'john@rpjs.co'
      })
      .end(function(e,res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.eql(1)
        expect(res.body[0]._id.length).to.eql(24)
        id = res.body[0]._id
        done()
      })
  })

  it('retrieves an object', function(done){
    superagent.get('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body._id.length).to.eql(24)
        expect(res.body._id).to.eql(id)
        done()
      })
  })

  it('retrieves a collection', function(done){
    superagent.get('http://localhost:3000/collections/test')
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(res.body.length).to.be.above(0)
        expect(res.body.map(function (item){return item._id})).to.contain(id)
        done()
      })
  })

  it('updates an object', function(done){
    superagent.put('http://localhost:3000/collections/test/'+id)
      .send({name: 'Peter'
        , email: 'peter@yahoo.com'})
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')
        done()
      })
  })

  it('checks an updated object', function(done){
    superagent.get('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body._id.length).to.eql(24)
        expect(res.body._id).to.eql(id)
        expect(res.body.name).to.eql('Peter')
        done()
      })
  })

  it('removes an object', function(done){
    superagent.del('http://localhost:3000/collections/test/'+id)
      .end(function(e, res){
        // console.log(res.body)
        expect(e).to.eql(null)
        expect(typeof res.body).to.eql('object')
        expect(res.body.msg).to.eql('success')
        done()
      })
  })
})

为了运行测试,我们可以使用$ mocha express.test.js命令。现在,测试应该会失败,因为我们还没有实现服务器!

对于那些需要多个版本的 Mocha 的人来说,另一个更好的选择是使用本地 Mocha 二进制文件运行测试:

$ ./node_modules/mocha/bin/mocha express.test.js

当然,这是假设您已经在本地将 Mocha 安装到了node_modules中。

Image 注意默认情况下,Mocha 不使用任何记者,结果输出乏善可陈。要接收更多的解释性日志,请提供-R <name>选项(例如$ mocha test -R spec$ mocha test -R list)。

属国

和上一篇教程一样(第 20 章),我们将使用 Mongoskin ,一个 MongoDB 库,它是 Node.js. 欲了解更多信息,请查看https://github.com/kissjs/node-mongoskin#comparation

Express.js 是核心 Node.js HTTP 模块对象(http://nodejs.org/api/http.html)的包装器。Express.js 框架构建在 Connect 中间件(https://github.com/senchalabs/connect)之上,提供了极大的便利。有些人把这个框架比作 Ruby 的 Sinatra,因为它是非自以为是和可配置的。

如果您在上一节中创建了一个rest-api-express文件夹,只需运行这些命令来为应用安装模块:

$ npm install express@4.8.1 --save
$ npm install mongoskin@1.4.1 --save

最终的package.json文件可能如下所示:

{
  "name": "rest-api-express",
  "version": "0.0.4",
  "description": "",
  "main": "express.js",
  "scripts": {
    "start": "node express.js",
    "test": "mocha express.test.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/azat-co/rest-api-express.git"
  },
  "author": "Azat Mardan",
  "license": "BSD-2-Clause",
  "bugs": {
    "url": "https://github.com/azat-co/rest-api-express/issues"
  },
  "dependencies": {
    "body-parser": "1.9.2",
    "express": "4.10.1",
    "mongoskin": "1.4.4",
    "morgan": "1.5.0" },
  "devDependencies": {
    "expect.js": "0.3.1",
    "mocha": "2.0.1",
    "superagent": "0.20.0" }
}

服务器实现

要实现服务器,我们首先需要定义我们的依赖关系:

var express = require('express'),
  mongoskin = require('mongoskin'),
  bodyParser = require('body-parser')
  logger = require('morgan')

在 3.x 版之后,Express.js 简化了其应用实例的实例化,因此这一行为我们提供了一个服务器对象:

var app = express()

为了从请求体中提取参数,我们将使用body-parser中间件。(如何使用中间件在第 4 章的中讨论过。)以下是 JSON 和 URL 编码函数的语句:

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true}))

morgan ( logger)中间件允许我们查看传入的请求:

app.use(logger('dev'))

中间件(在此为 3 等形式 4 )是 Express.js 和 Connect 中一种强大便捷的模式,用于组织和重用代码。

与节省我们编写额外代码(用于解析 HTTP 请求的主体对象)的bodyParser()方法一样, Mongoskin 使连接到 MongoDB 数据库成为可能,与原生 MongoDB 驱动程序代码相比,只需一行代码:

var db = mongoskin.db('@localhost:27017/test', {safe:true});

Image 注意如果您希望连接到一个远程数据库,比如 MongoHQ ( https://www.mongohq.com/home),用您的用户名、密码、主机和端口值替换该字符串。以下是 URI 字符串的格式:

mongodb://[username:password@] host1[:port1][,host2[:port2],... [,hostN[:portN]]] [/[database][?options]]

方法是另一个 Express.js 中间件。它基本上是说“每当请求处理程序的 URL 模式中有这个值时,就做一些事情。”在我们的例子中,当一个请求模式包含一个以冒号为前缀的字符串collectionName(您将在后面的 routes 中看到它):时,我们选择一个特定的集合

app.param('collectionName', function(req, res, next, collectionName){
  req.collection = db.collection(collectionName)
  return next()
})

为了方便用户,让我们在根路由中包含一条消息:

app.get('/', function(req, res, next) {
  res.send('please select a collection, e.g., /collections/messages')
})

现在真正的工作开始了。下面是我们检索一个条目列表的方法,这个列表按照_id (sort: {'_id':-1})排序,并且限制为十个(limit: 10):

app.get('/collections/:collectionName', function(req, res, next) {
  req.collection.find({},{
    limit: 10, sort: {'_id': -1}
  }).toArray(function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

您是否注意到 URL 模式参数中有一个:collectionName字符串?这个中间件和之前的app.param()中间件为我们提供了指向数据库中指定集合的req.collection对象。

创建对象的端点稍微容易理解,因为我们只是将整个有效负载传递给 MongoDB 方法(也称为 free-JSON REST API):

app.post('/collections/:collectionName', function(req, res, next) {
  req.collection.insert(req.body, {}, function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

单一对象检索函数(例如,findById())比find()更快,但是它们使用不同的接口。它们直接返回一个对象,而不是一个光标——请注意!ID 来自 URL 路径的:id部分,带有req.params.id Express.js magic:

app.get('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.findById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send(result)
  })
})

PUT 请求处理程序变得更加有趣,因为updateById() (as update())不返回增强的对象;相反,它返回受影响对象的计数。

另外,{$set: req.body}是一个特殊的 MongoDB 操作符(操作符往往以美元符号开始),它设置值。在这种情况下,我们更新发送给我们的任何机体数据。这被称为 free-JSON API 方法。这对于原型开发来说很棒,但是在大多数系统中,你需要执行验证(你可以使用express-validator中间件,在第 15 章中有所介绍)。

第二个{safe: true, multi: false}参数是一个带有选项的对象,告诉 MongoDB 在运行回调函数之前等待执行,并且只处理一个(第一个)项目:

app.put('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.updateById(req.params.id,
    {$set: req.body},
    {safe: true, multi: false},
    function(e, result){
        if (e) return next(e)
        res.send((result === 1) ? {msg: 'success'} : {msg: 'error'})
    }
  )
})

最后,下面是删除方法 ,它利用了 Mongoskin 的removeById()方法,在成功的情况下输出一个自定义的 JSON 消息({msg: success}):

app.delete('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.removeById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send((result === 1)?{msg: 'success'} : {msg: 'error'})
  })
})

Image 注意app.delete()方法是现已废弃的(但仍在旧项目中使用)app.del()的别名。

在本例中,在端口 3000 上实际启动服务器的最后一行是:

app.listen(3000, function(){
  console.log('Express server listening on port 3000')
})

以防万一,这里有rest-api-express/express.js文件的完整代码:

var express = require('express'),
  mongoskin = require('mongoskin'),
  bodyParser = require('body-parser'),
  logger = require('morgan')

var app = express()
app.use(bodyParser())
app.use(logger('dev'))

var db = mongoskin.db('mongodb://@localhost:27017/test', {safe:true})

app.param('collectionName', function(req, res, next, collectionName){
  req.collection = db.collection(collectionName)
  return next()
})

app.get('/', function(req, res, next) {
  res.send('please select a collection, e.g., /collections/messages')
})

app.get('/collections/:collectionName', function(req, res, next) {
  req.collection.find({} ,{limit: 10, sort: {'_id': -1}}).toArray(function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

app.post('/collections/:collectionName', function(req, res, next) {
  req.collection.insert(req.body, {}, function(e, results){
    if (e) return next(e)
    res.send(results)
  })
})

app.get('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.findById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send(result)
  })
})

app.put('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.updateById(req.params.id, {$set: req.body}, {safe: true, multi: false}, function(e, result){
    if (e) return next(e)
    res.send((result === 1) ? {msg:'success'} : {msg: 'error'})
  })
})

app.delete('/collections/:collectionName/:id', function(req, res, next) {
  req.collection.removeById(req.params.id, function(e, result){
    if (e) return next(e)
    res.send((result === 1)?{msg: 'success'} : {msg: 'error'})
  })
})

app.listen(3000, function(){
  console.log('Express server listening on port 3000')
})

退出编辑器,在终端中运行以下命令:

$ node express.js

在另一个窗口中(不关闭第一个窗口,让服务器运行),输入:

$ mocha express.test.js

或者

$ ./node_modules/mocha/bin/mocha express.test.js

或者

$ npm test

Mocha 的终端输出应该如下所示:

......
  6 passing (57ms)

在服务器终端窗口中,您应该会看到如下内容:

Express server listening on port 3000
POST /collections/test 200 35.242 ms - 73
GET /collections/test/54724135101acb1334635994 200 4.254 ms - 71
GET /collections/test 200 5.181 ms - 108
PUT /collections/test/54724135101acb1334635994 200 4.037 ms - 17
GET /collections/test/54724135101acb1334635994 200 1.638 ms - 75
DELETE /collections/test/54724135101acb1334635994 200 1.382 ms - 17

如果你真的不喜欢摩卡和/或 BDD,你可以一直用 CURL。例如,下面是如何发出帖子请求:

$ curl -d "name=peter&email=peter337@rpjs.co" http://localhost:3000/collections/proexpressjs-readers
$ curl http://localhost:3000/collections/proexpressjs-readers

在这种情况下,输出是:

[{"name":"peter","email":"peter337@rpjs.co","_id":"541714c23f5b557785700d4c"}]%
...
[{"_id":"541714c23f5b557785700d4c","name":"peter","email":"peter337@rpjs.co"}]%

GET 请求也适用于浏览器。例如,您可以前往http://localhost:3000/collections/proexpressjs-readers获取收藏中的项目列表。

在本教程中,我们的测试比应用代码本身还要长,所以放弃测试驱动开发可能很有诱惑力,但是请相信我,在任何严肃的开发项目中,当您正在开发的应用非常复杂时,TDD 的好习惯将会节省您的工作时间

摘要

当您需要用几行代码构建一个简单的 REST API 服务器时,Express.js 和 Mongoskin 库是很好的资源。稍后,如果您需要扩展这些库,它们还提供了一种配置和组织代码的方法。像 MongoDB 这样的 NoSQL 数据库擅长处理 free-REST API,这意味着你不必定义模式,你可以向它抛出任何数据,它就会被保存。

在下一章中,我们将把 REST API 方法与前端框架 Backbone.js 结合起来,它将从服务器获取数据,编译数据,并在浏览器中呈现 HTML(不像第 20 章中的 Todo 应用,它在服务器上处理模板)。


1T0】

2T0】

3T0】

4T0】