JSONP 是一个非官方的协议,它允许在服务器端集成 Script Tags 返回至客户端,通过 JavaScript Callback 的形式实现跨域访问。其本质就是在服务端调用前端的 JavaScript 方法,并在服务端将参数传入该前端方法中。这种方式在处理跨域请求时尤为重要。
jsonpEcho: {
transport: "JSONP",target: "jsonpEcho.PHP",callbackParamName: "testCallbackParam",var td = this.svc.jsonpEcho({message: this.name});
td.addCallback(this,sans-serif; margin-top: 0px; margin-bottom: 0px; padding-top: 0.3em; padding-right: 5px; padding-bottom: 0.7em; padding-left: 5px; font-size: 0.76em; text-align: left; ">前半段代码是关于 JSONP 的调用配置,注意这里的“transport: "JSONP"”和“callbackParamName: "testCallbackParam"”,这里的“callbackParamName”用于告诉服务端他需要回调的方法,这里为“testCallbackParam”。
当然,仅仅基于前端的代码,是不可能实现 JSONP 的,我们来看看他后端的代码示例:
清单 12. JSONP 风格的 RPC 后端代码示例(PHP)
$jsonp = false;
$result = "";
if ($_REQUEST["testCallbackParam"]){
$jsonp=true;
$result .= $_REQUEST['testCallbackParam'] . "('";
}
if (!$_REQUEST["message"]){
$result .= "ERROR: message property not found";
}
$result .= $_REQUEST["message"];
if ($jsonp) {
$result .= "');";
}
print $result;
细心的读者会发现,这里处理 JSONP 的返回值是一段函数的调用:
1. “$result .= $_REQUEST['testCallbackParam'] . "('";”
2. if ($jsonp) { $result .= "');"; }
本示例是拼接了两段字符串,构造了一段函数调用的语句,返回到客户端执行。这就是 JSONP 的实现方式。
JSON-RPC
我们先来看看 JSON-RPC 的定义:
JSON-RPC 是一个轻量级的远程调用协议。数据通讯由两部分组成:在一次连接的生命期内,一端将发出一个请求来调用另一端的函数。另一端将回应该请求,除非这个请求是一个公告。
请求部分:通过向一个远程服务器发送一个请求来调用一个远程函数。该请求是一个用 JSON 进行了编码 ( 序列化 ) 的对象。它有 3 个部分:
* 函数名(method):被调用方法名。
* 参数数组(params):被调用方法的参数列表。
* 标识码(请求 id,请求的标识码是用来匹配它所对应的回复):可以是任何类 型,用于与响应匹配。
回复部分:当调用请求结束时,服务器将回复该请求。回复同样是用 JSON 进行了编码的对象。它有 3 个部分:
* 返回值 - 是一个由被调用方法返回的对象,如果错误调用方法时,则其值为 null。
* 错误信息 - 如果没有错误调用方法,则其值为 null。
* 标识码 - 和请求的标识码(id)一致。
公告部分:公告(notification)是一种没有回复的请求 . 同样为用 JSON 编码对象。它的标识码(id)为空,其他和普通请求一致。
以上便是 JSON-RPC 的协议规则,其实他就是一个规范了请求数据格式和返回值数据格式等等的远程调用通信协议,这个协议很适合于轻量级的远程调用实现。Dojo 的 RPC 包对该协议也做了很好的封装。尤其是针对已有的 JSON-RPC 后端服务,我们的前端实现只需要基于 Dojo 的 RPC 包,加上一些简单的配置,就能快速的构建完成。Dojo 在底层会把我们的请求转换成 JSON-RPC 协议规定的格式:
清单 13. JSON-RPC 模式示例
postJsonRpc10Echo: {
transport: "POST",envelope: "JSON-RPC-1.0",target: "jsonRpc10.PHP",parameters: [
{type: "string",optional: true}
]
},var td = this.svc.postJsonRpc10Echo(this.name);
td.addCallback(this,function(result){
if (result==this.name){
d.callback(true);
}else{
d.errback(new Error("Unexpected Return Value: ",result));
}
});
这里我们先定义请求方式:“transport: "POST" ”,再通过“envelope: "JSON-RPC-1.0" ”定义 JSON-RPC 协议模式。最后在代码中简单的调用“postJsonRpc10Echo ”方法即可。
当然,服务端也应当满足 JSON-RPC 模式,参见如下示例:
清单 14. JSON-RPC 模式示例(后端 PHP)
$json = new Services_JSON;
$results = array();
$results['error'] = null;
$jsonRequest = file_get_contents('PHP://input');
$req = $json->decode($jsonRequest);
$method = $req->method;
$params = $req->params;
switch($method) {
case "postJsonRpc10EchoNamed":
case "postJsonRpc10Echo":
$results['result']=$params[0];
break;
default:
$results['result']="";
$results['error']="JSON-RPC 1.0 METHOD NOT FOUND";
break;
}
$results['id'] = $req->id;
$encoded = $json->encode($results);
print $encoded;
其实这里的实现和之前的大体相似,主要的不同就在于这里的请求格式是遵循 JSON-RPC 协议的 JSON 模式,可以参见他对请求的解析方式代码:
$req = $json->decode($jsonRequest);
$method = $req->method;
$params = $req->params;
可见其方法“method”和参数“params”符合 JSON-RPC 的规范。同样,返回值也是基于 JSON-RPC 的 JSON 格式:
$encoded = $json->encode($results);
print $encoded;
注意这里的参数取值“$results['result']=$params[0]”,因为我们传参方式是简单的直接传参“this.svc.postJsonRpc10Echo(this.name)”,所以这里是通过数组模式数序匹配取值,同样它也支持对象模式:“this.svc.postJsonRpc12Echo({message: this.name})”对应着“$results['result']=$params->message;”。
JSON-REST-Store
Dojo 里面有一个很好用的用于管理数据的 Store:“dojox.data.JsonRestStore”,他的实现就是基于 Dojo 的 RPC 接口的。通过这个接口,我们能够很方便并快速的构建我们的 REST 应用。这里我们简单介绍一下:
清单 15. JsonRestStore 基本用法
var testServices = new
dojox.rpc.Service(dojo.moduleUrl("dojox.rpc.tests.resources","test.smd"));
var jsonStore = new dojox.data.JsonRestStore({service:testServices.jsonRestStore});
jsonRestStore: {
transport: "REST",optional: true}
],returns: {
properties:{
name:{
type:"string",minLength:3
}
}
}
},jsonStore.fetch({query:"query",onComplete: function(items,request){
t.is(4,items.length);
d.callback(true);
},onError: dojo.partial(dojox.rpc.tests.stores.JsonRestStore.error,doh,d)
});
从上上述示例中我们可以看到:这里还是先初始化了 RPC 对象,然后基于此 RPC 对象构造了我们的 JsonRestStore 对象。
第二段代码是关于这个 REST 的 Store 的定义,与之前介绍的基本无异,只是多了一个返回值的定义“returns”。所以,服务端在返回数据时必然会包含“name”这个属性及其属性值。
第三段代码就是 Store 的使用案例,其使用方式与基本 Store 无异,主要通过“fetch”取值,可以加入自己的检索条件等等。
同样,它也支持添加,修改,删除,甚至修改后的回滚等操作等操作:
清单 16. JsonRestStore 进阶用法
// 修改和保存
jsonStore.fetch({query:"query",request){
var now = new Date().getTime();
jsonStore.setValue(items[0],"updated",now);
jsonStore.setValue(items[0],"obj",{foo:'bar'});
// 修改和保存操作
jsonStore.setValue(items[0],"obj dup",items[0].obj);
jsonStore.setValue(items[0],"testArray",[1,2,3,4]);
jsonStore.save();
jsonStore.fetch({query:"obj1",onComplete: function(item,request){
t.is("Object 1",item.name);
t.is(now,item.updated);
t.is("bar",item.obj.foo);
t.is(item.obj,item['obj dup']);
d.callback(true);
},onError: dojo.partial(
dojox.rpc.tests.stores.JsonRestStore.error,d)});
},d)
});
// 添加和删除
jsonStore.fetch({query:"obj1",request){
var now = new Date().getTime();
var testArray = item.testArray;
// 添加和删除操作
var newObject = {"name":"new object"};
testArray.push(newObject);
jsonStore.save();
jsonStore.deleteItem(newObject,{parent:testArray});
jsonStore.save();
testArray.sort(function(obj1,obj2) { return obj1 < obj2; });
jsonStore.save();
testArray.sort(function(obj1,obj2) { return obj1 > obj2; });
jsonStore.save();
d.callback(true);
},d)
});
// 回滚
jsonStore.fetch({query:"obj1",request){
jsonStore.setValue(item,"name","new name");
jsonStore.setValue(item,"newProp","new value");
jsonStore.unsetAttribute(item,"updated");
t.is(jsonStore.getValue(item,"name"),"new name");
t.is(jsonStore.getValue(item,"newProp"),"new value");
t.is(jsonStore.getValue(item,"updated"),undefined);
// 回滚操作
jsonStore.revert();
t.is(jsonStore.getValue(item,"Object 1");
t.is(jsonStore.getValue(item,undefined);
t.t(typeof jsonStore.getValue(item,"updated") == 'number');
d.callback(true);
},sans-serif; margin-top: 0px; margin-bottom: 0px; padding-top: 0.3em; padding-right: 5px; padding-bottom: 0.7em; padding-left: 5px; font-size: 0.76em; text-align: left; ">以上是关于 JsonRestStore 的简单介绍,他还有延迟加载,分页等等功能,这里不再深入,有兴趣的读者可以参考 Dojo 的官方文档。
Dojo 的 RPC 的具体应用
之前我们介绍了 Dojo 的 RPC 接口,主要是基于一些 PHP 写的后台服务,接下来我们来看看现在如何利用 Dojo 来使用市面上流行的一些 RPC 服务。
Dojo 对一些比较流行的 RPC 服务,如雅虎,社交网络(FriendFeed),地理信息(Geonames),维基百科(Wikipedia)等等都写好了相应的 SMD 配置文件,使得我们能够非常方便的基于这些文件实现我们的 RPC 应用。同样,我们也能够模仿这些 SMD 文件去实现我们自己喜欢的 RPC 服务的 SMD 配置。针对这些市面上流行服务的 SMD 配置文件主要在“dojox/rpc/SMDLibrary”中,大家可以直接拿来使用,也可以添加一些自定义的配置信息。
雅虎 RPC
雅虎的 RPC 调用方式与之前我们介绍的方式基本一样。大家可以先参考雅虎 RPC 的官方文档描述,决定使用的接口之后再将其集成到我们的远程服务配置 SMD 文件里面。Dojo 的“yahoo.smd”配置文件已经配置好了很多服务,大家可以直接使用或添加自己需要的服务配置:
清单 17. 雅虎 RPC
questionSearch: {
target: "http://answers.yahooapis.com/AnsweRSService/V1/questionSearch",parameters: [
{ name: "query",optional: false,"default": "" },{ name: "search_in",optional: true,"default":
"all" },// 取值列表 "all","question","best_answer"
{ name: "category_id",type: "integer","default":
null },// (category_id,category_name) 其中之一为必须
{ name: "category_name",{ name: "region","default": "us" },// 取值列表 "us","uk","ca","au","in","es","br","ar","mx","e1","it","de","fr","sg"
{ name: "date_range","7","7-30","30-60","60-90","more90"
{ name: "sort","default":
"relevance" },// 取值列表 "relevance","date_desc","date_asc"
{ name: "type","default": "all" },{ name: "start","default": 0 },{ name: "results","default": 10 } ]
},}
var yd = dojox.rpc.tests.yahooService["questionSearch"]({query: "dojo toolkit"});
yd.addCallback(this,function(result){
if (result[method.expectedResult]){
d.callback(true);
}else{
d.errback(new Error("Unexpected Return Value: ",sans-serif; margin-top: 0px; margin-bottom: 0px; padding-top: 0.3em; padding-right: 5px; padding-bottom: 0.7em; padding-left: 5px; font-size: 0.76em; text-align: left; ">前段代码是雅虎 RPC 服务的配置,这个服务主要为雅虎问答的检索。
后段代码用于调用服务,与之前介绍的方式无异。
雅虎还提供了很多的服务,包括音频,视屏的检索,图片查询,交通,旅游信息的查询等等,一下是关于音频,视屏的检索的 SMD 配置:
清单 18. 雅虎 RPC 服务
songSearch: {
target: "http://search.yahooapis.com/AudioSearchService/V1/songSearch",parameters: [
{ name: "artist",{ name: "artistid",{ name: "album",{ name: "albumid",{ name: "song",{ name: "songid",{ name: "type","any","phrase"
{ name: "results","default": 10 },// 最大值 50
{ name: "start","default": 1 }
]
},videoSearch: {
target: "http://search.yahooapis.com/VideoSearchService/V1/videoSearch","default": "any" },"default": 1 },{ name: "format","default":
"any" },// 取值列表 "any","avi","flash","mpeg","msmedia","quicktime","realmedia"
{ name: "adult_ok",type: "boolean",{ name: "site","default": null }
]
},sans-serif; margin-top: 0px; margin-bottom: 0px; padding-top: 0.3em; padding-right: 5px; padding-bottom: 0.7em; padding-left: 5px; font-size: 0.76em; text-align: left; ">基于这些 RPC 服务资源,我们能丰富我们的 Web 应用的内容,构建更加多彩的 Web 产品。
社交网络 FriendFeed 的 RPC
与雅虎 RPC 一样,他的使用方式也相当简单,只需要简单的配置加上方法的调用即可,以下为其服务的配置:
清单 19. FriendFeed 的 RPC 配置
services: {
users: {
target: "http://friendFeed.com/api/Feed/user",parameters: [
{ name: "nickname","default": "" }
]
},entry: {
target: "http://friendFeed.com/api/Feed/entry",parameters: [
{ name: "entry_id",search: {
target: "http://friendFeed.com/api/Feed/search",parameters: [
{ name: "q","default":
"" }
]
},url: {
target: "http://friendFeed.com/api/Feed/url",parameters: [
{ name: "url",domain: {
target: "http://friendFeed.com/api/Feed/domain",parameters: [
{ name: "domain","default":"" }
]
}
}
可以看到,FriendFeed 主要支持 5 种接口,大家可以利用这些接口给自己的 Web 应用添加相应的社交功能。
同样,还有推特 (Twitter),地理信息(Geonames)和维基百科(Wikipedia)等等 RPC 服务在 Dojo 中均有其默认 SMD 配置,有兴趣的读者可以深入研究。
应用举例
接下来我们以 Google 的 RPC 服务为例实现一个简单的检索应用:
图 1. Google 的 RPC 示例
这是一个用 Dojo 实现的 Web 搜索引擎,该引擎基于 Google 的 RPC 服务。我们通过查询 Google 的 RPC 文档,找到了 Google 的关于 Web 各种资源检索的 RPC 服务地址,并且通过解读他的参数文档,实现了我们的 SMD 文件的配置:
清单 20. Google 的 Web 检索配置
"parameters": [
// 检索参数 :
{ "name": "q","default":"" },// 返回值大小 : large | small ( 每页 8 或 4 条内容 )
{ "name": "rsz",optional:true,"default": "small" },// 语言 :
{ "name": "hl","default": "en" },...................
],"services": {
"webSearch": {
"target": "http://ajax.googleapis.com/ajax/services/search/web","parameters": [
{ "name": "cx","type":"string","optional":true },{ "name": "cref",]
},"videoSearch": {
"target": "http://ajax.googleapis.com/ajax/services/search/video","parameters": [
{ "name": "scoring","type": "string","optional": true }
]
},"blogSearch": {
"target": "http://ajax.googleapis.com/ajax/services/search/blogs","newsSearch": {
"target": "http://ajax.googleapis.com/ajax/services/search/news","optional": true },{ "name": "geo",optional:true }
]
}
....................
}
可以看到,这里有网页内容的检索服务,视屏检索服务,博客检索以及新闻检索等等服务,注意到开头的"parameters"配置,这里主要配置通用参数,这里的参数“q”便是检索关键字的形参,我们主要也是通过这个参数信息向 Google 的 RPC 后台发请求。
当用户通过我们构建的页面选择检索类型(视屏,新闻等等)并输入检索参数后,我们便会调用 Dojo 的 RPC 接口想 Google 发送查询请求。
清单 21. Google 信息检索
google = new
dojox.rpc.Service(dojo.moduleUrl("dojox.rpc","SMDLibrary/google.smd"));
//searchType 为"webSearch", "videoSearch"等等
google[searchType]({ q: dojo.byId("test").value })
.addCallback(function(returned){
var ret = returned.responseData;
var info = ret.cursor;
var data = ret.results || [];
dojo.forEach(data,function(item){
var li = dojo.doc.createElement('li');
li.innerHTML =
"<a target='_new' hr"+"ef='"+ (item.unescapedUrl ||
item.url) +"'>" + item.title + "</a><br />" +
"<span class='summary'>" + (item.content || item.streetAddress || "unknown")
+ "</span>";
console.log(item);
dojo.byId("top").appendChild(li);
});
tehloader("hidden");
})
// something bad happened:
.addErrback(function(err){
console.warn('ooops',err);
tehloader("hidden");
});
};
请求返回后,将结果解析并展示在页面上。至此,一个简单的搜索引擎便制作完成。
结束语
这篇文章介绍了 Dojo 中的 RPC 的工具包,先从基本的 RPC 接口入手,介绍 Dojo 的 RPC 远程调用的配置和使用,并深入到高级的用法,包括不同方式的参数传递和方法的声明。然后依次介绍了基于 REST,JSONP 和 JSON-RPC 的 RPC 调用,同时也提到了一个基于 RPC 的 Store:“dojox.data.JsonRestStore”。最后,扩展到市面上一些流行的 RPC 服务,如雅虎,推特,维基百科等等。文章结束的时候,通过一个具体的基于 Google 的 RPC 服务的应用介绍了 Dojo 的 RPC 应用的开发流程。这些内容我们可以在开发过程中多关注一下,以尽可能多的完善我们的 Web 应用。