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之后,对逻辑部分的测试就变得十分容易了。
独立特性:
可扩展性,因为不同的组件可以独立部署到不同的服务器上。
使用简单的动作和内容替换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删除相应的对象
$ npm install mongoskin body-parser morgan mocha superagent expect.js standard express mongodb
创建test/index.js文件,包含6个测试用例:P180
创建一个新对象
通过对象ID检查对象
检索整个集合
通过对象ID更新对象
通过对象ID检查对象是否更新
通过对象ID删除对象
$ node index
$ node test -R nyan 测试报告
它的日志功能十分强大
$ npm install good hapi mongoskin mocha superagent expect.js
rest-hapi/hapi-app.js
$ node hapi-app
只需要一套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
相关文章推荐
- 第8章-使用Express.js和Hapi构建Node.js-REST-API服务-8.2.项目依赖
- Node.js项目实战-构建可扩展的Web应用(第一版): 2 使用Express.js 4创建Web应用程序
- Node.js项目实战-构建可扩展的Web应用(第一版): 7 使用ORM类库Mongoose提升你的Node.js数据
- 第8章-使用Express.js和Hapi构建Node.js-REST-API服务-8.5.小结
- 第8章-使用Express.js和Hapi构建Node.js-REST-API服务-8.1.REST(表述性状态传递)Representational State Transfer
- Node.js项目实战-构建可扩展的Web应用(第一版): 6 在Node.js应用中使用session和OAuth进行用户认证和授权
- 第8章-使用Express.js和Hapi构建Node.js-REST-API服务-8.4.重构:使用Hapi搭建REST API服务器
- 第8章-使用Express.js和Hapi构建Node.js-REST-API服务-8.3.使用Express和Mongoskin实现REST API服务器
- Node.js项目实战-构建可扩展的Web应用(第一版):3 Node.js基于Mocha的测试驱动开发和行为驱动开发
- Node.js项目实战-构建可扩展的Web应用(第一版): 1 安装Node.js及相关要点
- Node.js项目实战-构建可扩展的Web应用(第一版): 5 MongoDB、Mongoskin特性
- Node.js项目实战-构建可扩展的Web应用(第一版): 4 模板引擎:Jade和Handlebars
- Node.js项目实战-构建可扩展的Web应用(第一版):9 WebSocket,Socket.IO和DerbyJS的实时应用程序
- Node.js项目实战-构建可扩展的Web应用(第一版):11 部署Node.js应用
- Node.js项目实战-构建可扩展的Web应用(第一版):10 为Node.js应用上线做准备
- 使用Node.js和MongoDB通过Mongoshin和Express.js构建JSON REST API服务器
- 使用express.js框架一步步实现基本应用以及构建可扩展的web应用
- 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?
- 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?
- 从零开始-使用React+Webpack+Nodejs+Express快速构建项目