REST(Representational State Transfer)描述了一个架构样式的网络系统,它首次出如今 2000 年 Roy Fielding 的博士论文中。在REST服务中,应用程序状态和功能能够分为各类资源。资源向客户端公开,客户端能够对资源进行增删改操做。资源的例子有:应用程序对象、数据库记录、算法等等。node
REST经过抽象资源,提供了一个很是容易理解和使用的API,它使用 URI (Universal Resource Identifier) 惟一表示资源。REST接口使用标准的 HTTP 方法,好比 GET、PUT、POST 和 DELET在客户端和服务器之间传输状态。mysql
狭义的RESTful关注点在于资源,使用URL表示的资源及对资源的操做。Leonard Richardson 和 Sam Ruby 在他们的著做 RESTful Web Services 中引入了术语 REST-RPC 混合架构。REST-RPC 混合 Web 服务不使用信封包装方法、参数和数据,而是直接经过 HTTP 传输数据,这与 REST 样式的 Web 服务是相似的。可是它不使用标准的 HTTP 方法操做资源。git
和传统的RPC、SOA相比,RESTful的更为简单直接,且构建于标准的HTTP之上,使得它很是快速地流行起来。github
Node.js能够用不多代码简单地实现一个Web服务,而且它有一个很是活跃的社区,经过Node出色的包管理机制(NPM)能够很是容易得到各类扩展支持。web
对简单的应用场景Node.js实现REST是一个很是合适的选择(有很是多的理由选择这个或者那个技术栈,本文不会介入各类语言、架构的争论,咱们着眼点仅仅是简单)。算法
下面,就用一个App游戏排行榜后台服务来讲明一下如何用Node.js快速地开发一个的RESTful服务。sql
当App游戏玩家过关时,会提交游戏过关时间(秒)数值到REST服务器上,服务器记录并对过关记录进行排序,用户能够查看游戏TOP 10排行榜。mongodb
游戏应用提交的数据格式使用JSON表示,以下:数据库
{express
"id": "aaa",
"score": 9.8,
"token": "aaa-6F9619FF-8B86-D011-B42D-00C04FC964FF"
};
Id为用户输入的用户名,token用于区别不一样的用户,避免id重名,score为过关所耗费的时间(秒)。
可使用curl做为客户端测试RESTful服务。
提交游戏记录的命令以下:
curl -d "{\"cmd\":1,\"record\":{\"id\":\"test11\",\"score\":29.8,\"token\":\"aaa\"}}" http://localhost:3000/leaderboards
这个命令的语义不只仅是狭义的REST增删改,咱们为它添加一个cmd命令,实际上经过POST一个JSON命令,把这个服务实现为REST-RPC。
删除游戏记录的命令格式以下:
curl -X DELETE http://localhost:3000/leaderboards/aaa
或(使用REST-RPC语义)
curl -d "{\"cmd\":2,\"record\":{\"id\":\"test11\"}}" http://localhost:3000/leaderboards
查看TOP 10命令以下:
curl http://localhost:3000/leaderboards
标准REST定义中,POST和PUT有不一样含义,GET能够区分单个资源或者资源列表。对这个应用咱们作了简化,ADD和UPDATE都统一使用POST,对单个资源和列表也再也不区分,直接返回TOP 10数据。
安装Node.js
本文使用的版本是v5.5.0。
寻找一款方便的IDE
本文做者使用Sublime敲打代码,eclipse+nodeclipse生成框架代码和调试。
在Node中,实现一个HTTP服务器是很简单的事情。在项目根目录下建立一个叫app.js的文件,并写入如下代码:
var http = require("http");
http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}).listen(3000);
用Node.js执行你的脚本:
node server.js
打开浏览器访问http://localhost: 3000/,你就会看到一个写着“Hello World”的网页。
即便彻底不懂Node,也能够很是直观的看到这里经过require引入了一个http模块,而后使用createServer建立HTTP服务对象,当收到客户端发出的HTTP请求后,将调用咱们提供的函数,并在回调函数里写入返回的页面。
接下来,咱们将把这个简单的应用扩展为一个RESTful服务。
如今须要超越“hello world”,咱们将修改以前的http回调函数,根据请求类型返回不一样的内容。
代码以下:
var server = http.createServer(function(req, res) {
var result;
switch (req.method) {
case 'POST':
break;
case 'GET':
break;
case 'DELETE':
break;
}
});
经过req.method,能够获得请求的类型。
1. 增长和修改
其中POST请求,是要求咱们添加或更新记录,请求的数据能够经过回调获得。
代码以下:
var item = '';
req.setEncoding('utf8');
req.on('data', function(chunk) {
item += chunk;
});
req.on('end', function() {
try {
var command = JSON.parse(item);
console.log(commandNaNd+ ';'+ command.record.id+ ':'+ command.record.score+ '('+ command.record.token+ ')');
if (commandNaNd === CMD.UPDATE_SCORE){
addRecord(command.record,result);
}
else if (commandNaNd === CMD.DEL_USE){
db('leaderboards').remove({id:command.record.id});
}
res.end(JSON.stringify(result));
}
catch (err) {
result.comment= 'Can\'t accept post, Error: '+ err.message;
result.code= ErrCode.DataError;
console.log(result.comment);
res.end(JSON.stringify(result));
}
});
当框架解析读入数据时,会调用req.on('data', function(chunk)提供的回调函数,咱们把请求的数据记录在item中,一有数据,就调用item += chunk,直到数据读入完成,框架调用req.on('end', function()回调,在回调函数中,使用JSON.parse把请求的JSON数据还原为Javascript对象,这是一个命令对象,经过commandNaNd能够区分是须要添加或删除记录。
addRecord添加或更新记录。
代码以下:
function addRecord(record,result) {
var dbRecord = db('leaderboards').find({ id: record.id });
if (dbRecord){
if (dbRecord.token !== record.token){
result.code= ErrCode.DataError;
result.comment= 'User exist';
}
else{
db('leaderboards')
.chain()
.find({id:record.id})
.assign({score:record.score})
.value();
result.comment= 'OK, New Score is '+ record.score;
}
}
else{
db('leaderboards').push(record);
}
}
命令执行结束后,经过res.end(JSON.stringify(result))写入返回数据。返回数据一样是一个JSON字符串。
执行结果以下图:
在这个简单的样例中,使用了lowdb(https://github.com/typicode/lowdb#license?utm_source=ourjs.com)。
LowDB 是一个基于Node的纯Json文件数据库,它无需服务器,能够同步或异步持久化到文件中,也能够单纯做为内存数据库,很是快速简单。LowDB 提供Lo-Dash接口,可使用相似.find({id:record.id})风格的方法进行查询。经过chain(),能够把多个操做链接在一块儿,完成数据库的查找更新操做。
这个简单的数据库实现,若是游戏仅保存得分高的用户记录,实际上已经能够知足咱们的应用了。对更复杂的应用,Node也提供了各类数据库链接模块,比较常见的是mongodb或mysql。
2. 返回TOP 10
经过查询数据库里的数据,首先使用.sortBy('score'),取前10个,返回到记录集中,而后使用JSON.stringify转为字符串,经过res返回。
代码以下:
var records= [];
var topTen = db('leaderboards')
.chain()
.sortBy('score')
.take(10)
.map(function(record) {
records.push(record);
})
.value();
res.end(JSON.stringify(records));
执行结果以下图:
3. 删除记录
RESTful的删除资源ID通常带着URL里,相似“http://localhost:3000/leaderboards/aaa”,所以使用var path = parse(req.url).pathname解析出资源ID“aaa”。
代码以下:
case 'DELETE':
result= {code:ErrCode.OK,comment: 'OK'};
try {
var path = parse(req.url).pathname;
var arrPath = path.split("/");
var delObjID= arrPath[arrPath.length-1];
db('leaderboards').remove({id:delObjID});
res.end(JSON.stringify(result));
break;
}
执行结果以下图:
至此,咱们实现了一个带基本功能,可真正使用的RESTful服务。
实际应用场合的REST服务可能会更复杂一些,一个应用或者会提供多个资源URL的服务;或者还同时提供了基本的WEB服务功能;或者REST请求带有文件上传等等。
这样,咱们的简单实现就不够看了。
Express 是一个基于 Node.js 平台的 web 应用开发框架,它提供一系列强大的特性,帮助你建立各类 Web应用。
可使用eclipse+nodeclipse生成默认的express应用框架。一个express应用以下所示
var express = require('express')
, routes = require('./routes')
, user = require('./routes/user')
, http = require('http')
, path = require('path');
var app = express();
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// development only
if ('development' == app.get('env')) {
app.use(express.errorHandler());
}
app.get('/', routes.index);
app.get('/users', user.list);
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
Express是一个Web服务器实现框架,虽然咱们用不上页面和页面渲染,不过做为样例,仍是保留了缺省生成的页面,并对其进行简单解释。
在这个生成的框架代码里,选择view engine模板为ejs,这是一个相似JSP的HTML渲染模板引擎,app.get('/', routes.index)表示把HTTP的“/”请求路由给routes.index处理,routes.index对应于工程结构下的index.js文件处理,其内容以下:
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
这个函数调用了对应view目录下的index.ejs模板,并把{ title: 'Express' }传递给ejs模板,在ejs模板中,可使用<%= title %>获得传入的json对象。
首先咱们实现一个本身的服务类,在routes子目录中,建立leaderboards.js文件,这个文件结构大体为定义REST对应的操做函数。
exports.fnList = function(req, res){
};
exports.fnGet = function(req, res){
};
exports.fnDelete = function(req, res){
};
exports.fnUpdate = function(req, res){
};
exports.fnAdd = function(req, res){
};
在app.js文件中,须要把HTTP请求路由给对应函数。
var leaderboards = require('./routes/leaderboards');
…
app.get('/leaderboards', leaderboards.fnList);
app.get('/leaderboards/:id', leaderboards.fnGet);
app.delete('/leaderboards/:id', leaderboards.fnDelete);
app.post('/leaderboards', leaderboards.fnAdd);
app.put('/leaderboards/:id', leaderboards.fnUpdate);
这样就把标准Web服务请求路由到leaderboards处理。由于请求中带有POST数据,可使用
var bodyParser = require('body-parser');
// parse various different custom JSON types as JSON
app.use(bodyParser.json({ limit: '1mb',type: 'application/*' }));
把请求的JSON结构解析后添加到req.body中。Limit是为避免非法数据占用服务器资源,正常状况下,若是解析JSON数据,type应该定义为'application/*+json',在本应用里,为避免某些客户端请求不指明类型,把全部输入都解析为JSON数据了。
'body-parser'是一个颇有用的库,能够解析各类类型的HTTP请求数据,包括处理文件上传,详细能够参见https://www.npmjs.com/package/body-parser。
有了这个路由映射机制,咱们再也不须要考虑URL和数据的解析,仅仅指定路由,实现对应函数就能够了。
exports.fnList = function(req, res){
var result= {code:ErrCode.OK,comment: 'OK'};
try {
var records= [];
var topTen = db('leaderboards')
.chain()
.sortBy('score')
.take(10)
.map(function(record) {
records.push(record);
})
.value();
res.end(JSON.stringify(records));
}catch (err) {
result.comment= 'Can\'t get leaderboards, Error: '+ err.message;
result.code= ErrCode.DataError;
console.log(result.comment);
res.end(JSON.stringify(result));
}
return;
};
对相似http://localhost:3000/leaderboards/aaa的URL,express已经解析到req.param里了,能够经过req.param('id')获得。
exports.fnDelete = function(req, res){
var result= {code:ErrCode.OK,comment: 'OK'};
try {
var resID= req.param('id');
db('leaderboards').remove(resID);
res.end(JSON.stringify(result));
console.log('delete record:'+ req.param('id'));
}
catch (err) {
result.comment= 'Can\'t DELETE at '+ req.param('id')+ ', Error: '+ err.message;
result.code= ErrCode.DelError;
console.log(result.comment);
res.end(JSON.stringify(result));
}
};
使用了bodyParser.json()后,对POST请求中的JSON数据,已经解析好放到req.body里了,代码中能够直接使用。
function processCmd(req, res){
var result= {code:ErrCode.OK,comment: 'OK'};
try{
var command = req.body;
console.log(req.bodyNaNd+ ';'+ req.body.record.id+ ':'+ req.body.record.score+ '('+ req.body.record.token+ ')');
if (commandNaNd === CMD.UPDATE_SCORE){
addRecord(command.record,result);
console.log('add record:'+ command.record.id);
}
else if (commandNaNd === CMD.DEL_USE){
db('leaderboards').remove({id:command.record.id});
console.log('delete record:'+ command.record.id);
}
res.end(JSON.stringify(result));
}
catch (err) {
result.comment= 'Can\'t accept post, Error: '+ err.message;
result.code= ErrCode.DataError;
console.log(result.comment);
res.end(JSON.stringify(result));
}
return;
}
exports.fnUpdate = function(req, res){
processCmd(req,res);
};
exports.fnAdd = function(req, res){
processCmd(req,res);
};
使用express的好处是有一些细节能够扔给框架处理,代码结构上也更容易写得清晰一些。