瘦客户端和瘦服务端的架构变得越来越流行。瘦客户端一般基于Backbone.js, Anglers JS, Ember.js等框架构建,而瘦服务端通常代表着REST风格的WebAPI服务。它有如下一些优点:
在分布式系统中,每次请求都需要携带关于客户端的足够的信息。从某种意义上讲,RESTful是无状态的,因为没有任何关于客户端状态的信息被存储在服务器上,这样也就保证了每次请求都能够被任意服务器处理,而得到相同的结果。
独立特性:
PUT和DELETE请求是幕等的,当服务器收到多条相同的请求时,均能得到相同的结果。
GET也是幕等的,但POST是非幕等的,所以重复请求可能会引发状态改变或其他未知异常。
需要处理几种地址格式的请求:
$ mkdir rest-express
$ npm install mongoskin body-parser morgan mocha superagent expect.js standard express mongodb
借助Mocha和superagent库,发送HTTP请求到服务器执行基本的CURD操作
创建test/index.js文件,包含6个测试用例:P180
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()
})
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 test -R nyan 测试报告
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