您的位置:首页 > Web前端 > Node.js

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

2018-01-11 20:55 846 查看
   瘦客户端和瘦服务端的架构变得越来越流行。瘦客户端一般基于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作为一种设计风格,在内容格式的选择上更加灵活。



     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

创建一个新对象
通过对象ID检查对象
检索整个集合
通过对象ID更新对象
通过对象ID检查对象是否更新
通过对象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: 'john@rpjs.co'
})
.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: 'peter@yahoo.com'})
.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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