级别: 中级 Federico Kereki, 系统工程师, IBM
2009 年 9 月 03 日 对于现代的 Web 2.0 站点而言,若能融合来源各异的信息将无疑会锦上添花。您可以使用 Google Web Toolkit (GWT) 获得并处理基于 XML 的新闻提要,比如 RSS 以及更为现代的 Atom Syndication
Format。在本文中,探索访问任何适当的提要 — 不受同源原则(SOP)的限制 — 以及处理传入 XML 数据所需的方法。
 |
常用缩略语
- Ajax:Asynchronous JavaScript + XML
- API:应用程序编程接口(Application program interface)
- RSS:真正简单的联合(Really Simple Syndication)
- URL:统一资源定位符(Uniform Resource Locator)
- XML:可扩展标记语言(Extensible Markup Language)
|
|
在如今的 Web 2.0 热潮中,您很可能希望在自己的应用程序中使用来自 RSS 或 Atom
提要的数据。通过使用 GWT 和 Asynchronous JavaScript + XML
(Ajax),已经有几种方法可以通过客户端代码获得和处理这类提要。不过,绕过 SOP 这样的浏览器限制无论如何都需要一些临时的解决方法。本文给出了一个简单的 GWT 应用程序,它可以解决提要的获得和处理。本文无意构建一个功能完善的提要阅读器,相反,本文旨在为您指明方向以便您能开始用 RSS 和 Atom 为自己的 Web 应用程序提供提要。
获得提要
首先,考虑如何获得一个提要。通常,答案是应用 Ajax。不过,SOP(参见 SOP 问题)不允许连接 GWT 页面与提要源 — 当然,除非,您的提要源本身就处于您自己的服务器上,但是,那并不怎么有趣!
SOP 问题
同源原则是一种浏览器安全限制,为的是防止从一个源头(即原始 URL 的协议/主机/端口这三者)加载的页面访问来自任何其他源头的数据。( Windows® Internet Explorer® 对于 SOP 比较随意并且会忽视端口更改,但这并不符合标准。)比如,如果某个 Web 页面是从
http://www.yourownsite.com:80/some/page 加载的,那么 SOP 就不会允许它从任何其他的 URL(包括如下所列)获得数据:
- https://www.yourownsite(更改了协议)
- http://othersite(更改了主机)
- http://www.yourownsite.com:8080(更改了端口)
因 SOP 不允许来自一个源头的页面访问、处理和显示来自另一个源头的数据,所以它对安全性很有意义。消除 SOP 对于网页假冒者无疑是个好消息,因为您虽然浏览的是一个来自可信站点的有效页面,但是第三方却有可能监视您的行动以及任何您所发送或接收的数据。SOP 能够确保您接收的任何数据都是由原始的 Web 站点实际发送的;您不能够从任何其他(有可能是可疑的)源头接收数据。
考虑到这个限制,可以有如下两个选项:
- 使用 Ajax 连接到自己服务器上的一个代理来获得提要(如果代码没有在浏览器上运行,就没有这些 SOP 限制)并将其发送回页面。
- 利用一个有趣的与 JavaScript 有关的技术 — 由 Google 的 API
协助 — 允许绕过 SOP 限制。
本文首先探讨代理技术,然后转而讨论 Google
AJAX Feed API 方法,并会进行 Java™ 和
JavaScript 的混合编程。
Web 页面设计
我们从最为基础的页面设计开始。我的页面设计中包括了一个文本框,用于所需提要的 URL,一个列表框,用来选择以何种方法获得字段(在实际中,您不需要给用户这样的选项),一个命令按钮,用来获得数据。在 图 1 中,可以看到一个基础的空白窗口,其中只有一个已经填充好的默认 URL。顺便说一下,这种浏览器,您可能并不熟悉,它是 GWT 自托管模式的基于 Mozilla 的浏览器。当然,在编译并部署了应用程序后,可以使用任何一种浏览器。
图 1. 空白页面
当提要回来时,我不做任何特殊的处理,而只是简单地显示主要的新闻标题、一个简短的描述和一个链接。您可以按您所想,获得尽量多的提要,在新数据显示之前,先前的数据会被删除。图 2 给出了一个实际运行的例子。
图 2. 获取提要所得到的结果
总之,这个程序的总体结构如 清单 1 所示。出于简明的原因,我对每个方法都进行了简短的说明,并借此缩短了需要显示的代码。我稍后会详细介绍每个方法内的代码。参见 下载 获得完整的原始代码。
清单 1. 代码的总体结构
package com.fkereki.rssread.client;
//... "import" lines ...
public class Rssreader implements EntryPoint {
// ... variable definitions...
public void onModuleLoad() {
// set up the form and its fields
// call getFeedViaProxy(...) or getFeedViaGoogle(...)
// depending on the listbox value
}
void getFeedViaProxy(final String feedUrl) {
// connect to the remote server via RPC
// when data arrives, call processAndShowFeed(...)
}
native void getFeedViaGoogle(final String feedUrl) /*-{
// call Google Feed API (using native JavaScript, not Java)
// when data arrives, call processAndShowFeed(...)
}-*/;
void processAndShowFeed(final String xmlDocument) {
// clear results from a previous run, if any
// decide whether it's RSS or Atom, and call
// processRssFeed() or processAtomFeed() as required
}
void processRssFeed(final Element root) {
// navigate a RSS feed, extraction titles, descriptions,
// and links, and using showFeedItem(...) to show them
}
void processAtomFeed(final Element root) {
// navigate an Atom feed, extraction titles, descriptions,
// and links (by using getValueIfPresent(...) and
// getLinkIfPresent(...), and showFeedItem(...) to show them
}
private String getValueIfPresent(final Element el, final String tn) {
// get an XML node, and return the value that corresponds to a certain tag
}
private String getLinkIfPresent(final Element el) {
// given a XML "link" node, return the corresponding address
}
private void showFeedItem(final String title, final String description,
final String link) {
// add some lines to the screen, with the data for the latest news
}
}
|
使用代理
GWT 允许使用远端过程调用(Remote
Procedure Call,RPC)轻松访问服务器端 servlet。为此,必须为客户机端代码编写几个接口,以实际的 servlet 作为服务器端代码。请首先考虑后者:因为您需要这样一个服务,在给定了一个提要 URL 时,它能够连接到该站点、下载其内容、将这些内容发送回给调用程序。(对于 shell 行 parallel,请考虑使用
wget 或 curl 命令。)可以以多种方式实现此目的,清单 2 显示了实现此任务最为简单的一种方法。由于我决定将我的远端代理命名为
RemoteProxy,因此服务器端类必须被称为 RemoteProxyImpl;这里,Impl 代表的是 “Implementation”。
清单 2. GWT 的一个 servlet 代理
package com.fkereki.rssread.server;
//... "import" lines...
public class RemoteProxyImpl
extends RemoteServiceServlet implements RemoteProxy {
//... variable definitions ...
public String getFeed(final String feedUrl) {
String result= "";
try {
final BufferedReader in= new BufferedReader(new InputStreamReader(
new URL(feedUrl).openStream()));
String inputLine;
while ((inputLine= in.readLine()) != null) {
result+= inputLine;
}
in.close();
return result;
} catch (final Exception e) {
return "";
}
}
}
|
要完成整个过程,还需要进行客户端代码编写。根据 GWT 的命名规则,必须实现在 清单 3 内所示的这两个接口:RemoteProxy 和
RemoteProxyAsync。在远端代理返回其值时,需要以 Ajax 的形式将这些接口用于异步回调。
清单 3. RPC 调用的客户端需要如下两个接口
@RemoteServiceRelativePath("remoteProxy")
public interface RemoteProxy extends RemoteService {
public String getFeed(String feedUrl);
}
//
public interface RemoteProxyAsync {
void getFeed(java.lang.String feedUrl,
com.google.gwt.user.client.rpc.AsyncCallback<String> arg2);
}
|
有了上述三段代码,获得一个提要就十分轻松了,如 清单 4 所示:
清单 4. 这些接口被用于 Ajax 形式的回调
void getFeedViaProxy(final String feedUrl) {
final RemoteProxyAsync rp= (RemoteProxyAsync)GWT
.create(RemoteProxy.class);
rp.getFeed(feedUrl, new AsyncCallback<String>() {
public void onFailure(final Throwable caught) {
Window.alert("failure?!");
}
public void onSuccess(final String result) {
processAndShowFeed(result);
}
});
}
|
对于给定的这个提要 URL,我使用 GWT 机制,创建一个合适的类来处理此 RPC 的更为详细的方面(串行化数据、设置回调、实际调用服务器端 servlet、对所接收的答复进行反串行化等)。我还编程实现了这个异步回调以便指定当数据回来时该如何去做:
- 如果发生错误,调用
onFailure 方法。
- 如果调用成功,调用
onSuccess,在本例中,就是处理所接收的 XML 并在屏幕上显示此提要。
使用 Google AJAX Feed
API
SOP 不允许您的代码从其他站点获得数据,但是有一种情况例外:您可以 使用 <script ... /> 标签下载并执行 JavaScript 代码。如果您所下载的代码恰好包含了数据并调用了您的一个函数将这些数据用于好的用途,那么您就已经绕过了 SOP。Google AJAX Feed API 背后的理念如下所列:
- 它使用
<script ... />
标签来调用一个充当代理的 Google 站点。
- 远端站点获得提要数据并将其以 JavaScript 代码的形式返回。
- 所下载的 JavaScript 代码调用函数以便您能处理传入的 XML
由于 Google AJAX Feed API 以 JavaScript 代码编写,所以无需使用 GWT 的 JavaScript Native Interface (JSNI)。首先,如
清单 5 所示,您必须包括
jsapi 脚本。可以直接从 Google 的站点(请确认在这种情况下 SOP 不适用)直接将其包括进来,也可以下载它,然后从服务器将其包括进来。此脚本创建了一个全局 google 对象,您可以稍候使用。正如清单 5 中的 “mandatory initialization” 代码行所示,在使用前,必须初始化这个全局的 google 对象。
清单 5. 具有与 Google AJAX Feed API 相关编码的 Web 页面
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<!-- ... -->
</head>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("feeds", "1"); // mandatory initialization
</script>
<body>
<!-- ... -->
</body>
</html>
|
清单 6 给出了所需的 JSNI 代码。我无意对其中所涉细节做过多介绍,但请注意:
native 访问修饰符用来突出强调 getFeedViaGoogle 方法是以 JavaScript 代码而非 Java 代码编写的
/*-{ 和
}-*/ 分隔符用于方法实现
- 全局
$wnd 变量用来访问 google 变量(GWT 在一个子 iframe 中加载所编译的应用程序代码,所以任何想要在主窗口直接进行访问的企图都会失败。)
- 针对所接收的 XML 文档,有一个
serializeToString 参数,因为
processAndShowFeed 方法需要一个
String 参数
- 调用此 Java 方法所需的语法多少有点令人费解
- 因闭包的需要,使用
myself 变量(而不是直接使用 this),参见 参考资料
清单 6. 借助 JSNI 来用 Google AJAX Feed API 获得一个提要
native void getFeedViaGoogle(final String feedUrl) /*-{
var myself= this;
var feed= new $wnd.google.feeds.Feed(feedUrl);
feed.setResultFormat($wnd.google.feeds.Feed.XML_FORMAT);
feed.load(function(xmlResult) {
myself.@com.fkereki.rssread.client.Rssreader::processAndShowFeed(Ljava/lang/String;)
((new XMLSerializer()).serializeToString(xmlResult.xmlDocument));
});
}-*/;
|
若不考虑 JSNI 的细节,这段代码其实十分直白:我创建一个 feed 对象以便指定所要获得的 URL、将结果格式设置为 XML、然后调用 load 函数,给它一个回调函数来将这个提要(已转变为字符串)传递给 processAndShowFeed 方法。
使用提要
由于 RSS 和 Atom 提要都是一些 XML 文档,因此可以简单地使用 GWT 的
XMLParser。若具有一个非空的 XML 字符串,可以解析它并到达其根元素。判断它是 RSS 还是 Atom 的一种最为简便的做法是看它的根节点名称:如果是 RSS,则为 rss,如果是 Atom,则为 feed。如果都不匹配那就说明有错误发生。最后,调用合适的方法来遍历这个 XML 文档并挑选出它的某些部分进行显示,如 清单 7 所示。
清单 7. 显示一个提要
void processAndShowFeed(final String xmlDocument) {
if (xmlDocument.isEmpty()) {
// warn about the problem; most likely, a wrong URL
} else {
final Document xmlDoc= XMLParser.parse(xmlDocument);
XMLParser.removeWhitespace(xmlDoc);
final Element root= xmlDoc.getDocumentElement();
// clear out the previous feed results, if any,
// to make space for the feed that is to be loaded
if (root.getNodeName().equals("rss")) {
processRssFeed(root);
} else if (root.getNodeName().equals("feed")) {
processAtomFeed(root);
} else {
// warn about the unknown feed format
}
}
}
|
清单 8 给出了这个 RSS 提要(稍做缩减)。
清单 8. 稍做缩减的 RSS 新闻提要
<?xml version="1.0" encoding="ISO-8859-1"?>
<rss xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
<channel>
<title>CNN.com - Technology</title>
<link>http://edition.cnn.com/TECH/?eref=edition_technology</link>
<description>CNN.com delivers up-to-the-minute news ...</description>
<language>en-us</language>
<copyright>© 2009 Cable News Network LP, LLLP.</copyright>
<pubDate>Thu, 16 Apr 2009 20:56:04 EDT</pubDate>
<ttl>10</ttl>
... several more channel properties
<item>
<title>YouTube orchestra wows Carnegie Hall</title>
<link>http://rss.cnn.com/~r/rss/edition_technology/~3/XxF062aMfCI/index.html</link>
<description>The YouTube and Carnegie Hall generations ...</description>
<pubDate>Thu, 16 Apr 2009 15:28:47 EDT</pubDate>
</item>
<item>
... another news item...
</item>
<item>
... yet another item...
</item>
</channel>
</rss>
|
用来处理 RSS 和 Atom XML 提要的方法实际上非常类似。通常,我使用
getElementsByTagName 来获得所有新闻节点(如果是 RSS,则为 item,如果是 Atom,则为 entry)。请注意在 RSS 内,必须要更向下一级,跳过中间的 channel 节点,如
清单 9 所示)。然后,我再逐一地检查每个新闻条目,提取并显示
title、description(如为 Atom,还会显示 summary)和
link。getValueIfPresent 方法适用于标题和描述。getLinkIfPresent 方法则可处理 RSS 和 Atom 之间的差异:在 RSS 中,节点值就是实际的链接,而在 Atom 中,则需要使用 href 属性。
清单 9. 用来处理 RSS 提要的部分代码
void processRssFeed(final Element root) {
final NodeList items=
((Element)root.getElementsByTagName("channel").item(0)).
getElementsByTagName("item");
for (int i= 0; i < items.getLength(); i++) {
final Element item= (Element)items.item(i);
final String rssDescription= getValueIfPresent(item, "description");
final String rssTitle= getValueIfPresent(item, "title");
final String rssLink= getLinkIfPresent(item);
// display rssTitle, rssDescription and rssLink onscreen
}
}
private String getValueIfPresent(final Element el, final String tn) {
final NodeList nl= el.getElementsByTagName(tn);
if (nl.getLength() == 0) {
return "";
} else {
return nl.item(0).getFirstChild().getNodeValue();
}
}
private String getLinkIfPresent(final Element el) {
final NodeList nl= el.getElementsByTagName("link");
if (nl.getLength() == 0) {
return "";
} else {
if (nl.item(0).hasChildNodes()) {
return nl.item(0).getFirstChild().getNodeValue();
} else {
return ((Element)nl.item(0)).getAttribute("href");
}
}
}
|
这里有一点需要提醒您。用来处理 Atom 提要的代码更倾向于假设 summary 是一些可显示的文本。而更安全的做法是通过查看 type 属性对此进行确认,以防其中有些内容不能显示,比如 XML、按 base64 编码的二进制内容或者指向其他内容的指针。由于出现上述内容的可能性很小,我就将代码保持得尽量简单,但如果想要编程实现一个真正通用的 Atom 阅读器,请务必小心!
结束语
本文展示了从 GWT Web 页面获得和处理
RSS 和 Atom 提要的两种不同的方法。在本文中,您看到了通过直接处理 XML 提要本身,代码就能够提供基础的提要处理。扩展这些代码来提供更宽泛的处理并不复杂,而您得到的却是为 Web 页面使用任何提要的机会。尝试一下吧!
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 本文的示例 GWT 代码 | rssreader.tar.gz | 4305KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | 
|  | Federico Kereki 是一名来自乌拉圭的系统工程师,拥有超过 20 年的开发、咨询和大学教学的经验。目前,他正在与各种各样的缩略词打交道:SOA、GWT、Ajax、PHP,当然还有 FLOSS! |
对本文的评价
|