使用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 相关文章推荐
详解new function(){}和function(){}() 区别分析
Mar 22 Javascript
再论Javascript的类继承
Mar 05 Javascript
Javascript 异步加载详解(浏览器在javascript的加载方式)
May 20 Javascript
js 获取范围内的随机数实例代码
Aug 02 Javascript
js 中文汉字转Unicode、Unicode转中文汉字、ASCII转换Unicode、Unicode转换ASCII、中文转换
Dec 06 Javascript
实现JavaScript高性能的数据存储
Dec 11 Javascript
鼠标经过出现气泡框的简单实例
Mar 17 Javascript
ES6 javascript中class静态方法、属性与实例属性用法示例
Oct 30 Javascript
angularJS开发注意事项
May 26 Javascript
js使用ajax传值给后台,后台返回字符串处理方法
Aug 08 Javascript
微信小程序 行的删除和增加操作实现详解
Sep 29 Javascript
在js文件中引入(调用)另一个js文件的三种方法
Sep 11 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
MySQL修改密码方法总结
2008/03/25 PHP
linux下 C语言对 php 扩展
2008/12/14 PHP
解析MySql与Java的时间类型
2013/06/22 PHP
在WordPress中获取数据库字段内容和添加主题设置菜单
2016/01/11 PHP
golang 调用 php7详解及实例
2017/01/04 PHP
PJBlog插件 防刷新的在线播放器
2006/10/25 Javascript
从sohu弄下来的flash中展示图片的代码
2007/04/27 Javascript
js 鼠标点击事件及其它捕获
2009/06/04 Javascript
jQuery学习5 jQuery事件模型
2010/02/07 Javascript
jquery 多行滚动代码(附详细解释)
2010/06/17 Javascript
javascript 主动派发事件总结
2011/08/09 Javascript
杨氏矩阵查找的JS代码
2013/03/21 Javascript
js实现仿京东2级菜单效果(带延时功能)
2015/08/27 Javascript
jQuery Ajax Post 回调函数不执行问题的解决方法
2016/08/15 Javascript
jquery滚动条插件(可以自定义)
2016/12/11 Javascript
bootstrap中添加额外的图标实例代码
2017/02/15 Javascript
关于Node.js的events.EventEmitter用法介绍
2017/04/01 Javascript
详解Vue.js组件可复用性的混合(mixin)方式和自定义指令
2017/09/06 Javascript
VUE v-for循环中每个item节点动态绑定不同函数的实例
2018/09/26 Javascript
在小程序中推送模板消息的实现方法
2019/07/22 Javascript
jQuery中DOM常见操作实例小结
2019/08/01 jQuery
react用Redux中央仓库实现一个todolist
2019/09/29 Javascript
JS实现电商商品展示放大镜特效
2020/01/07 Javascript
pycharm使用matplotlib.pyplot不显示图形的解决方法
2018/10/28 Python
Tensorflow:转置函数 transpose的使用详解
2020/02/11 Python
Html5 localStorage入门教程
2018/04/26 HTML / CSS
HTML5 3D书本翻页动画的实现示例
2019/08/28 HTML / CSS
HTML5实现移动端点击翻牌功能
2020/10/23 HTML / CSS
现代家居用品及礼品:LBC Modern
2018/06/24 全球购物
英国简约舒适女装品牌:Great Plains
2018/07/27 全球购物
幼儿园爱国卫生月活动总结
2014/06/30 职场文书
原料仓仓管员岗位职责
2014/07/08 职场文书
公司自我介绍演讲稿
2014/08/21 职场文书
2014各大专业毕业生自我评价
2014/09/17 职场文书
2014办公室副主任四风对照检查材料思想汇报
2014/09/20 职场文书
只需要这一行代码就能让python计算速度提高十倍
2021/05/24 Python