本文假设您已经下载了 Project Zero 并且完成了 简明教程 的学习,或者曾经写过简单的应用程序。您应该熟悉 REST 的基本原理和不同类型的 HTTP 方法(GET、POST 等等)。有关 REST 的介绍,请参考 参考资料 部分。
Yahoo! 的 Flickr 照片共享服务(参考 参考资料)是一个很好的 RIA 示例。由于 Flickr 具有其他 RIA 作者想要的许多特性,使用 Zero 重新创建这类应用程序,能够很好地证明 Zero 是一款开发 RIA 的优秀平台。幸运的是,我们无须从头创建整个照片共享服务 — Zero 平台包括许多 RIA 组件,例如,blog、评定系统、配置文件管理器等等,在构建您自己的应用程序时,所有这些组件均可重用。接下来的章节将讨论照片共享服务的本质,同时也会展示有了 Zero 中的这些组件后,这类应用程序的共同之处非常之多。
创建照片共享服务初看上去非常复杂,但如果静下心来仔细想想,就会发现这类应用程序有很多共同之处,它们都具有两个容易理解的 Web 2.0 应用程序:blog 和文件共享。Flickr 本质上是一种 blog 服务,为其 blog 文章(blog post)提供了一种更加特定于域的视图。用户收藏夹中的每张照片都由一个 blog 文章表示,这些照片(blog 文章)再被组织成各个相册(blog 类别)并用描述性标记加以扩充。此外,Flickr 还允许用户访问普通用户界面之外的真正的图像文件,这就意味着图像存储是由一种与 blog 服务分离的高端文件共享服务处理的。Project Zero 的可选项库套件中既包括了 blog 与文件共享组件,而且二者还可以组合在一起提供类似的照片共享体验。
我们这个照片共享 UI 的实现包括用文件共享组件存储照片,和用 blog 组件对照片进行组织。当用户浏览文件共享或 blog 内容时,将会看到他们的照片和照片共享 UI 更加以照片为中心,而且用户友好性更好。奇特的 Ajax 和受 Flash 驱动的小部件都使得 Flickr UI 非常有吸引力,但是如何创建它们不属于本教程讨论的范围。本文介绍了驱动这些小部件行为所需的 RESTful 接口和逻辑。一个设计良好的照片共享服务就绪后,就可以在它上面放置任何类型的 UI 装饰了。
让我们先来仔细看一下 Zero 的 blog 和文件共享组件的设计,了解如何轻松地将它们应用到照片共享应用程序中。
blog 和文件共享组件都由能使用 HTTP 方法处理的一个或多个资源类型组成。每个 HTTP 方法代表资源上的一个读或写操作,每个 HTTP URI 代表单个的资源实例。例如,针对 http://www.example.com/blogs/jsmith/posts/our-latest-vacation 的一个 HTTP GET 可能会返回用户 jsmith 的一个 blog 文章表示,标题为 Our Latest Vacation。同样地,针对 http://www.example.com/blogs/jsmith 的一个 HTTP POST 也可能被用来创建用户 jsmith 的 blog 中的一个新的 blog 文章。表 1 解释了 HTTP 方法和 URI 是如何用来与 Zero 的 blog 服务交互的,包括客户机和服务器之间交换的数据格式。用粗体表示的这些 URI 标记是变量:
表 1. Zero blog 组件的 REST 接口
| URI | HTTP 方法 | 数据格式 | 描述 |
|---|---|---|---|
| /blogs | POST | JSON | 使用在请求体中发送的 JSON 表示创建一个新的 blog |
| /blogs/blog-name | GET | JSON | 检索给定名称的 blog 的资源定义 |
| /blogs/blog-name | PUT | JSON | 更新给定名称的 blog 的资源定义 |
| /blogs/blog-name | DELETE | 无 | 删除给定名称的 blog,包括它的所有文章和注释 |
| /blogs/blog-name/posts | POST | Atom | 基于在请求体中发送的 JSON 表示创建一个新的 blog 文章 |
| /blogs/blog-name/posts/post-id | GET | Atom | 检索给定 ID 的 blog 文章的资源定义 |
| /blogs/blog-name/posts/post-id | PUT | Atom | 更新给定 ID 的 blog 文章的整个资源定义 |
| /blogs/blog-name/posts/post-id | DELETE | 无 | 删除给定 ID 的 blog 文章 |
当通过 HTTP 添加身份验证和授权时,就会注意到此 API 提供了管理一个或多个 blog 所需的全部功能。此 API 可非常容易地被 Ajax 工具箱(例如 Dojo)调用,这就让开发人员能够将响应数据和 UI 小部件进行集成,而不需要页面刷新(有关 Dojo 工具箱的更多信息,请参阅 参考资料)。当然,此 API 也能通过普通 HTTP 客户机访问,比如 Apache 的 HttpClient(面向 Java™ 技术的)或 cURL 命令行工具(面向 C 的)(有关链接,请参阅 参考资料)。您甚至可以使用 Web 浏览器的地址栏调用此 API 的某些部分。可使用 JavaScript Object Notation(JSON)对 blog 进行调用和修改,而 blog 文章则可使用 Atom(有关链接,请参见 参考资料)进行处理。表 2 给出了用于文件共享组件的一个类似的 API,它存储公共文件和目录:
表 2. Zero 文件共享组件的 REST 接口
| URI | HTTP 方法 | 数据格式 | 描述 |
|---|---|---|---|
| /file-shares/user-name | POST | JSON | 使用用户文件共享的 JSON 表示向其中添加新文件或目录 |
| /file-uploads/user-name | POST | HTML 表单数据 | 使用 HTML 表单向用户的文件共享中添加新文件或目录 |
| /file-shares/user-name/path | GET | JSON | 检索用户文件共享中给定路径上的文件或目录 |
| /file-shares/user-name/path | PUT | JSON | 更新用户文件共享中给定路径上的文件或目录 |
| /file-shares/user-name/path | DELETE | 无 | 删除用户文件共享中给定路径上的文件或目录 |
此文件共享 API 中值得注意的地方包括使用用户名来分离共享目录和使用双重方式来添加文件。每个用户都有自己的共享目录,这有利于使用用户照片共享帐号来确定存储图像文件的位置。在设计相册 UI 时利用 HTTP POST 添加文件的两个方法带来了很大的灵活性 — 既可以使用 HTML 表单来上传数据,也可以使用类似 Flickr Uploadr 工具的非浏览器客户机(有关链接,请参阅 参考资料)。
这些 API 准备好后,就可以用这两个组件上的一系列操作来设计我们的照片共享服务了。您无需知道任何有关组件实现的详情 — 所有与它们的交互都通过其 RESTful HTTP 接口来实现。下一步就是做一些实质性的决定,以便确定照片共享动作如何映射到 HTTP 请求。
现在可以开始设计应用程序了,方法是构建一个类似 表 1 和 表 2 的表格,然后看看这些 blog 和文件共享组件已经涵盖了哪些功能以及需要编写什么代码。表 3 描述了需要支持的功能以及它们将如何映射到组件 API 调用:
表 3. 将照片共享动作映射到 blog 和文件共享
| 照片共享动作 | 底层的动作 |
|---|---|
| 创建新照片共享帐号 | 创建一个新的 blog 来对照片进行分类 为图像文件创建一个新的文件共享目录 |
| 创建新相册 | 创建一个新的 blog 类别 |
| 向相册中添加照片 | 将一个图像文件的副本上载到文件共享目录 用指向此图像文件的 <img/> 标记创建一个新的 blog 文章
|
| 检索所有照片 | 检索此照片 blog 中的所有 blog 文章 |
| 检索整个相册 | 检索给定类别中的所有 blog 文章 |
| 检索单个照片 | 检索此照片的 blog 文章 |
| 更新单个照片,包括其相册 | 更新此照片的 blog 文章和类别名 |
| 删除所有照片 | 删除此照片 blog 可选地,可以连同图像文件删除此文件共享目录 |
| 删除整个相册 | 删除此 blog 类别 删除类别中的 blog 文章 可选地,可以删除与这些 blog 文章相关的图像文件 |
| 删除单个照片 | 删除此 blog 文章 可选地,可以删除与此 blog 文章相关的图像文件 |
表 3 说明了只有少数特性需要底层的多个操作,所以需要编写的胶合代码(glue code)数量将会最少。表 4 与表 3 基本相同,只不过描述性文字已经被需要使用的确切 HTTP 方法和 URI 所替代:
表 4. 将照片共享动作映射到 HTTP 请求
| 照片共享动作 | 底层的动作 |
|---|---|
| 创建新的照片共享帐户 | HTTP POST 到 /blogs HTTP POST 到 /file-shares/user-name |
| 创建新相册 | HTTP POST 到 /blogs/blog-name/categories |
| 向相册中添加照片 | HTTP POST 到 /files-shares/user-name
HTTP POST 到 /blogs/blog-name/posts |
| 检索所有照片 | 在 /blogs/blog-name 上进行 HTTP GET |
| 检索整个相册 | 在 /blogs/blog-name/categories/category-name 上进行 HTTP GET |
| 检索单个照片 | 在 /blogs/blog-name/post/post-id 上进行 HTTP GET |
| 更新单个照片,包括其相册 | HTTP PUT 到 /blogs/blog-name/post/post-id |
| 删除所有照片 | 在 /blogs/blog-name 上进行 HTTP DELETE
在 /file-shares/user-name/photos 上进行 HTTP DELETE |
| 删除整个相册 | 在 /blogs/blog-name/categories/category-name 上进行 HTTP DELETE
在 /blogs/blog-name/posts/post-id 上进行 HTTP DELETE 在 /file-shares/user-name/photos/photo-file-name 上进行 HTTP DELETE |
| 删除单个相片 | 在 /blogs/blog-name/posts/post-id 上进行 HTTP DELETE
在 /file-shares/user-name/photos/photo-file-name 上进行 HTTP DELETE |
HTTP 请求的所有特性都可以很容易地利用前面所提到的某一个 Ajax 或 HTTP 客户机库处理;多操作的特性将需要一些胶合代码,确保所有的 HTTP 请求均可成功完成(作为一个 “事务”)。在此处,尚未确立这个照片共享服务独立于 blog 或文件共享的任何特性,所以无需在 Zero 应用程序中创建任何新的 RESTful 资源。
对照片共享应用程序背后的概念有了基本的了解之后,就可以着手进行实践了。本节假设您已经使用 Zero 的 Eclipse 插件或命令行工具创建了一个新的 Zero 应用程序,名为 photo-share。随后的指令会引用此命令行接口,但很容易就可从项目的上下文菜单中找到 Eclipse 对等物。
第一步是将此 blog 和文件共享组件添加到应用程序中。打开存在于 /config/ivy.xml 的应用程序的 Ivy 文件并添加如下代码:
清单 1. 将 blog 和文件共享组件作为依赖项添加
<dependency org="zero" name="zero.services.blog" rev="1.0.0+"/>
<dependency org="zero" name="zero.services.share" rev="1.0.0+"/>
|
一旦添加了 清单 1 中所示的 XML,就必须从命令行运行
zero resolve, 以完成这两个组件的安装。尽管它们实际的代码和工件还没有添加到项目中,但借助于 Zero 的依赖项解析(dependency resolution)机制,从您自己的代码对这些组件的引用会在运行时工作。
映射到单个 HTTP 请求的照片共享动作(请参阅 表 4)可以很容易地用 Dojo JavaScript 工具箱实现。Zero 包括了 Dojo 的最新版本(0.4.3),也可以通过向 Ivy 文件添加清单 2 中的 XML 从而将其添加到应用程序中:
清单 2. 添加 Dojo 依赖项
<dependency org="dojo" name="dojo" rev="0.4.3+"/>
|
通过 Dojo 可以很容易地将 HTML 表单数据转换成 HTTP 请求,而且这也是这些单一请求动作所需的全部操作。清单 3 中的代码显示了一个可用来输入新相册名称的 HTML 表单和一个可通过 HTTP POST 创建新相册的 JavaScript 函数。请注意从表单取得的数据是如何作为一个 JSON 对象直接添加到请求体中的;zero.services.blog 的实现需要一个 JSON 对象,而且根本无需修改这些数据即可处理请求。
清单 3. 用 HTML 和 JavaScript 创建一个新相册
<script type="text/javascript" src="/dojo.js">
</script>
<script>
dojo.require("dojo.io");
dojo.require("dojo.json");
dojo.require("dojo.widget.Form");
function createAlbum()
{
var form = dojo.widget.byId("CreateAlbumForm");
var albumData = form.getValues();
var blogName = getPhotoShareName();
dojo.io.bind({
url: '/resources/blogs/' + blogName + '/categories',
method: 'POST',
sync: false,
mimetype: 'text/json',
contentType: 'text/json',
postContent: dojo.json.serialize(albumData),
load: function(type, data) {
alert("The new album was added successfully.");
},
error: function(type, err) {
alert(dojo.errorToString(err));
}
});
}
function getPhotoShareName()
{
return <%= _gc.get("/request/subject/remoteUser") + "-photos"; %>
}
</script>
...
<form dojoType="form" id="CreateAlbumForm" name="CreateAlbumForm">
<table cellpadding="10" cellspacing="0" border="0" width="50%">
<tr>
<td><b>New Photo Album:</b></td>
<td><input type="text" size="64" name="categoryid"/></td>
</tr>
</table>
<br/>
<input type="button" onClick="createAlbum();" value="Submit" />
</form>
|
正如您所见,createAlbum() 函数直接从 HTML 表单(form.getValues())取得 JSON 数据,并将其作为一个到 dojo.io.bind() 的参数添加到 HTTP 请求。此请求是一个带 URI /blogs/blog-name/categories 的 HTTP POST,如 表 4 所示。请注意即使 HTML 告诉用户他们正在创建一个新相册,实际上,在底层,是创建了一个可用来代表它的新的 blog 类别;类似地,在构造 HTTP 请求 URI 时,用户照片共享的名称(由 getNameOfPhotoShare() 方法提供)可用作 blog 的名称。用户无需知道有关应用程序对 blog 的使用的任何内容 — 用户知道的是他们现在有办法可以组织自己的照片了。
其他单一请求动作的实现非常简单,只需复制并修改 createAlbum() 方法,以便它能有新的名称并使用不同的 HTTP 方法(例如,deleteAlbum() 和 method: 'DELETE')。在所有情况下,都需要对表单按钮单击或页面重载进行响应以便利用 表 4 中的 URI 来 GET、POST、PUT 或 DELETE 内容。
熟悉了 dojo.io.bind() 函数后,单一请求动作并不难实现,但对多请求动作的处理则需要多加考虑。清单 4 显示了可用来创建新照片共享帐户的 HTML 表单和 JavaScript
函数。其中的 createPhotoShare() 方法发送两个 HTTP POST 请求:一个用来创建此用户文件共享上的目录,另一个用来创建组织照片所需的 blog。请注意针对后者的错误处理程序可撤销前者所做的工作,以使数据不会处在一种不一致的状态。
清单 4. 用 HTML 和 JavaScript 创建新的文件共享
<script type="text/javascript" src="/dojo.js">
</script>
<script>
dojo.require("dojo.io");
dojo.require("dojo.json");
dojo.require("dojo.widget.Form");
function createPhotoShare()
{
var userName = getUserName();
//
// create JSON objects to represent shared directory and blog
//
var directoryData = {
path = userName + '-photos',
contenttype = 'directory',
owner = userName
};
var blogData = {
DESCRIPTION : 'The photo blog for ' + userName,
AUTHOR: userName,
HANDLE: userName + '-photos',
TITLE: 'The photo blog for ' + userName
};
dojo.io.bind({
url: '/resources/file_shares/' + userName,
method: 'POST',
sync: false,
mimetype: 'text/json',
contentType: 'text/json',
postContent: dojo.json.serialize(directoryData),
load: function(type, data) {
alert("The new photo directory was created successfully.");
},
error: function(type, err) {
alert(dojo.errorToString(err));
}
});
dojo.io.bind({
url: '/resources/blogs',
method: 'POST',
sync: false,
mimetype: 'text/json',
contentType: 'text/json',
postContent: dojo.json.serialize(blogData),
load: function(type, data) {
alert("The new photo blog was created successfully.");
},
error: function(type, err) {
alert("Could not complete creation of photo share.");
deletePhotoShare();
}
});
}
function deletePhotoShare()
{
var user = getUserName();
dojo.io.bind({
url: '/resources/file_shares/' + user + '/' + user + '-photos',
method: 'POST',
sync: false,
headers: { 'X-Method-Override' : 'DELETE' },
load: function(type, data) {
alert("The photo directory was deleted successfully.");
},
error: function(type, err) {
alert(dojo.errorToString(err));
}
});
dojo.io.bind({
url: '/resources/blogs/' + user + '-photos',
method: 'POST',
sync: false,
headers: { 'X-Method-Override' : 'DELETE' },
load: function(type, data) {
alert("The photo blog was deleted successfully.");
},
error: function(type, err) {
alert(dojo.errorToString(err));
}
});
}
function getUserName()
{
return <%= _gc.get("/request/subject/remoteUser"); %>
}
</script>
...
<form dojoType="form" id="CreateAlbumForm" name="CreateAlbumForm">
<input type="button" onClick="createPhotoShare();" value="Create My Photo Share!" />
</form>
|
清单 4 中需要理解的东西很多,所以请多加注意。HTML 表单只不过是一个按钮,用户可以在其上单击以创建自己名下的照片共享帐户。默认的行为是使用模式
user-photos 命名用户帐号(以及连带的文件共享和 blog),其中的 user 是用户的登录 ID。可以使用这个帐号名称来构造 createPhotoShare() 函数中的 HTTP POST 请求和 deletePhotoShare() 函数中的 HTTP DELETE 请求。在这两个函数中,均可以使用 dojo.io.bind() 来填入 HTTP 请求数据及处理响应。此代码最有趣的地方是前面提到的事务管理和用以表示新的共享目录和 blog 的 JSON 对象的创建,二者都在 createPhotoShares() 中进行。后者需要您知道,为了能被组件所接受,哪些字段是 JSON 对象所必需要具有的;这类信息可以在 projectzero.org 网站上的组件 API 文档中找到(请参阅 参考资料)。
表 4 中的另一个多请求动作所需要的请求与 清单 4 中的那些请求稍有不同,但总的结构应该大体相同:用能撤销所有已完成任务的错误处理程序来多次调用 dojo.io.bind()。这里惟一的区别是与照片或帐户删除所关联的那些动作;一旦删除了部分帐户数据,即使某些部分未成功删除,也必须要完成此任务。这可以通过执行服务器端脚本中(可以在其中执行真正的数据库事务)的所有删除任务加以避免,但对该方法的介绍已经超出了本文的范围。
请花些时间留意一下,现在,针对完全由 blog 和共享目录构成的后端所使用的还是以照片为中心的名称和值。当用户访问这个照片共享网站时,如果不查看 HTML 源代码,他们将不会看到任何对 blog 或文件共享的引用,这就让应用程序看起来很有整体性,尽管底层是分块的。最后一点值得一提的是,这两个组件在照片共享的范围之外也很有用,这就使应用程序比原来计划的还要功能强大。这些组件安装完毕之后,您自然可以使用它们来为用户托管 “常规” blog 和非照片文件共享,所有这些均可在一个网站上实现。漂亮!
至此,您已经看到了 Zero 组件如何能通过其 RESTful 接口相互组合在一起以提供丰富和功能强大的 Internet 应用程序。这种实现照片共享应用程序的方式构建在面向服务的架构和 RESTful 资源之上,其结果是该实现可以尽量多地重用代码并且大部分定制代码都在用户界面中。与 REST 本身一样,照片共享应用程序并非真正的代码集合,而更像是一种架构风格。用 Zero 构建其他类型应用程序的开发人员在计划自己的实现时,都应该仔细分析和考虑 Zero 所能提供的组件。
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
-
加入 Project Zero 社区,并了解相关内容!
-
阅读 “面向资源与面向活动的 Web 服务”(developerWorks,2004 年 10 月),学习 REST 设计的基础知识。
-
了解如何在 Project Zero 应用程序中应用 REST 设计原理。
-
阅读有关 blog 和文件共享组件 的 API 文档以便在自己的应用程序中使用它们。
-
Yahoo! 的 Flickr 应用程序和站点为我们创建这个相册应用程序提供了很多启发。
-
查阅 Dojo Toolkit。
- 在 Apache 的 HttpClient 上获得更多信息。
-
了解 cURL 工具。
-
查阅 JSON,一种轻量型的数据交换格式。
- 获得有关 Atom 1.0 Syndication Format 概述(developerWorks,2005 年 8 月)的概览。
-
研究 Flickr Uploadr 工具。
-
浏览 developerWorks Web 开发专区 以查找用于开发 Web 2.0 应用程序的工具、代码和资源。
-
developerWorks Ajax 资源中心 包含面向所有技术水平读者的信息,可以帮助您将 Ajax 构建到自己的应用程序中和显著改善您的用户 Web 体验。
获得产品和技术
-
下载 Project Zero 并立即开始应用在本文中所学到的技能。
讨论
-
参与 projectzero.org 论坛。
