使用Node.js实现RESTful API的示例


Posted in Javascript onAugust 01, 2017

RESTful基础概念

REST(Representational State Transfer)描述了一个架构样式的网络系统,它首次出现在 2000 年 Roy Fielding 的博士论文中。在REST服务中,应用程序状态和功能可以分为各种资源。资源向客户端公开,客户端可以对资源进行增删改操作。资源的例子有:应用程序对象、数据库记录、算法等等。

REST通过抽象资源,提供了一个非常容易理解和使用的API,它使用 URI (Universal Resource Identifier) 唯一表示资源。REST接口使用标准的 HTTP 方法,比如 GET、PUT、POST 和 DELET在客户端和服务器之间传输状态。

狭义的RESTful关注点在于资源,使用URL表示的资源及对资源的操作。Leonard Richardson 和 Sam Ruby 在他们的著作 RESTful Web Services 中引入了术语 REST-RPC 混合架构。REST-RPC 混合 Web 服务不使用信封包装方法、参数和数据,而是直接通过 HTTP 传输数据,这与 REST 样式的 Web 服务是类似的。但是它不使用标准的 HTTP 方法操作资源。

和传统的RPC、SOA相比,RESTful的更为简单直接,且构建于标准的HTTP之上,使得它非常快速地流行起来。

Node.js可以用很少代码简单地实现一个Web服务,并且它有一个非常活跃的社区,通过Node出色的包管理机制(NPM)可以非常容易获得各种扩展支持。

对简单的应用场景Node.js实现REST是一个非常合适的选择(有非常多的理由选择这个或者那个技术栈,本文不会介入各种语言、架构的争论,我们着眼点仅仅是简单)。

应用样例场景

下面,就用一个App游戏排行榜后台服务来说明一下如何用Node.js快速地开发一个的RESTful服务。 

当App游戏玩家过关时,会提交游戏过关时间(秒)数值到REST服务器上,服务器记录并对过关记录进行排序,用户可以查看游戏TOP 10排行榜。 

游戏应用提交的数据格式使用JSON表示,如下:

{

  "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.js中基础的HTTP服务器

在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服务。

 简单直观的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框架

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对象。 

Express框架实现RESTful服务

首先我们实现一个自己的服务类,在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的好处是有一些细节可以扔给框架处理,代码结构上也更容易写得清晰一些。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
通过AJAX的JS、JQuery两种方式解析XML示例介绍
Sep 23 Javascript
jQuery子属性过滤选择器用法分析
Feb 10 Javascript
JS拖拽组件学习使用
Jan 19 Javascript
jquery分页插件jquery.pagination.js实现无刷新分页
Apr 01 Javascript
浅谈String.valueOf()方法的使用
Jun 06 Javascript
JS原型链怎么理解
Jun 27 Javascript
基于BootStrap栅格栏系统完成网站底部版权信息区
Dec 23 Javascript
BootStrapValidator初使用教程详解
Feb 10 Javascript
使用vue框架 Ajax获取数据列表并用BootStrap显示出来
Apr 24 Javascript
vue中,在本地缓存中读写数据的方法
Sep 21 Javascript
layui中select,radio设置不生效的解决方法
Sep 05 Javascript
vue项目使用$router.go(-1)返回时刷新原来的界面操作
Jul 26 Javascript
关于jquery form表单序列化的注意事项详解
Aug 01 #jQuery
js下拉菜单生成器dropMenu使用方法详解
Aug 01 #Javascript
简述jQuery Easyui一些用法
Aug 01 #jQuery
js图片上传的封装代码
Aug 01 #Javascript
使用重写url机制实现验证码换一张功能
Aug 01 #Javascript
js实现拖拽上传图片功能
Aug 01 #Javascript
Jquery中.bind()、.live()、.delegate()和.on()之间的区别详解
Aug 01 #jQuery
You might like
咖啡常见的种类
2021/03/03 新手入门
php编写的一个E-mail验证类
2015/03/25 PHP
PHP中SERIALIZE和JSON的序列化与反序列化操作区别分析
2016/10/11 PHP
php通过header发送自定义数据方法
2018/01/18 PHP
jquery中获得$.ajax()事件返回的值并添加事件的方法
2010/04/15 Javascript
一个简单的网站访问JS计数器 刷新1次加1次访问
2012/09/20 Javascript
使用Jquery实现点击文字后变成文本框且可修改
2013/09/21 Javascript
js实现瀑布流的一种简单方法实例分享
2013/11/04 Javascript
js拖拽一些常见的思路方法整理
2014/03/19 Javascript
jquery的each方法使用示例分享
2014/03/25 Javascript
js从Cookies里面取值的简单实现
2014/06/30 Javascript
JavaScript中的Promise使用详解
2015/06/24 Javascript
解决jQuery使用JSONP时产生的错误
2015/12/02 Javascript
React.js入门学习第一篇
2016/03/30 Javascript
jQuery实现滚动条滚动到子元素位置(方便定位)
2017/01/08 Javascript
微信小程序教程系列之视图层的条件渲染(10)
2017/04/19 Javascript
Node.js中的require.resolve方法使用简介
2017/04/23 Javascript
Nodejs--post的公式详解
2017/04/29 NodeJs
vue mint-ui学习笔记之picker的使用
2017/10/11 Javascript
Element Card 卡片的具体使用
2020/07/26 Javascript
Python实现的多线程端口扫描工具分享
2015/01/21 Python
python模拟鼠标拖动操作的方法
2015/03/11 Python
python+pandas生成指定日期和重采样的方法
2018/04/11 Python
python使用udp实现聊天器功能
2018/12/10 Python
Virtualenv 搭建 Py项目运行环境的教程详解
2020/06/22 Python
selenium判断元素是否存在的两种方法小结
2020/12/07 Python
Fairyseason:为个人和批发商提供女装和配件
2017/03/01 全球购物
阿迪达斯墨西哥官方网站:adidas墨西哥
2017/11/03 全球购物
名人珠宝设计师:Melinda Maria Jewelry
2019/03/06 全球购物
英国玛莎百货新西兰:Marks & Spencer New Zealand
2019/07/21 全球购物
艺术系应届生的自我评价
2013/10/19 职场文书
大学生的网络创业计划书
2013/12/26 职场文书
超强台风观后感
2015/06/09 职场文书
创业不要错过,这4种餐饮新模式
2019/07/18 职场文书
「我的青春恋爱物语果然有问题。-妄言录-」第20卷封面公开
2022/03/21 日漫
nginx lua 操作 mysql
2022/05/15 Servers