Node.js项目实战-构建可扩展的Web应用(第一版):8 使用Express.js和Hapi构建Node.js REST API服务

   瘦客户端和瘦服务端的架构变得越来越流行。瘦客户端一般基于Backbone.js, Anglers JS, Ember.js等框架构建,而瘦服务端通常代表着REST风格的WebAPI服务。它有如下一些优点:

  • 只需要一套REST API接口,便可以同时为多种客户端提供服务。
  • 更换客户端不会影响到核心的业务逻辑,同样,更换服务端也不会。
  • 测试方便,UI本身是难以进行自动化测试的,尤其是使用了事件驱动的用户界面以及单页面应用等,同时跨浏览器情形更加大了测试的难度。但是,当我们把业务逻辑分开成不同的后端API之后,对逻辑部分的测试就变得十分容易了。

8.1 RESTful API基础

   在分布式系统中,每次请求都需要携带关于客户端的足够的信息。从某种意义上讲,RESTful是无状态的,因为没有任何关于客户端状态的信息被存储在服务器上,这样也就保证了每次请求都能够被任意服务器处理,而得到相同的结果。

   独立特性:

  • 可扩展性,因为不同的组件可以独立部署到不同的服务器上。
  • 使用简单的动作和内容替换SOAP协议
  • 使用不同的HTTP请求方式,如GET,POST,DELETE,PUT,OPTIONS等。
  • JSON并不是唯一可选的内容格式,XML, CSV等不同于SOAP,后者是一种协议,而RESTful作为一种设计风格,在内容格式的选择上更加灵活。

Node.js项目实战-构建可扩展的Web应用(第一版):8 使用Express.js和Hapi构建Node.js REST API服务_第1张图片

     PUT和DELETE请求是幕等的,当服务器收到多条相同的请求时,均能得到相同的结果。

     GET也是幕等的,但POST是非幕等的,所以重复请求可能会引发状态改变或其他未知异常。

     需要处理几种地址格式的请求:

  • POST  /collections/{collectionName}:请求创建一个对象,返回新建对象的ID
  • GET  /collections/{collectionName}/{id}:根据请求ID返回查询到的对象
  • GET  /collections/{collectionName}/:请求返回集合中的全部元素,在这个例子中,限制最多返回10个元素,并根据ID排序
  • PUT  /collections/{collectionName}/{id}:根据请求ID更新相应的对象
  • DELETE  /collections/{collectionName}/{id}:根据ID删除相应的对象

8.2 项目依赖

     $ mkdir   rest-express

     $ npm install mongoskin body-parser morgan  mocha superagent expect.js standard express  mongodb

8.3 使用Mocha和superagent进行测试

   借助Mocha和superagent库,发送HTTP请求到服务器执行基本的CURD操作

   创建test/index.js文件,包含6个测试用例:P180

  1. 创建一个新对象
  2. 通过对象ID检查对象
  3. 检索整个集合
  4. 通过对象ID更新对象
  5. 通过对象ID检查对象是否更新
  6. 通过对象ID删除对象
const boot = require('../index.js').boot
const shutdown = require('../index.js').shutdown
//const port = require('../index.js').port

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

const port = process.env.PORT || 3000

before(() => {
  boot()
})

