您好,登錄后才能下訂單哦!
RESTful基礎概念
REST(Representational State Transfer)描述了一個架構樣式的網(wǎng)絡系統(tǒng),它首次出現(xiàn)在 2000 年 Roy Fielding 的博士論文中。在REST服務中,應用程序狀態(tài)和功能可以分為各種資源。資源向客戶端公開,客戶端可以對資源進行增刪改操作。資源的例子有:應用程序?qū)ο?、?shù)據(jù)庫記錄、算法等等。
REST通過抽象資源,提供了一個非常容易理解和使用的API,它使用 URI (Universal Resource Identifier) 唯一表示資源。REST接口使用標準的 HTTP 方法,比如 GET、PUT、POST 和 DELET在客戶端和服務器之間傳輸狀態(tài)。
狹義的RESTful關注點在于資源,使用URL表示的資源及對資源的操作。Leonard Richardson 和 Sam Ruby 在他們的著作 RESTful Web Services 中引入了術語 REST-RPC 混合架構。REST-RPC 混合 Web 服務不使用信封包裝方法、參數(shù)和數(shù)據(jù),而是直接通過 HTTP 傳輸數(shù)據(jù),這與 REST 樣式的 Web 服務是類似的。但是它不使用標準的 HTTP 方法操作資源。
和傳統(tǒng)的RPC、SOA相比,RESTful的更為簡單直接,且構建于標準的HTTP之上,使得它非??焖俚亓餍衅饋怼?/p>
Node.js可以用很少代碼簡單地實現(xiàn)一個Web服務,并且它有一個非?;钴S的社區(qū),通過Node出色的包管理機制(NPM)可以非常容易獲得各種擴展支持。
對簡單的應用場景Node.js實現(xiàn)REST是一個非常合適的選擇(有非常多的理由選擇這個或者那個技術棧,本文不會介入各種語言、架構的爭論,我們著眼點僅僅是簡單)。
應用樣例場景
下面,就用一個App游戲排行榜后臺服務來說明一下如何用Node.js快速地開發(fā)一個的RESTful服務。
當App游戲玩家過關時,會提交游戲過關時間(秒)數(shù)值到REST服務器上,服務器記錄并對過關記錄進行排序,用戶可以查看游戲TOP 10排行榜。
游戲應用提交的數(shù)據(jù)格式使用JSON表示,如下:
{ "id": "aaa", "score": 9.8, "token": "aaa-6F9619FF-8B86-D011-B42D-00C04FC964FF" };
Id為用戶輸入的用戶名,token用于區(qū)別不同的用戶,避免id重名,score為過關所耗費的時間(秒)。
可以使用curl作為客戶端測試RESTful服務。
提交游戲記錄的命令如下:
這個命令的語義不僅僅是狹義的REST增刪改,我們?yōu)樗砑右粋€cmd命令,實際上通過POST一個JSON命令,把這個服務實現(xiàn)為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可以區(qū)分單個資源或者資源列表。對這個應用我們做了簡化,ADD和UPDATE都統(tǒng)一使用POST,對單個資源和列表也不再區(qū)分,直接返回TOP 10數(shù)據(jù)。
一些準備工作
安裝Node.js
本文使用的版本是v5.5.0。
尋找一款方便的IDE
本文作者使用Sublime敲打代碼,eclipse+nodeclipse生成框架代碼和調(diào)試。
Node.js中基礎的HTTP服務器
在Node中,實現(xiàn)一個HTTP服務器是很簡單的事情。在項目根目錄下創(chuàng)建一個叫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執(zhí)行你的腳本:node server.js
打開瀏覽器訪問http://localhost: 3000/,你就會看到一個寫著“Hello World”的網(wǎng)頁。
即使完全不懂Node,也可以非常直觀的看到這里通過require引入了一個http模塊,然后使用createServer創(chuàng)建HTTP服務對象,當收到客戶端發(fā)出的HTTP請求后,將調(diào)用我們提供的函數(shù),并在回調(diào)函數(shù)里寫入返回的頁面。
接下來,我們將把這個簡單的應用擴展為一個RESTful服務。
簡單直觀的RESTful服務
現(xiàn)在需要超越“hello world”,我們將修改之前的http回調(diào)函數(shù),根據(jù)請求類型返回不同的內(nèi)容。
代碼如下:
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請求,是要求我們添加或更新記錄,請求的數(shù)據(jù)可以通過回調(diào)得到。
代碼如下:
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)); } });
當框架解析讀入數(shù)據(jù)時,會調(diào)用req.on('data', function(chunk)提供的回調(diào)函數(shù),我們把請求的數(shù)據(jù)記錄在item中,一有數(shù)據(jù),就調(diào)用item += chunk,直到數(shù)據(jù)讀入完成,框架調(diào)用req.on('end', function()回調(diào),在回調(diào)函數(shù)中,使用JSON.parse把請求的JSON數(shù)據(jù)還原為Javascript對象,這是一個命令對象,通過commandNaNd可以區(qū)分是需要添加或刪除記錄。
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); } }
命令執(zhí)行結束后,通過res.end(JSON.stringify(result))寫入返回數(shù)據(jù)。返回數(shù)據(jù)同樣是一個JSON字符串。
在這個簡單的樣例中,使用了lowdb(https://github.com/typicode/lowdb#license?utm_source=ourjs.com)。
LowDB 是一個基于Node的純Json文件數(shù)據(jù)庫,它無需服務器,可以同步或異步持久化到文件中,也可以單純作為內(nèi)存數(shù)據(jù)庫,非??焖俸唵?。LowDB 提供Lo-Dash接口,可以使用類似.find({id:record.id})風格的方法進行查詢。通過chain(),可以把多個操作連接在一起,完成數(shù)據(jù)庫的查找更新操作。
這個簡單的數(shù)據(jù)庫實現(xiàn),如果游戲僅保存得分高的用戶記錄,實際上已經(jīng)可以滿足我們的應用了。對更復雜的應用,Node也提供了各種數(shù)據(jù)庫連接模塊,比較常見的是mongodb或mysql。
2. 返回TOP 10
通過查詢數(shù)據(jù)庫里的數(shù)據(jù),首先使用.sortBy('score'),取前10個,返回到記錄集中,然后使用JSON.stringify轉(zhuǎn)為字符串,通過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; }
至此,我們實現(xiàn)了一個帶基本功能,可真正使用的RESTful服務。
實際應用場合的REST服務可能會更復雜一些,一個應用或者會提供多個資源URL的服務;或者還同時提供了基本的WEB服務功能;或者REST請求帶有文件上傳等等。
這樣,我們的簡單實現(xiàn)就不夠看了。
Express框架
Express 是一個基于 Node.js 平臺的 web 應用開發(fā)框架,它提供一系列強大的特性,幫助你創(chuàng)建各種 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服務器實現(xiàn)框架,雖然我們用不上頁面和頁面渲染,不過作為樣例,還是保留了缺省生成的頁面,并對其進行簡單解釋。
在這個生成的框架代碼里,選擇view engine模板為ejs,這是一個類似JSP的HTML渲染模板引擎,app.get('/', routes.index)表示把HTTP的“/”請求路由給routes.index處理,routes.index對應于工程結構下的index.js文件處理,其內(nèi)容如下:
exports.index = function(req, res){ res.render('index', { title: 'Express' }); };
這個函數(shù)調(diào)用了對應view目錄下的index.ejs模板,并把{ title: 'Express' }傳遞給ejs模板,在ejs模板中,可以使用<%= title %>得到傳入的json對象。
Express框架實現(xiàn)RESTful服務
首先我們實現(xiàn)一個自己的服務類,在routes子目錄中,創(chuàng)建leaderboards.js文件,這個文件結構大致為定義REST對應的操作函數(shù)。
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請求路由給對應函數(shù)。
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數(shù)據(jù),可以使用
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是為避免非法數(shù)據(jù)占用服務器資源,正常情況下,如果解析JSON數(shù)據(jù),type應該定義為'application/*+json',在本應用里,為避免某些客戶端請求不指明類型,把所有輸入都解析為JSON數(shù)據(jù)了。
'body-parser'是一個很有用的庫,可以解析各種類型的HTTP請求數(shù)據(jù),包括處理文件上傳,詳細可以參見https://www.npmjs.com/package/body-parser。
有了這個路由映射機制,我們不再需要考慮URL和數(shù)據(jù)的解析,僅僅指定路由,實現(xiàn)對應函數(shù)就可以了。
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已經(jīng)解析到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數(shù)據(jù),已經(jīng)解析好放到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的好處是有一些細節(jié)可以扔給框架處理,代碼結構上也更容易寫得清晰一些。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。