Shindig 最初是在 2007 年由 Google 发起的,作为 iGoogle 的 Gadget 容器。Shindig 作为一个参考容器,可以在任意网站上运行支持 OpenSocial 的社交应用。当时 Shindig 支持 Google Gadget 的规范和 OpenSocial 的规范。Shindig 的中文释义为盛大的社交舞会,作为 OpenSocial 规范的一个参考实现的项目名称也是非常合适的。OpenSocial 提供给了开发者一系列通用的 API,基于这些 API 开发的社交应用程序(Gadget、iWidget)可以运行在任意支持 OpenSocial 规范的社交网站上。
Shindig 的主要目的就是在帮助这些社交网站可以在很短的时间内实现对 OpenSocial 规范的支持,从而使得社交应用的开发者可以不用去关心平台的转换。Shindig 自从 2007 年底成为 Apache 的一个开源项目后,就作为 OpenSocial 规范的参考实现和可以支持 OpenSocial 应用的容器不断更新,目前最新发布的版本是 2.0.1,实现了 OpenSocial 1.1 的规范。很多社交网站都是基于 Shindig 实现自己的 OpenSocial 功能的,比如 LinkedIn、hi5。
Shindig 的另一个目标是支持多种语言的实现,目前有 Java 和 PHP 两个版本,Java 版本基于 Java Servlet Stack 实现,本文的一些示例以及代码也都基于 Java 版本。
Shindig 的主要组件包括:
Shindig 的组件架构如图 1 所示:
Gadget 由 XML 和其所使用的特性 JavaScript 类库构成,默认的 Gadget 容器会将 Gadget 放在一个 iframe 里面来展现。当 Gadget 容器准备 Render 一个 Gadget 时,首先会获取该 Gadget 的 metadata 信息,进而通过对应的信息组成 iframeUrl,并将该 URL 设置为 iframe 的 src,此时便会触发服务器端名为“xml-to-html”的 servlet 即 Gadget Rendering Sevlet 负责处理这个请求并最终返回 HTML。JsonRpcServlet 和 DataServiceServlet 负责处理 OpenSocial 相关的请求,DataServiceServlet 处理 Rest 请求,JsonRpcServlet 处理 RPC 请求,在后台他们共享同样的实现。OpenSocial Hanlder 负责处理 OpenSocial 相关的请求,具体由下面各个相关的 Service 实现。中间的 JsonDBOpenSocialService 则是一个实现了各个 Service 接口的具体实现,以 Json 文件作为数据源。
Shindig 的项目基于 Maven 构建,共有以下几个子项目(基于 Jave 版本的源码):
Shindig 的客户端包括:Gadget 容器、OpenSocial 容器、JSON、Restful 容器和对 Caja 的支持。对应的流程如图 2 所示:
所有这几个容器最终都通过 Gadget.io 的 XmlHttpRequest 发送请求到服务器端。
Gadget 容器目前在 Shindin 里面有两个版本:shindig-container 和 shindig-container 1.0。Shindig 里面所给出的示例都是基于 shindig-container 的,这也是最初 Shindig 的 Gadget 容器。Shindig-container 1.0 是由 Google 在今年 5 月份提交到 Shindig 的,旨在对 Gadget 容器进行更好的分层。本文后面也会对 shindig-container 1.0 进行相应的介绍并给出示例。图 3 所示即为 shindig-container 的组件图:
使用 shindig-container 来展现一个 Gadget 的 code 如下所示:
<html> <head> <title>Sample: A Sample Gadget</title> <link rel="stylesheet" href="gadgets.css"> <script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1"> </script> <script type="text/javascript"> var specUrl0 = 'http://localhost:8080/container/helloWorld.xml'; function init() { shindig.container.layoutManager = new shindig.FloatLeftLayoutManager('gadget-parent'); var gadget = shindig.container .createGadget({specUrl: specUrl0, width: 500}); shindig.container.addGadget(gadget); }; function renderGadgets() { shindig.container.renderGadgets(); }; </script> </head> <body onLoad="init();renderGadgets();"> <h2>Sample: A Sample Gadget</h2> <div id="gadget-parent" class="gadgets-gadget-parent"></div> </body> </html> |
新的 Gadget 容器更加简单方便,对应的组件图如图 4 所示:使用新的 Gadget 容器来展现 Gadget 的 Code 如清单 2 所示:
<html> <head> <title>Sample: A Sample Gadget with new container</title> <link rel="stylesheet" href="gadgets.css"> <script type="text/javascript" src="/gadgets/js/container:rpc:pubsub-2.js?c=1&container=default"> </script> <script type="text/javascript"> var my = {}; my.gadgetSpecUrls = [ 'http://localhost:8080/container/sample-pubsub-2-publisher.xml', 'http://localhost:8080/container/sample-pubsub-2-subscriber.xml' ]; my.init = function() { gadgets.pubsub2router.init( { onSubscribe: function(topic, container) { return true; }, onUnsubscribe: function(topic, container) { }, onPublish: function(topic, data, pcont, scont) { return true; } }); }; my.renderGadgets = function() { shindig.auth.updateSecurityToken( 'john.doe:john.doe:appid:shindig:url:0:default'); var config = {}; config[shindig.container.ServiceConfig.API_PATH] = '/rpc'; config[shindig.container.ContainerConfig.RENDER_DEBUG] = "1"; var myContainer = new shindig.container.Container(config); for (var i = 0; i < my.gadgetSpecUrls.length; ++i) { var el = document.getElementById("gadget-site-" + i); var gadgetSite = myContainer.newGadgetSite(el); myContainer.navigateGadget(gadgetSite, my.gadgetSpecUrls[i], {}, {}); } }; </script> </head> <body onLoad="my.init();my.renderGadgets();"> <h2>Sample: A Sample Gadget with new container</h2> <div id="gadget-site-0" class="gadgets-gadget-chrome"></div> <div id="gadget-site-1" class="gadgets-gadget-chrome"></div> </body> </html> |
新的 Gadget 容器相比 shindig-container 更加清晰,使用起来也更加简单,只需要创建一个 gadget site,然后调用 navigateGadget 即可。在 shindig 发布的 2.0.1 版本中,新的 Gadget 容器使用起来还有几个小 bug,如果您想使用最新的 Gadget 容器,请下载本文提供的针对新的 Gadget 容器的 patch。新的 Gadget 容器提供了很好的分层结构和扩展性,用户可以参考该容器实现自己的 Gadget 容器。本系列后面的文章将会介绍如何基于这个新的 Gadget 容器在当前页面上的内嵌,来展现一个 Gadget, 而不是使用 iframe。
OpenSocial 的容器用来创建与 OpenSocial 有关的请求,例如:newFetchPersonRequest、newFetchPeopleRequest、newFetchPersonAppDataRequest 等,定义了诸如 Person、Activity、Phone 等对象。早期的请求通常通过:
var req = opensocial.newDataRequest(); req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.VIEWER),'viewer'); req.add(req.newFetchPeopleRequest(opensocial.DataRequest.Group.VIEWER_FRINENDS), 'viewerFriends'); req.send(callback); |
来发送请求。目前用的比较多的是使用 osapi:
var batch = osapi.newBatch(); var fields = ['id','age','name','gender','profileUrl','thumbnailUrl']; batch.add('viewer', osapi.people.getViewer({sortBy:'name',fields:fields})); batch.add('viewerFriends', osapi.people.getViewerFriends({sortBy:'name',fields:fields})); batch.execute(callback); |
Shindig 的服务器端流程主要分为两个核心部分,一个是 Render Gadget,一个是处理 OpenSocial 相关的请求。Render Gadget 由 GadgetRenderingServlet 处理,如图 5 所示:首先调用 doGet 方法,调用 Renderer 的 render 方法,通过 Process 解析出一个 Gadget 实例,而后调用具体的 HTMLRenderer 的 render 方法,核心的就是很多的 Rewriter,通过对 Gadget 里面的内容进行重写来生成 HTML。
处理 OpenSocial 请求的具体流程可参见 Shindig 的组件架构图中所示,分为以下几步:
关于服务器端的组件图及各个 Servlet 之间的关系可参考 Shindig 中 REST 实现概述 中的详细介绍。
Shindig 所提供的 OpenSocial API 介绍
Shindig 提供了两套 OpenSocial 的 API:Rest 和 JSON-RPC 的。下面分别对两种 API 加以介绍:Rest 的 API 主要用于服务器到服务器的交互,JSON-RPC 主要用于 Gadget 跟服务器端的交互,诸如 osapi 的很多服务都是通过调用 JSON-RPC API 完成的。
Shindig 实现了所有必须的 RPC 服务,包括 People、Activity、Appdata、Messages 和 System。完整的方法列表请参考 Shindig 概述 中的图表介绍。本文仅以以下几个例子加以说明:
获取所有可用的 RPC 服务:http://localhost:8080/vulcan/shindig/rpc?method=system.listMethods, 返回结果如下所示,可以看到所有 Shindig 支持的服务。
{"result":[ "samplecontainer.update", "gadgets.supportedFields", "userprefs.create","http.head", "http.post", "activities.supportedFields", "gadgets.defaultRenderType", "activities.delete", "appdata.update", "messages.delete","http.delete", "userprefs.update", "activitystreams.delete", "activities.get", "gadgets.metadata", "messages.modify", "activitystreams.supportedFields", "activities.create", "messages.create", "activitystreams.create", "cache.invalidate", "people.supportedFields", "http.put", "activities.update", "activitystreams.get", "userprefs.get", "appdata.delete","http.get", "gadgets.token", "activitystreams.update", "samplecontainer.create", "appdata.get","messages.get", "system.listMethods", "gadgets.tokenSupportedFields", "people.get", "samplecontainer.get", "appdata.create" ]} |
所有这些 RPC 的服务最终都动态绑定到 osapi,用户可以在客户端通过 osapi 的方法调用对应的服务,比如 osapi.gadgets.metadata(request).execute(callback)、osapi.people.get(request).execute(callback)、osapi.appdata.update(request).execute 等。所有这些既可以通过 osapi 来调用完成,也可以直接通过调用 RPC 来完成,最终在服务器端由对应的 Handler 来处理,比如 activity 相关的服务都可以在 ActivityHanlder 里面找到,gadgets 相关的都可以在 GadgetHanlder 里面找到。下面的 code 以 gadgets.metadata 为例加以说明:
var gadgetUrl = "http://localhost:8080/container/sample-pubsub-2-publisher.xml"; var request = { 'container': "default", 'ids': gadgetUrl, 'fields': [ 'iframeUrl', 'modulePrefs.*', 'needsTokenRefresh', 'userPrefs.*', 'views.preferredHeight', 'views.preferredWidth' ] }; osapi.gadgets.metadata(request).execute(function(response) { if (response.error) { console.debug("error when getting metadata!"); } else { for (var id in response) { response[id]['url'] = id; // make sure url is set } var gadgetInfo = response[gadgetUrl]; console.debug("iframeUrl is :",gadgetInfo.iframeUrl); console.debug("required features are :",gadgetInfo.modulePrefs.features); console.debug("gadget title is :",gadgetInfo.modulePrefs.title); } }) |
得到的结果为:
iframeUrl is: http://localhost:8080/gadgets/ifr? url=http%3A%2F%2Flocalhost%3A8080%2Fcontainer%2Fsample-pubsub-2-publisher.xml &container=default&view=%25view%25&lang=%25lang%25& country=%25country%25&debug=%25debug%25&nocache=%25nocache%25& v=16b40aa73ad5c7cf1769dcdc4f4b4e7a required features are {"pubsub-2":{"required":true},"core":{"required":true}} gadget title is Sample PubSub Publisher |
同样的结果也可以通过直接调用 RPC 获取,在浏览器中输入:http://localhost:8080/rpc?method=gadgets.metadata& container=default&ids=http://localhost:8080/container/sample-pubsub-2-publisher.xml& fields=iframeUrl,modulePrefs.features,modulePrefs.title,得到的结果如下:
{"result":{ "http://localhost:8080/container/sample-pubsub-2-publisher.xml": {"modulePrefs":{ "title":"Sample PubSub Publisher", "features":{"pubsub-2":{"required":true},"core":{"required":true}} }, "iframeUrl":"http://localhost:8080/gadgets/ifr?url= http%3A%2F%2Flocalhost%3A8080%2Fcontainer%2Fsample-pubsub-2-publisher.xml &container=default&view=%25view%25&lang=%25lang%25 &country=%25country%25&debug=%25debug%25&nocache=%25nocache%25 &v=16b40aa73ad5c7cf1769dcdc4f4b4e7a" } }} |
通过 metadat 请求获取到了 Gadget 的 iframeUrl 信息,可以直接设置到 iframe 的 src,这也是新的 Gadget 容器的做法。为了与下面的 REST API 做对比,我们再举一个列出用户所有好友列表的请求:http://localhost:8080/rpc?method=people.get&userId=john.doe&groupId=@friends。
{"result":{ "totalResults":3,"filtered":false,"sorted":false, "list":[ {"name":{"givenName":"Jane","formatted":"Jane Doe","familyName":"Doe"}, "id":"jane.doe"}, {"name":{"givenName":"George","formatted":"George Doe","familyName":"Doe"}, "id":"george.doe"}, {"name":{"givenName":"Maija","formatted":"Maija Meikäläinen", "familyName":"Meikäläinen"},"id":"maija.m"} ], "updatedSince":false } } |
Shindig 实现了四种类型的 REST 服务:People、Activity、Appdata 和 Group。完整的 URI 列表请参考 Shindig 概述 中的图表介绍。下面以获取用户好友列表为例,对应的 REST 请求为:http://localhost:8080/social/rest/people/john.doe/@friends。
{"startIndex":0, "totalResults":3, "entry":[ {"name":{"givenName":"Jane","formatted":"Jane Doe","familyName":"Doe"}, "id":"jane.doe"}, {"name":{"givenName":"George","formatted":"George Doe","familyName":"Doe"}, "id":"george.doe"}, {"name":{"givenName":"Maija","formatted":"Maija Meikäläinen", "familyName":"Meikäläinen"},"id":"maija.m"} ],"itemsPerPage":3 } |
Shindig 提供的特性基本都在 shindig-feature 这个项目里面,而另外一些扩展的特性在 shindig-extra 项目里面。根据 OpenSocial 规范里面核心 Gadget 的规范,在 Gadget 的世界里有两种上下文(context):一种是 Gadget 自己的上下文;一种是 Gadget 可以嵌入到其中的 Gadget 容器的上下文。shindig 默认使用 iframe 的形式来展现一个 Gadget,这样这两种上下文就是完全隔离的。外面容器的上下文称为 container,里面 Gadget 的上下文称为 gadget。这样 shindig 里面的特性都是基于这两种上下文来构成的,每个特性都有一个 feature.xml 来描述该特性的名称、所依赖的特性以及该特性用到的 JavaScript 文件。而针对不同的上下文也可能需要不同的 JavaScript 文件。
以 pubsub-2 为例,下面展示的就是 pubsub-2 这个特性所定义的 feature.xml。对于 container 来说需要 pubsub-2-router 这个文件实例化一个 openajaxhub,而对于 gadget 则需要 pubsub-2 来创建一个新的 hubclient。因此该特性针对两种不同的上下文定义了不同的 JavaScript 文件。
<feature> <name>pubsub-2</name> <dependency>globals</dependency> <dependency>org.openajax.hub-2.0.5</dependency> <gadget> <script src="pubsub-2.js"/> <script src="taming.js"/> </gadget> <container> <script src="pubsub-2-router.js"/> </container> </feature> |
shindig 的特性可以分为以下几类:
本系列的下一篇文章会详细介绍 pubsub-2 和 OAuth 相关的特性。
本系列的第一篇文章介绍了如何写自己的第一个 Gadget 以及 OpenSocial 的基本概念,本文则着重介绍了 OpenSocial 的参考实现 Shindig 的基本架构、客户端和服务器端的流程以及基本的特性。