使用Node.js實(shí)現(xiàn)RESTful API的示例
RESTful基礎(chǔ)概念
REST(Representational State Transfer)描述了一個(gè)架構(gòu)樣式的網(wǎng)絡(luò)系統(tǒng),它首次出現(xiàn)在 2000 年 Roy Fielding 的博士論文中。在REST服務(wù)中,應(yīng)用程序狀態(tài)和功能可以分為各種資源。資源向客戶端公開,客戶端可以對(duì)資源進(jìn)行增刪改操作。資源的例子有:應(yīng)用程序?qū)ο?、?shù)據(jù)庫(kù)記錄、算法等等。
REST通過(guò)抽象資源,提供了一個(gè)非常容易理解和使用的API,它使用 URI (Universal Resource Identifier) 唯一表示資源。REST接口使用標(biāo)準(zhǔn)的 HTTP 方法,比如 GET、PUT、POST 和 DELET在客戶端和服務(wù)器之間傳輸狀態(tài)。
狹義的RESTful關(guān)注點(diǎn)在于資源,使用URL表示的資源及對(duì)資源的操作。Leonard Richardson 和 Sam Ruby 在他們的著作 RESTful Web Services 中引入了術(shù)語(yǔ) REST-RPC 混合架構(gòu)。REST-RPC 混合 Web 服務(wù)不使用信封包裝方法、參數(shù)和數(shù)據(jù),而是直接通過(guò) HTTP 傳輸數(shù)據(jù),這與 REST 樣式的 Web 服務(wù)是類似的。但是它不使用標(biāo)準(zhǔn)的 HTTP 方法操作資源。
和傳統(tǒng)的RPC、SOA相比,RESTful的更為簡(jiǎn)單直接,且構(gòu)建于標(biāo)準(zhǔn)的HTTP之上,使得它非常快速地流行起來(lái)。
Node.js可以用很少代碼簡(jiǎn)單地實(shí)現(xiàn)一個(gè)Web服務(wù),并且它有一個(gè)非?;钴S的社區(qū),通過(guò)Node出色的包管理機(jī)制(NPM)可以非常容易獲得各種擴(kuò)展支持。
對(duì)簡(jiǎn)單的應(yīng)用場(chǎng)景Node.js實(shí)現(xiàn)REST是一個(gè)非常合適的選擇(有非常多的理由選擇這個(gè)或者那個(gè)技術(shù)棧,本文不會(huì)介入各種語(yǔ)言、架構(gòu)的爭(zhēng)論,我們著眼點(diǎn)僅僅是簡(jiǎn)單)。
應(yīng)用樣例場(chǎng)景
下面,就用一個(gè)App游戲排行榜后臺(tái)服務(wù)來(lái)說(shuō)明一下如何用Node.js快速地開發(fā)一個(gè)的RESTful服務(wù)。
當(dāng)App游戲玩家過(guò)關(guān)時(shí),會(huì)提交游戲過(guò)關(guān)時(shí)間(秒)數(shù)值到REST服務(wù)器上,服務(wù)器記錄并對(duì)過(guò)關(guān)記錄進(jìn)行排序,用戶可以查看游戲TOP 10排行榜。
游戲應(yīng)用提交的數(shù)據(jù)格式使用JSON表示,如下:
{
"id": "aaa",
"score": 9.8,
"token": "aaa-6F9619FF-8B86-D011-B42D-00C04FC964FF"
};
Id為用戶輸入的用戶名,token用于區(qū)別不同的用戶,避免id重名,score為過(guò)關(guān)所耗費(fèi)的時(shí)間(秒)。
可以使用curl作為客戶端測(cè)試RESTful服務(wù)。
提交游戲記錄的命令如下:
curl -d "{\"cmd\":1,\"record\":{\"id\":\"test11\",\"score\":29.8,\"token\":\"aaa\"}}" http://localhost:3000/leaderboards
這個(gè)命令的語(yǔ)義不僅僅是狹義的REST增刪改,我們?yōu)樗砑右粋€(gè)cmd命令,實(shí)際上通過(guò)POST一個(gè)JSON命令,把這個(gè)服務(wù)實(shí)現(xiàn)為REST-RPC。
刪除游戲記錄的命令格式如下:
curl -X DELETE http://localhost:3000/leaderboards/aaa
或(使用REST-RPC語(yǔ)義)
curl -d "{\"cmd\":2,\"record\":{\"id\":\"test11\"}}" http://localhost:3000/leaderboards
查看TOP 10命令如下:
curl http://localhost:3000/leaderboards
標(biāo)準(zhǔn)REST定義中,POST和PUT有不同含義,GET可以區(qū)分單個(gè)資源或者資源列表。對(duì)這個(gè)應(yīng)用我們做了簡(jiǎn)化,ADD和UPDATE都統(tǒng)一使用POST,對(duì)單個(gè)資源和列表也不再區(qū)分,直接返回TOP 10數(shù)據(jù)。
一些準(zhǔn)備工作
安裝Node.js
本文使用的版本是v5.5.0。
尋找一款方便的IDE
本文作者使用Sublime敲打代碼,eclipse+nodeclipse生成框架代碼和調(diào)試。
Node.js中基礎(chǔ)的HTTP服務(wù)器
在Node中,實(shí)現(xiàn)一個(gè)HTTP服務(wù)器是很簡(jiǎn)單的事情。在項(xiàng)目根目錄下創(chuàng)建一個(gè)叫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
打開瀏覽器訪問(wèn)http://localhost: 3000/,你就會(huì)看到一個(gè)寫著“Hello World”的網(wǎng)頁(yè)。
即使完全不懂Node,也可以非常直觀的看到這里通過(guò)require引入了一個(gè)http模塊,然后使用createServer創(chuàng)建HTTP服務(wù)對(duì)象,當(dāng)收到客戶端發(fā)出的HTTP請(qǐng)求后,將調(diào)用我們提供的函數(shù),并在回調(diào)函數(shù)里寫入返回的頁(yè)面。
接下來(lái),我們將把這個(gè)簡(jiǎn)單的應(yīng)用擴(kuò)展為一個(gè)RESTful服務(wù)。
簡(jiǎn)單直觀的RESTful服務(wù)
現(xiàn)在需要超越“hello world”,我們將修改之前的http回調(diào)函數(shù),根據(jù)請(qǐng)求類型返回不同的內(nèi)容。
代碼如下:
var server = http.createServer(function(req, res) {
var result;
switch (req.method) {
case 'POST':
break;
case 'GET':
break;
case 'DELETE':
break;
}
});
通過(guò)req.method,可以得到請(qǐng)求的類型。
1. 增加和修改
其中POST請(qǐng)求,是要求我們添加或更新記錄,請(qǐng)求的數(shù)據(jù)可以通過(guò)回調(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));
}
});
當(dāng)框架解析讀入數(shù)據(jù)時(shí),會(huì)調(diào)用req.on('data', function(chunk)提供的回調(diào)函數(shù),我們把請(qǐng)求的數(shù)據(jù)記錄在item中,一有數(shù)據(jù),就調(diào)用item += chunk,直到數(shù)據(jù)讀入完成,框架調(diào)用req.on('end', function()回調(diào),在回調(diào)函數(shù)中,使用JSON.parse把請(qǐng)求的JSON數(shù)據(jù)還原為Javascript對(duì)象,這是一個(gè)命令對(duì)象,通過(guò)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í)行結(jié)束后,通過(guò)res.end(JSON.stringify(result))寫入返回?cái)?shù)據(jù)。返回?cái)?shù)據(jù)同樣是一個(gè)JSON字符串。
在這個(gè)簡(jiǎn)單的樣例中,使用了lowdb(https://github.com/typicode/lowdb#license?utm_source=ourjs.com)。
LowDB 是一個(gè)基于Node的純Json文件數(shù)據(jù)庫(kù),它無(wú)需服務(wù)器,可以同步或異步持久化到文件中,也可以單純作為內(nèi)存數(shù)據(jù)庫(kù),非??焖俸?jiǎn)單。LowDB 提供Lo-Dash接口,可以使用類似.find({id:record.id})風(fēng)格的方法進(jìn)行查詢。通過(guò)chain(),可以把多個(gè)操作連接在一起,完成數(shù)據(jù)庫(kù)的查找更新操作。
這個(gè)簡(jiǎn)單的數(shù)據(jù)庫(kù)實(shí)現(xiàn),如果游戲僅保存得分高的用戶記錄,實(shí)際上已經(jīng)可以滿足我們的應(yīng)用了。對(duì)更復(fù)雜的應(yīng)用,Node也提供了各種數(shù)據(jù)庫(kù)連接模塊,比較常見(jiàn)的是mongodb或mysql。
2. 返回TOP 10
通過(guò)查詢數(shù)據(jù)庫(kù)里的數(shù)據(jù),首先使用.sortBy('score'),取前10個(gè),返回到記錄集中,然后使用JSON.stringify轉(zhuǎn)為字符串,通過(guò)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;
}
至此,我們實(shí)現(xiàn)了一個(gè)帶基本功能,可真正使用的RESTful服務(wù)。
實(shí)際應(yīng)用場(chǎng)合的REST服務(wù)可能會(huì)更復(fù)雜一些,一個(gè)應(yīng)用或者會(huì)提供多個(gè)資源URL的服務(wù);或者還同時(shí)提供了基本的WEB服務(wù)功能;或者REST請(qǐng)求帶有文件上傳等等。
這樣,我們的簡(jiǎn)單實(shí)現(xiàn)就不夠看了。
Express框架
Express 是一個(gè)基于 Node.js 平臺(tái)的 web 應(yīng)用開發(fā)框架,它提供一系列強(qiáng)大的特性,幫助你創(chuàng)建各種 Web應(yīng)用。
可以使用eclipse+nodeclipse生成默認(rèn)的express應(yīng)用框架。一個(gè)express應(yīng)用如下所示
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是一個(gè)Web服務(wù)器實(shí)現(xiàn)框架,雖然我們用不上頁(yè)面和頁(yè)面渲染,不過(guò)作為樣例,還是保留了缺省生成的頁(yè)面,并對(duì)其進(jìn)行簡(jiǎn)單解釋。
在這個(gè)生成的框架代碼里,選擇view engine模板為ejs,這是一個(gè)類似JSP的HTML渲染模板引擎,app.get('/', routes.index)表示把HTTP的“/”請(qǐng)求路由給routes.index處理,routes.index對(duì)應(yīng)于工程結(jié)構(gòu)下的index.js文件處理,其內(nèi)容如下:
exports.index = function(req, res){
res.render('index', { title: 'Express' });
};
這個(gè)函數(shù)調(diào)用了對(duì)應(yīng)view目錄下的index.ejs模板,并把{ title: 'Express' }傳遞給ejs模板,在ejs模板中,可以使用<%= title %>得到傳入的json對(duì)象。
Express框架實(shí)現(xiàn)RESTful服務(wù)
首先我們實(shí)現(xiàn)一個(gè)自己的服務(wù)類,在routes子目錄中,創(chuàng)建leaderboards.js文件,這個(gè)文件結(jié)構(gòu)大致為定義REST對(duì)應(yīng)的操作函數(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請(qǐng)求路由給對(duì)應(yīng)函數(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);
這樣就把標(biāo)準(zhǔn)Web服務(wù)請(qǐng)求路由到leaderboards處理。因?yàn)檎?qǐng)求中帶有POST數(shù)據(jù),可以使用
var bodyParser = require('body-parser');
// parse various different custom JSON types as JSON
app.use(bodyParser.json({ limit: '1mb',type: 'application/*' }));
把請(qǐng)求的JSON結(jié)構(gòu)解析后添加到req.body中。Limit是為避免非法數(shù)據(jù)占用服務(wù)器資源,正常情況下,如果解析JSON數(shù)據(jù),type應(yīng)該定義為'application/*+json',在本應(yīng)用里,為避免某些客戶端請(qǐng)求不指明類型,把所有輸入都解析為JSON數(shù)據(jù)了。
'body-parser'是一個(gè)很有用的庫(kù),可以解析各種類型的HTTP請(qǐng)求數(shù)據(jù),包括處理文件上傳,詳細(xì)可以參見(jiàn)https://www.npmjs.com/package/body-parser。
有了這個(gè)路由映射機(jī)制,我們不再需要考慮URL和數(shù)據(jù)的解析,僅僅指定路由,實(shí)現(xiàn)對(duì)應(yīng)函數(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;
};
對(duì)類似http://localhost:3000/leaderboards/aaa的URL,express已經(jīng)解析到req.param里了,可以通過(guò)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()后,對(duì)POST請(qǐng)求中的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的好處是有一些細(xì)節(jié)可以扔給框架處理,代碼結(jié)構(gòu)上也更容易寫得清晰一些。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解使用 Node.js 開發(fā)簡(jiǎn)單的腳手架工具
這篇文章主要介紹了詳解使用 Node.js 開發(fā)簡(jiǎn)單的腳手架工具,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-06-06
寶塔部署nodejs項(xiàng)目的實(shí)戰(zhàn)步驟
前段時(shí)間部署node項(xiàng)目的時(shí)候出現(xiàn)了一點(diǎn)問(wèn)題,所以想著給大家總結(jié)下,這篇文章主要給大家介紹了關(guān)于寶塔部署nodejs項(xiàng)目的實(shí)戰(zhàn)步驟,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-12-12
node.js中的fs.realpath方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.realpath方法使用說(shuō)明,本文介紹了fs.realpath的方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
node.js中的fs.createWriteStream方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.createWriteStream方法使用說(shuō)明,本文介紹了fs.createWriteStream方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
NVM管理Node.js實(shí)現(xiàn)不同版本Angular環(huán)境切換
Node Version Manager(NVM)是一個(gè)用于管理多個(gè)Node.js版本的工具,它允許用戶在同一臺(tái)機(jī)器上安裝和使用多個(gè)Node.js版本,本文將給大家介紹NVM管理Node.js實(shí)現(xiàn)不同版本Angular環(huán)境切換的流程步驟,需要的朋友可以參考下2024-05-05
NodeJS創(chuàng)建最簡(jiǎn)單的HTTP服務(wù)器
這篇文章主要介紹了NodeJS創(chuàng)建最簡(jiǎn)單的HTTP服務(wù)器的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05
npm安裝淘寶鏡像報(bào)錯(cuò)問(wèn)題解決(npm install -g cnpm)
本文主要介紹了npm安裝淘寶鏡像報(bào)錯(cuò)問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01
Node.js連接數(shù)據(jù)庫(kù)實(shí)現(xiàn)過(guò)程詳解
這篇文章主要為大家介紹了Node.js連接數(shù)據(jù)庫(kù)實(shí)現(xiàn)過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

