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的好處是有一些細節能夠扔給框架處理,代碼結構上也更容易寫得清晰一些。