describe('express rest api server', () => {
  let id

  it('post object', (done) => {
    superagent.post(`http://localhost:${port}/collections/test`)
      .send({ name: 'John',
        email: '[email protected]'
      })
      .end((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', (done) => {
    superagent.get(`http://localhost:${port}/collections/test/${id}`)
      .end((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', (done) => {
    superagent.get(`http://localhost:${port}/collections/test`)
      .end((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', (done) => {
    superagent.put(`http://localhost:${port}/collections/test/${id}`)
      .send({name: 'Peter',
        email: '[email protected]'})
      .end((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', (done) => {
    superagent.get(`http://localhost:${port}/collections/test/${id}`)
      .end((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', (done) => {
    superagent.del(`http://localhost:${port}/collections/test/${id}`)
      .end((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()
      })
  })
})

after(() => {
  shutdown()
})

8.4 使用Express和Mongoskin构建REST API服务器

     index.js作为程序的入口文件

const express = require('express')
const mongoskin = require('mongoskin')
const bodyParser = require('body-parser')
const logger = require('morgan')
const http = require('http')

const app = express()

app.set('port', process.env.PORT || 3000);

app.use(bodyParser.json());
app.use(logger());

const db = mongoskin.db('mongodb://@localhost:27017/test')
const id = mongoskin.helper.toObjectID

//作用是当URL中出现对应的参数时进行一些操作,以冒号开头的collectionName时,选择一个特定的集合
app.param('collectionName', (req, res, next, collectionName) => {
  req.collection = db.collection(collectionName)
  return next()
})

app.get('/', (req, res, next) => {
  res.send('Select a collection, e.g., /collections/messages')
})
//对列表按_id属性进行排序,并限制最多只返回10个元素
app.get('/collections/:collectionName', (req, res, next) => {
  req.collection.find({}, {limit: 10, sort: [['_id', -1]]})
    .toArray((e, results) => {
      if (e) return next(e)
      res.send(results)
    }
  )
})

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

app.get('/collections/:collectionName/:id', (req, res, next) => {
  req.collection.findOne({_id: id(req.params.id)}, (e, result) => {
    if (e) return next(e)
    res.send(result)
  })
})
//update方法返回的不是变更的对象,而是变更对象的计数
app.put('/collections/:collectionName/:id', (req, res, next) => {
  req.collection.update({_id: id(req.params.id)},
    {$set: req.body}, //是一种特殊的MongoDB操作,用来设置值
    {safe: true, multi: false}, (e, result) => {  //保存配置的对象,执行结束后才运行回调,并且只处理一条请求
      if (e) return next(e)
      res.send((result.result.n === 1) ? {msg: 'success'} : {msg: 'error'})
    })
})

app.delete('/collections/:collectionName/:id', (req, res, next) => {
  req.collection.remove({_id: id(req.params.id)}, (e, result) => {
    if (e) return next(e)
    // console.log(result)
    res.send((result.result.n === 1) ? {msg: 'success'} : {msg: 'error'})
  })
})

const server = http.createServer(app)
const boot = () => {
  server.listen(app.get('port'), () => {
    console.info(`Express server listening on port ${app.get('port')}`)
  })
}

const shutdown = () => {
  server.close(process.exit)
}

if (require.main === module) {
  boot()
} else {
  console.info('Running app as a module')
  exports.boot = boot
  exports.shutdown = shutdown
  exports.port = app.get('port')
}

 $ node index

 $ node test  -R nyan  测试报告

8.5 重构:基于Hapi.js的REST API服务器

    Hapi是一个企业级的框架。它比Express.js复杂,但功能更加丰富,更适合大团队开发使用。

    它的日志功能十分强大

     $ npm install good hapi mongoskin mocha superagent expect.js

     rest-hapi/hapi-app.js

const port = process.env.PORT || 3000
const Hapi = require('hapi')
server.connection({ port: port, host: 'localhost' })
const server = new Hapi.Server()

const mongoskin = require('mongoskin')

const db = mongoskin.db('mongodb://@localhost:27017/test', {})
const id = mongoskin.helper.toObjectID
//接收数据库名做参数,然后异步加载数据库
const loadCollection = (name, callback) => {
  callback(db.collection(name))
}
//路由数组
server.route([
  {
    method: 'GET',
    path: '/',
    handler: (req, reply) => {
      reply('Select a collection, e.g., /collections/messages')
    }
  },
  {
    method: 'GET',
    path: '/collections/{collectionName}',
    handler: (req, reply) => {
      loadCollection(req.params.collectionName, (collection) => {
        collection.find({}, {limit: 10, sort: [['_id', -1]]}).toArray((e, results) => {
          if (e) return reply(e)
          reply(results)
        })
      })
    }
  },
  {
    method: 'POST',
    path: '/collections/{collectionName}',
    handler: (req, reply) => {
      loadCollection(req.params.collectionName, (collection) => {
        collection.insert(req.payload, {}, (e, results) => {
          if (e) return reply(e)
          reply(results.ops)
        })
      })
    }
  },
  {
    method: 'GET',
    path: '/collections/{collectionName}/{id}',
    handler: (req, reply) => {
      loadCollection(req.params.collectionName, (collection) => {
        collection.findOne({_id: id(req.params.id)}, (e, result) => {
          if (e) return reply(e)
          reply(result)
        })
      })
    }
  },
  {
    method: 'PUT',
    path: '/collections/{collectionName}/{id}',
    handler: (req, reply) => {
      loadCollection(req.params.collectionName, (collection) => {
        collection.update({_id: id(req.params.id)},
          {$set: req.payload},
          {safe: true, multi: false}, (e, result) => {
            if (e) return reply(e)
            reply((result.result.n === 1) ? {msg: 'success'} : {msg: 'error'})
          })
      })
    }
  },
  {
    method: 'DELETE',
    path: '/collections/{collectionName}/{id}',
    handler: (req, reply) => {
      loadCollection(req.params.collectionName, (collection) => {
        collection.remove({_id: id(req.params.id)}, (e, result) => {
          if (e) return reply(e)
          reply((result.result.n === 1) ? {msg: 'success'} : {msg: 'error'})
        })
      })
    }
  }
])

const options = {
  subscribers: {
    'console': ['ops', 'request', 'log', 'error']
  }
}

server.register(require('good', options, (err) => {
  if (!err) {
    // Plugin loaded successfully
  }
}))

const boot = () => {
  server.start((err) => {
    if (err) {
      console.error(err)
      return process.exit(1)
    }
    console.log(`Server running at: ${server.info.uri}`)
  })
}

const shutdown = () => {
  server.stop({}, () => {
    process.exit(0)
  })
}

if (require.main === module) {
  console.info('Running app as a standalone')
  boot()
} else {
  console.info('Running app as a module')
  exports.boot = boot
  exports.shutdown = shutdown
  exports.port = port
}
$ node hapi-app


你可能感兴趣的:(后端)