使用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 相关文章推荐
javascript 兼容鼠标滚轮事件
Apr 07 Javascript
js中的scroll和offset 使用比较的实例与分析
Sep 29 Javascript
jquery如何扑捉回车键触发的事件
Apr 24 Javascript
Javscript调用iframe框架页面中函数的方法
Nov 01 Javascript
js的toUpperCase方法用法实例
Jan 27 Javascript
js完整倒计时代码分享
Sep 18 Javascript
VueJS如何引入css或者less文件的一些坑
Apr 25 Javascript
浅析webpack 如何优雅的使用tree-shaking(摇树优化)
Aug 16 Javascript
解决angularjs前后端分离调用接口传递中文时中文乱码的问题
Aug 13 Javascript
vue获取时间戳转换为日期格式代码实例
Apr 17 Javascript
javascript面向对象创建对象的方式小结
Jul 29 Javascript
整理 node-sass 安装失败的原因及解决办法(小结)
Feb 19 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
基于PHP字符串的比较函数strcmp()与strcasecmp()的使用详解
2013/05/15 PHP
七种PHP开发环境搭建工具
2020/06/28 PHP
js checkbox(复选框) 使用集锦
2009/04/28 Javascript
js控制淡入淡出示例代码
2013/11/12 Javascript
js 中将多个逗号替换为一个逗号的代码
2014/06/07 Javascript
10条建议帮助你创建更好的jQuery插件
2015/05/18 Javascript
js过滤HTML标签完整实例
2015/11/26 Javascript
WordPress 单页面上一页下一页的实现方法【附代码】
2016/03/10 Javascript
表单中单选框添加选项和移除选项
2016/07/04 Javascript
jQuery实现的超链接提示效果示例【附demo源码下载】
2016/09/09 Javascript
js改变html的原有内容实现方法
2016/10/05 Javascript
jquery使用EasyUI Tree异步加载JSON数据(生成树)
2017/02/11 Javascript
BootStrap的双日历时间控件使用
2017/07/25 Javascript
浅谈JS函数节流防抖
2017/10/18 Javascript
详解angularjs 学习之 scope作用域
2018/01/15 Javascript
jQuery niceScroll滚动条错位问题的解决方法
2018/02/03 jQuery
jQuery阻止事件冒泡实例分析
2018/07/03 jQuery
Vue.js 中的 v-cloak 指令及使用详解
2018/11/19 Javascript
layui动态渲染生成select的option值方法
2019/09/23 Javascript
[54:57]DOTA2-DPC中国联赛定级赛 Aster vs DLG BO3第二场 1月8日
2021/03/11 DOTA
web.py获取上传文件名的正确方法
2014/08/26 Python
Python中input和raw_input的一点区别
2014/10/21 Python
详解Python 数据库 (sqlite3)应用
2016/12/07 Python
python使用Plotly绘图工具绘制气泡图
2019/04/01 Python
python隐藏类中属性的3种实现方法
2019/12/19 Python
Python如何实现在字符串里嵌入双引号或者单引号
2020/03/02 Python
基于python纯函数实现井字棋游戏
2020/05/27 Python
什么是虚拟内存?虚拟内存有什么优势?
2012/02/19 面试题
物业门卫岗位职责
2013/12/28 职场文书
建材投资建议书
2014/05/16 职场文书
经济贸易专业自荐信
2014/06/11 职场文书
2014年收银工作总结
2014/11/13 职场文书
2015年少先队活动总结
2015/03/25 职场文书
公司员工体检通知
2015/04/21 职场文书
2015年度个人教学工作总结
2015/05/20 职场文书
java如何实现获取客户端ip地址的示例代码
2022/04/07 Java/Android