码迷,mamicode.com
首页 > 其他好文 > 详细

REST-RPC风格服务接口实例fenxi

时间:2016-04-08 15:08:31      阅读:170      评论:0      收藏:0      [点我收藏+]

标签:

  无论设计原生手机App,或是前面文章曾提及过的“变脸式应用”(一种无网页刷新的多页面Web应用),都需要后端应用服务器提供业务支持。于是,如何设计后端服务接口是开发前必须考虑清楚的一件事。

  谈及接口设计,我们需要从两个维度来考虑:协议(Protocol)及原型(Prototype),简称2P维度。

  原型定义了一个调用的抽象形式。假定要做上门送餐业务,每个“商户”是个对象,取名为”Store”,那么一个“查询商户列表”接口,可以设计其原型为:

  Store.query() -> tbl(id, name, dscr)

  原型中,描述了调用名为Store.query,参数为空,返回Table类型的数据表示一张表(下文有对此类型介绍),表中的每行有id及name等几列,代表一个商户对象的属性。

  接着,需要设计协议来实现它。可以这样来实现上述调用:客户端对服务端的请求基于HTTP协议,使用HTTP GET或POST方法,将调用名放在URL末尾,将参数放在URL参数中(此处没有),服务端返回数据使用JSON格式来描述。于是,客户端需要像这样发出HTTP请求:

  GET /api/Store.query

  这便是筋斗云框架的服务接口设计。事实上,筋斗云框架是对DACA架构的实现,DACA全称“分布式访问和控制架构”,其中就有定义客户端-服务器如何通讯,即对上例中的设计方案进一步规范化,称为BQP协议(业务查询协议);DACA还规定了客户端如何调用服务接口,称为客户端公共调用接口,如下文将介绍的callSvr调用。

  BQP协议的设计风格介于RESTful和RPC之间,故称为REST-RPC风格。Leonard Richardson 和 Sam Ruby 在他们的著作 RESTful Web Services 中引入了术语 REST-RPC 混合架构(中文版译文),它不像SOAP或XML-RPC那样使用额外的信封格式来包装调用名和参数,而是直接通过HTTP传输数据,这与 REST 样式的 Web 服务是类似的;但是它不使用标准的HTTP PUT/DELETE等方法操作资源,而且在URI中存储调用名(例子中是Store.query)。

  我们考察协议设计的主要原则有:

  清晰易懂

  易实现

  传输及处理效率高

  对照这些原则,RESTful风格清晰易懂,但像HTTP PUT/DELETE等方法的兼容性并不好,不论服务端或客户端在实现上都会遇到障碍;而使用RPC风格的设计,不仅可读性差很多,而且封包解包效率较低。所以,从实用的角度,筋斗云的设计思想认为,REST-RPC是目前更好的选择。

  在BQP协议中,业务接口分为函数调用型接口(如login调用)和对象调用型接口(如Store.query调用)。函数调用型接口可以自由设计原型,而对象调用型接口其实是一种特殊的函数调用,用于操作业务对象,有相对固定的原型,设计者可以对它加以裁减或扩展。两类接口在通讯协议及客户端使用上没有太大区别,其主要区别在于后端服务的实现模型不同。

  本文只讨论对象调用型接口。BQP协议定义了一个对象的五种标准操作:查询列表(query),获取明细(get),添加(add),更新(set)和删除(del)。下文将详细举例说明,我们先假定有“商户”(Store)这个对象,其数据模型描述如下:

  @Store: id, name, addr, tel, dscr

  这表示商户表Store,有id, name等字段。注意:DACA规范建议,在设计数据模型时,应以id作为主键。

  DACA规范要求客户端应提供callSvr方法来调用服务接口,在筋斗云前端中,该接口为JS函数,其原型为

  callSvr(ac, param?, fn?, postParam?, userOptions?) -> XMLHttpRequest

  或

  callSvr(ac, fn?, postParam?, userOptions?) -> XMLHttpRequest

  其中ac表示调用名(action),param和postParam分别为通过URL和POST内容传递的参数,如果没有param,可以忽略该参数(即第二种原型)。fn为回调函数,调用格式为fn(data),其中参数data为返回的JSON对象,类型参考接口原型中的返回值描述。

  带问号的参数表示可缺省。

  函数返回XMLHttpRequest对象,与jQuery中的$.ajax返回值相同。

  以上调用为异步方式,即该函数执行后立即结束,待服务端数据返回再回调函数fn。也可以做同步调用,只要将函数名callSvr改为callSvrSync,即意味着该函数将等服务端返回数据才结束,而且,其返回值不再是XMLHttpRequest对象,而是服务接口返回的JSON对象。我们在Chrome控制台窗口测试接口时,常用同步调用以方便看到结果。

  只要熟悉这几个客户端接口,就可以根据设计文档中的接口原型调用任何接口了,不必再对BQP底层协议细节有深入了解。

  添加对象

  BQP协议中定义对象添加操作的原型如下:

  {object}.add()(POST fields...) -> id

  一般在原型定义中参数部分只用一个括号,表示参数通过URL或POST内容传递都可以。而这里出现了两个括号,就表示URL参数和POST参数不可混用,两个括号依次表示URL参数和POST参数。

  这样,我们添加一个商户,可以用:

  var postParam = {name: "华莹小吃", addr: "银科路88号", tel: "13712345678"};

  callSvr("Store.add", api_StoreAdd, postParam);

  function api_StoreAdd(data) {

  // 根据原型定义中的返回值,data是id值。

  alert("id=" + data);

  }

  由于没有URL参数,所以callSvr的第二个参数可以省略。如果想写完整,会像这样:

  callSvr("Store.add", null, api_StoreAdd, postParam);

  调用成功,则会调用指定的回调函数,如果调用失败,则前端框架会接管错误处理,调用者一般不必关心。

  更新对象

  原型为:

  {object}.set(id)(POST fields...)

  其中未指定返回值,表示调用成功时无特定返回值。筋斗云后端会返回字符串”OK”。

  假如要更新id=8这家商户对应的联系电话:

  var param = {id: 8};

  var postParam = {tel: "13812345678"};

  callSvr("Store.set", param, api_StoreSet, postParam);

  function api_StoreSet(data)

  {

  alert("更新成功");

  }

  注意:要更新的字段一定要放在POST参数中。

  置空一个字段

  在BQP协议中,设置一个字段为空串一般是被服务端忽略的,但在set操作中,如果在postParam中设置某个字段为空串(或特定字符串"null"),则表示清空该字段。

  要清空某商户的地址:

  var postParam = {addr: ""};

  // 或者 var postParam = {addr: "null"};

  callSvr("Store.set", {id: 8}, api_StoreSet, postParam);

  下次用Store.get获取该商户时,可见属性addr值为null (注意:不是字段串"null")

  删除对象

  原型为:

  {object}.del(id)

  调用很简单,假如要删除id=8对应的商户:

  callSvr("Store.del", {id: 8}, function (data) {

  alert("删除成功");

  });

  获取对象详情

  原型为:

  {object}.get(id, res?) -> {fields...}

  其默认返回对象对应主表中的字段,设计时也可以为返回内容增加子对象或虚拟字段(实现方法参考筋斗云后端文档)。

  假定在设计“获取商户”接口时,增加一个子对象“商品列表”名为items,设计接口原型为:

  Store.get(id, res?) -> {id, name, addr, tel, @items=[item]}

  item:: {id, name, price}

  (注意:在设计接口原型时,用的是“蚕茧表示法”层层解析和描述对象类型,不在本文讨论范围内,详见相关文章。)

  根据接口,要获取一个商户的详情可以这样调用:

  callSvr("Store.get", {id: 8}, api_StoreGet);

  function api_StoreGet(data) { ... }

  返回数据data像这样:

  {

  id: 8,

  name: "华莹小吃",

  addr: "银科路88号",

  tel: "13812345678",

  items: [

  {id: 1001, name: "鲜肉小笼", price: 10.0},

  {id: 1002, name: "大肉粽", price: 8.0}

  ...

  ]

  }

  在URL中的可选参数res它表示”result”,即返回字段的列表,多个字段中间用逗号分隔。如果你不想返回默认的字段,可以通过该参数指定想要哪些字段。

  例:获取商户详情,只返回店名和电话:

  callSvr("Store.get", {id: 8, res: "name,tel"}, api_StoreGet);

  function api_StoreGet(data)

  {

  // data示例:{name: "华莹小吃", tel: "13812345678"}

  }

  查询对象列表

  查询操作是标准操作中最灵活和最复杂的,它的可选参数很多,原型有两种(返回内容的格式不同):

  {object}.query(res?, cond?, orderby?, distinct?=0, _pagesz?=20, _pagekey?, _fmt?) -> tbl(field1,field2,...)

  {object}.query(wantArray=1, ...) -> [{field1,field2,...}]

  第一种原型返回特别的Table类型(下文介绍,可以转成对象数组),好处是数据精练,而且支持分页;第二种原型多了wantArray参数的设置(其它参数用法相同),返回类型变成对象数组,支持子对象,然而它不支持分页操作,一般使用较少。

  Table类型

  如果未指定参数wantArray(第一种原型),则返回的内容为Table类型,这种格式不可以返回子对象(如上节get操作中的子对象商品列表items),比如取商户列表:

  callSvr("Store.query", api_StoreQuery);

  function api_StoreQuery(data) { ... }

  回调函数api_StoreQuery中的data参数格式为:

  {

  h: [ "id", "name", "addr", "tel"]

  d: [

  [ 8, "华莹小吃", "银科路88号", "13812345678"],

  [ 9, ... ]

  ...

  ]

  nextkey: 998

  }

  其中属性h为列名数组,d表示数据行数组,每行的值数组与列名数组中元素一一对应。

  如果存在属性nextkey,则表示这只是一部分数据,要取下一页数据,可以用同样的查询,带上参数_pagekey设置为该值,如

  callSvr("Store.query", {_pagekey: 998});

  这种Table结构设计有利于传输效率的提高,同时便于分页机制的设计。

  筋斗云前端提供函数rs2Array,可将这个数据转换成通常用的对象数组:

  var arr = rs2Array(data);

  得到的arr像这样:

  [

  {id: 8, name: "华莹小吃", addr: "银科路88号", tel: "13812345678"},

  {id: 9, ...}

  ...

  ]

  查询参数

  对象查询支持灵活的查询条件(通过参数cond - condition),排序方法(参数orderby),返回字段(参数res,与get操作一样)。

  如果你了解SQL语句,则会发现这些参数用起来很简单。

  参数res指定返回字段, 多个字段以逗号分隔,例如, res=”field1,field2”.

  参数cond指定查询条件,其语法类似SQL语句的”WHERE”子句,例如”field1>100 AND field2=’hello’”,注意字符串值要加上单引号。

  参数orderby指定排序条件,语法可参照SQL语句的”ORDER BY”子句,例如:orderby=”id desc”,也可以多字段依次排序:”tm desc,status” (按时间倒排,再按状态正排)

  例如,要查询所有id小于10且名字中以”华莹”开头的商户,返回结果按名字(name)排序:

  var cond = "id<10 and name like ‘华莹%‘";

  var param = {res: "id,name,addr", cond: cond, orderby: "name"};

  callSvr("Store.query", param, api_StoreQuery);

  function api_StoreQuery(data)

  {

  // 先用rs2Array将table类型的数据转成对象数组

  var arr = rs2Array(data);

  // 遍历每个商户

  arr.forEach(function(store) {

  // 由于指定了res参数,store对象类型为:{id, name, addr}

  });

  }

  尽管这些参数值类似SQL语句,但它们有一些安全限制:

  res, orderby只能是字段(或虚拟字段)列表,不能出现函数、子查询等。

  cond可以由多个条件通过and或or组合而成,而每个条件的左边是字段名,右边是常量。不允许对字段运算,不允许子查询(不可以有select等关键字)。

  像参数cond中出现以下情况都不允许:

  left(type, 1)=‘A‘ -- 条件左边只能是字段,不允许计算或函数

  type=type2 -- 字段与字段比较不允许

  type in (select type from table2) -- 子表不允许

  分页支持

  参数_pagesz和_pagekey用于支持分页。_pagesz指定每次返回多少条数据(默认一次返回20条)。

  下面是一个获取所有商户的例子。第一次查询:

  callSvr("Store.query")

  返回数据像这样:

  {nextkey: 10800910, h: [id, ...], d: [...]}

  其中的nextkey表示数据未返回完,要查询下一页时需填写_pagekey字段。

  第二次查询(下一页):

  callSvr("Store.query", {_pagekey=10800910});

  返回:

  {nextkey: 10800931, h: [...], d: [...]}

  仍返回nextkey字段说明还可以继续查询,再查询下一页:

  callSvr("Store.query", {_pagekey=10800931});

  返回:

  {h: [...], d: [...]}

  返回数据中不带nextkey属性,表示所有数据获取完毕。

  如果想在首次查询时返回总记录数,可以设置_pagekey=0:

  callSvr("Store.query", {_pagekey: 0})

REST-RPC风格服务接口实例fenxi

标签:

原文地址:http://blog.csdn.net/sjzfhyyqq/article/details/51095163

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!