希腊哲学家亚里士多德曾经写到:“通往失败的道路有许多条……,但通往成功的道路仅有一条。” 遗憾的是,亚里士多德并不是一名计算机程序员。
虽然亚里士多德的第一个论断符合编程的特点 — “通往失败的道路有许多条” — 他的第二个推测却没有一点依据。
本系列文章针对同一问题运用了四种方法。每种方法都被证明是正确的 — 每种方法都有各自的优缺点。它们要解决的问题并不复杂,同样,解决方案也不复杂。尽管如此,这些方法涉及了大量 “权衡”,可以包含到非常简单的解决方案中。
|
要定义需要解决的问题,我列出了以下的问题说明:
构建一个 Ajax 库读取从国家气象服务(National Weather Service)获得的最新观测数据,然后提取一部分数据并转换为 HTML 格式,从而创建一个天气预报面板。
许多 Web 站点喜欢在 Web 页面上显示当地的天气状况。为此,他们需要访问最新的气象信息。那么如何获得这些数据呢?
在美国,National Weather Service (NWS) 提供大量的天气信息。其中包括美国数百个城市的当前天气观测数据。该数据可使用 RSS 或 XML 格式。
在 Ajax 中,字母 X 代表 XML,所以 NWS 数据似乎非常适合 Ajax 方法。
|
本系列文章共考察了四种不同的方法,用于构建一个 Ajax 天气预报面板 — 一个小的信息框,包含 NWS 监测的任何城市或城镇的气象信息。其设计目标是
- 简单性
- 易于重用
我将利用这四种方法来展示各个方法内在的 “权衡” 问题。任何一种方法都不存在绝对的对与错。
本质上讲,每种方法的实现截然不同,如 表 1 所描述。
方法 | 描述 |
---|---|
1:遍历 DOM 树 | 服务器上简单的 Web 代理从 NWS 服务器拉出数据并发送到浏览器。在浏览器内,JavaScript 解释器从返回的 responseXML DOM 树提取部分数据,添加一些 HTML 格式,然后将其插入到页面中的 DIV 标记。 |
2:服务器上的 XSLT | 一个服务器端脚本从 NWS 服务器拉出数据,使用 XSLT 将数据由 XML 转化成 HTML 格式,然后将 HTML 代码片段发回浏览器。浏览器随后将代码片段插入到一个 DIV 标记。 |
3:客户端 XSLT | 该方法使用一个简单的 Web 代理(同方法 1)将 XML 数据发送回浏览器。与方法 1 不同的是,使用客户端 XSLT 将 XML 转换为 HTML,并将其插入到一个 DIV 标记。 |
4:JSON 和动态脚本标记 | 一个外部服务(Yahoo! Pipes)将 NWS 数据从 XML 转换为 JavaScript Object Notation (JSON)。天气预报面板库利用 JSON 的特殊能力和 JavaScript 语言将转换后的数据拉回到浏览器 — 避免了对代理的需求。 |
这四种构建可重用 Ajax 天气预报面板的方法均共享以下元素:
- 管道(pipeline)方法
- 一个简单的 Ajax 库
weather_badge()
JavaScript 函数- National Weather Service 数据
数据管道的概念至少可以回溯到 UNIX® 的早期。在这个模型中,数据进入管道,然后在其中经历一系列过滤器。每个过滤器以某种方法转换数据。经转换后的数据被送回到管道,也有可能进行进一步的转换,直至完成所有转换为止。管道的终点可能是一个用户终端、重定向到一个文件或另一个程序。
这种方法能够有效处理从 Web 上获得的基于 XML 的数据和服务。程序能够从 Web 获取 XML 数据,发送至管道,并且将一系列转换链接以提取数据并重新格式化。
不同于 UNIX 命令行中的管道和过滤器,这种用于 Ajax 应用程序的方法要求管道横跨网络内多台计算机。 XML 数据可以源自一台 Web 服务器,然后被传送到另一个域内的不同服务器,最终到达目的地:用户的 Web 浏览器。
有关 Ajax 的介绍超出了本文的范围,但是可以获取优秀的初级读本(参见 参考资料)。
为使本系列尽可能涵盖更多的读者,本文的示例使用了一个小型 Ajax 库,如 清单 1 所示。这个库围绕 XMLHttpRequest
对象只提供了最少的修饰 — 刚好消除各个主要浏览器中的 XMLHttpRequest
差异。
清单 1. ajax-simple.js — 示例使用的最小的 Ajax/XMLHttpRequest
库
this.url = url;
this.parms = parms;
this.method = method;
this.callback = callback;
this.async = true;
this.create ();
this.req.onreadystatechange = this.dispatch (this);
}
Ajax.prototype.dispatch = function (ajax) ... {
function funcRef()
...{
if (ajax.req.readyState == 4) ...{
if (ajax.callback) ...{
ajax.callback (ajax.req);
}
}
}
return funcRef;
}
Ajax.prototype.request = function () ... {
if (this.method == "POST") ...{
this.req.open("POST", this.url, this.async);
this.req.send (this.parms);
}
else if (this.method == "GET") ...{
this.req.open("GET", this.url + this.parms, this.async);
this.req.send (null);
}
}
Ajax.prototype.setAsync = function (async) ... {
this.async = async;
}
Ajax.prototype.create = function () ... {
var xmlhttp;
/**//*@cc_on
@if (@_jscript_version >= 5)
try {
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
}
catch (e) {
try {
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (E) {
xmlhttp = false;
}
}
@else
xmlhttp = false;
@end @*/
if (!xmlhttp && typeof XMLHttpRequest != 'undefined') ...{
try ...{
xmlhttp = new XMLHttpRequest();
} catch (e) ...{
xmlhttp = false;
}
}
this.req = xmlhttp;
}
四种方法都具有一个在 Web 页面添加天气预报面板的接口。该接口是一个单一的 JavaScript 函数:weather_badge()
。该函数需要 2 个参数:一个用于识别相关城市和城镇的 NWS 站点 ID 和一个 HTML DIV 标记的元素 ID。这个 DIV 标记是呈现天气预报面板的目标。图 1 是一个 Ajax 天气预报面板的示例。
天气预报面板是使用 HTML 呈现的,但是你可以使用级联样式表(CSS)控制它的许多外观元素,包括字体、背景颜色和边框。
清单 2 演示如何在 Web 页面中嵌入天气预报面板。这里从一个 JavaScript onload
事件处理程序中调用 weather_badge()
函数。
< head >
< title > Apache Proxy Example </ title >
< link rel ="stylesheet" type ="text/css" href ="weather.css" />
< script language ="Javascript" src ="ajax-simple.js" ></ script >
< script language ="Javascript" src ="weather_badge_apache_proxy.js" > ...
</ script >
< script > ...
function init () ...{
weather_badge ("KAKQ", "target1");
}
</ script >
</ head >
< body onload ="init();" >
< h3 > Apache Proxy Example </ h3 >
< div class ="wbadge" id ="target1" >
Loading...
</ div >
</ body >
</ html >
National Weather Service 站点使用一个站点 ID 用于识别读取天气数据的城市、城镇或其他位置。站点 ID 是一个由四个字符组成的惟一编码。
所有 NWS 当前观测数据的基 URL 是:
http://www.nws.noaa.gov/data/current_obs/ |
基 URL 加上由四个字符组成的站点 ID,提供了一个气象数据 URL。例如 Richmond, Virginia 的站点 ID 是 KRIC
。Richmond 的天气数据的 URL 就是:
http://www.nws.noaa.gov/data/cuurent_obs/KRIC.xml |
一个简单的 XML 格式定义了当前观测数据,如 清单 3 所示。
清单 3. Richmond, Virginia 的 National Weather Service XML 数据
xsi:noNamespaceSchemaLocation =
"http://www.weather.gov/data/current_obs/current_observation.xsd" >
< credit > NOAA's National Weather Service </ credit >
< credit_URL > http://weather.gov/ </ credit_URL >
< image >
< url > http://weather.gov/images/xml_logo.gif </ url >
< title > NOAA's National Weather Service </ title >
< link > http://weather.gov </ link >
</ image >
< suggested_pickup > 15 minutes after the hour </ suggested_pickup >
< suggested_pickup_period > 60 </ suggested_pickup_period >
< location > Richmond International Airport, VA </ location >
< station_id > KRIC </ station_id >
< latitude > 37.51 </ latitude >
< longitude > -77.31 </ longitude >
< observation_time >
Last Updated on Dec 11, 12:54 pm EST
</ observation_time >
< observation_time_rfc822 >
Tue, 11 Dec 2007 12:54:00 -0500 EST
</ observation_time_rfc822 >
< weather > Overcast </ weather >
< temperature_string > 54 F (12 C) </ temperature_string >
< temp_f > 54 </ temp_f >
< temp_c > 12 </ temp_c >
< relative_humidity > 80 </ relative_humidity >
< wind_string > From the South at 5 MPH </ wind_string >
< wind_dir > South </ wind_dir >
< wind_degrees > 180 </ wind_degrees >
< wind_mph > 4.6 </ wind_mph >
< wind_gust_mph > NA </ wind_gust_mph >
< pressure_string > 30.31" (1026.7 mb) </ pressure_string >
< pressure_mb > 1026.7 </ pressure_mb >
< pressure_in > 30.31 </ pressure_in >
< dewpoint_string > 48 F (9 C) </ dewpoint_string >
< dewpoint_f > 48 </ dewpoint_f >
< dewpoint_c > 9 </ dewpoint_c >
< heat_index_string > NA </ heat_index_string >
< heat_index_f > NA </ heat_index_f >
< heat_index_c > NA </ heat_index_c >
< windchill_string > 53 F (12 C) </ windchill_string >
< windchill_f > 53 </ windchill_f >
< windchill_c > 12 </ windchill_c >
< visibility_mi > 7.00 </ visibility_mi >
< icon_url_base >
http://weather.gov/weather/images/fcicons/
</ icon_url_base >
< icon_url_name > ovc.jpg </ icon_url_name >
< two_day_history_url >
http://www.weather.gov/data/obhistory/KRIC.html
</ two_day_history_url >
< ob_url > http://www.nws.noaa.gov/data/METAR/KRIC.1.txt </ ob_url >
< disclaimer_url > http://weather.gov/disclaimer.html </ disclaimer_url >
< copyright_url > http://weather.gov/disclaimer.html </ copyright_url >
< privacy_policy_url > http://weather.gov/notice.html </ privacy_policy_url >
</ current_observation >
天气预报面板仅需要这些数据的一小部分。我将使用的值位于 location
、weather
、icon_url_base
、icon_url_name
、temperature_string
、wind_string
、relative_humidity
、visibility_mi
和 observation_time
元素内。
使用这种方法必须首先解决一个对 XMLHttpRequest
对象(由 Ajax 程序使用)的基本限制:域相同的问题。
出于安全考虑,一个 XMLHttpRequest
调用只能够发起传递初始 Web 页面的服务器请求。除非我为 National Weather Service 工作,否则我的服务器一定在他们的域之外(www.nws.noaa.gov)。图 2 展示了第一种方法的数据管道。
如果可以访问 Web 服务器的配置,有一个简单的解决方法:使用一个 Web 代理。
Web 代理 指将一个服务器请求重定向到另一台服务器。 我需要 Ajax 程序在我的服务器上请求一个资源,然后将该请求转换为在 NWS 上的请求。这样就解决了 “相同域” 的问题:Ajax 程序与它所在的服务器通信,后者将这个请求秘密地重定向到 NWS 服务器。
在 Apache Web 服务器上,代理是通过一个 ProxyPass
规则实现的。该规则的语法非常简单:
ProxyPass our_directory their_url |
第一个选项引用的是我的服务器上一个不存在的位置,而第二个选项是一个远程服务器上的 URL。当一个请求进入 our_directory
,请求被 Apache 重定向到 their_url
。请求者(我的 Ajax 程序)永远不会察觉到这个重定向。
这就是用于实现 National Weather Service 数据访问的代理规则:
ProxyPass /nws_currobs/ http://www.nws.noaa.gov/data/current_obs/ |
若要得到关于 Richmond,Virginia 的数据,则请求这个 URL:
/nws_currobs/KRIC.xml |
Apache 将请求转换成对 NWS 的请求:
http://www.nws.noaa.gov/data/current_obs/KRIC.xml |
在 Ajax 应用程序中,如果一个请求 XML 数据的服务器请求是成功的,responseXML
属性将被初始化。这个对象属性包含检索后的 XML,被解析为一个 XMLDocument 类型的 DOM 树(如果服务器数据不是有效的 XML,或者,在某些浏览器上,如果返回的数据没有伴随一个 text/xml 或 application/xml HTTP Content-type 报头,则不会创建 responseXML
属性。在这些情况下,responseText
属性包含由服务器返回的未处理过的文本)。
利用 responseXML
,我可以遍历 DOM,从返回的 XML 中提取值。清单 4 显示对返回的 XML 进行删减后的版本:
xsi:noNamespaceSchemaLocation =
"http://www.weather.gov/data/current_obs/current_observation.xsd" >
< location > Richmond International Airport, VA </ location >
< observation_time >
Last Updated on Dec 11, 12:54 pm EST
</ observation_time >
< weather > Overcast </ weather >
< temperature_string > 54 F (12 C) </ temperature_string >
< relative_humidity > 80 </ relative_humidity >
< wind_string > From the South at 5 MPH </ wind_string >
< visibility_mi > 7.00 </ visibility_mi >
< icon_url_base >
http://weather.gov/weather/images/fcicons/
</ icon_url_base >
< icon_url_name > ovc.jpg </ icon_url_name >
</ current_observation >
现在我可以从 responseXML
提取 wind_string
。 responseXML
属性是一个 XMLDocument 类型。XMLDocument 的 documentElement
属性返回我的 XML DOM 树的顶层元素。要在程序中进行检验,在代码中插入一个 alert()
函数:
alert ("tagName: " + req.responseXML.documentElement.tagName); |
在执行时,alert()
弹出一个窗口,包含下面的内容:
tagName: current_observation |
若访问 current_observation
下的各个元素,使用 getElementsByTagName()
。这个 Element 方法接受一个标记名参数,并返回一个包含所有子 Element 节点的数组,其名称与元素名相同。在一个 JavaScript 程序中,我可以编写以下代码
var elements = req.responseXML.documentElement.getElementsByTagName("wind_string"); |
NWS XML 数据仅包括一个 wind_string
元素,因此完全可以认为我需要的数据位于第一个元素中。wind_string
元素标记内的实际文本可以按以下方式访问:
elements[0].firstChild.data |
从一个 XML 文档中提取单一值非常困难,尤其是当文档拥有一个简单的结构时。很明显,这种从 XML 提取数据的方法很快会变得难以处理。如果我把上述提到的所有步骤组合到一个 DOM 引用中,它看起来类似下面的内容:
req.responseXML.documentElement.getElementsByTagName("wind_string")[0].firstChild.data |
对于这个应用程序,我可以定义一个 JavaScript helper 函数来提取值,使代码更具可读性,如 清单 5 所示。
function get_element (doc_el, name, idx) { var element = doc_el.getElementsByTagName (name); return element[idx].firstChild.data; } |
有了该函数后,weather_badge()
函数变得更易于管理,如 清单 6 所示。
清单 6. weather_badge()
函数使用一个 Apache 代理检索 XML
var ajax = new Ajax
("/nws_currobs/" + nws_id + ".xml",
"",
"GET",
function (req) ...{
var doc_el = req.responseXML.documentElement;
// Extract values from XML structure returned by
// by Ajax (XMLHttpRequest) call.
var location = get_element (doc_el, "location", 0);
var temperature_string = get_element (doc_el, "temperature_string", 0);
var weather = get_element (doc_el, "weather", 0);
var icon_url_base = get_element (doc_el, "icon_url_base", 0);
var icon_url_name = get_element (doc_el, "icon_url_name", 0);
var wind_string = get_element (doc_el, "wind_string", 0);
var relative_humidity = get_element (doc_el, "relative_humidity", 0);
var visibility_mi = get_element (doc_el, "visibility_mi", 0);
var observation_time = get_element (doc_el, "observation_time", 0);
var div = document.getElementById ("target1");
div.innerHTML =
"<center> "
+ "<b>" + location + "</b><br> "
+ weather + "<br>"
+ "<img border='0' src='"
+ icon_url_base + icon_url_name + "'/><br> "
+ temperature_string + "<br> "
+ "Wind: " + wind_string + "<br> "
+ "Humidity: " + relative_humidity + "<br> "
+ "Visibility: " + visibility_mi + "<br> "
+ "<br><span style='font-size: 0.8em; font-weight: bold;'>"
+ observation_time + "</span><br> "
+ "</center> ";
}
);
ajax.request ();
}
代码创建了一个 Ajax 对象(请回忆一下,这仅是一个围绕 XMLHttpRequest
对象的小型包装器)。Ajax 构造函数采用四个参数,如 表 2 所描述。
参数 | 描述 |
---|---|
url | 用于远程资源的 URL — 在本文示例中,National Weather Service XML 文件通过我的服务器进行代理。 |
parms | 一个包含任意 URL 参数的字符串。我请求的是一个静态的 XML 文档,而不是一个服务器端脚本,所以不需要任何参数。 |
method | 该参数通知 Ajax 生成一个 HTTP GET 请求。 |
callback | 这个参数定义在 XML 文档返回到浏览器时由 Ajax 调用的 callback 函数。在本文中,我从 XML 提取值,然后使用一些 HTML 格式化标记将它们拼接到一起,创建了一个 HTML 代码片段。这个代码片段定义了我的天气预报面板。 |
我使用 innerHTML
属性将 HTML 代码片段填入到 Web 页面中。目标 DIV 标记已经被作为 div_name
参数传入 weather_badge()
。现在,可以非常轻松地插入我的 HTML 代码片段:
var div = document.getElementById (div_name); div.innerHTML = html_snippet; |
解决编程问题的任何方法都需要进行权衡。表 3 列举了这种方法的优缺点。
优点 | 缺点 |
---|---|
适用于所有主要的浏览器。 不要求附加的库或第三方工具。 | 语法不规则使得难于访问 XML 元素,即使对简单的 XML 文档也是如此。 |
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
本系列的样例代码 | x-xmlajax.zip | 194KB | HTTP |