在本系列的 第 8 部分 中,我们为 Chomp 应用程序添加了来自 Yahoo!、Amazon 和 Flickr 的结果。现在,我们要在用户发出请求时,仅装载用户请求的数据,通过这样的方式提高性能。但首先让我们来看一下这篇文章处在整个系列的什么位置。
这个共分九部分的 “理解 Zend 框架” 系列按顺序记录了构建在线提要阅读器 Chomp 的过程,同时对使用近期引入的开放源码的 PHP Zend 框架的主要方面进行了解释。
在 第 1 部分 中,我们讨论了 Zend 框架的全部概念,包括一系列相关的类和对 MVC 模式的总体探讨。在 第 2 部分 中,我们详述了这部分内容以展示如何在 Zend 框架应用程序中实现 MVC 模式。我们还创建了用户注册和登录过程,将用户信息添加到数据库中并重新获取这些信息。
第 3 和第 4 部分处理实际的 RSS 和 Atom 提要。在 第 3 部分 中,我们使用户能够订阅独立的提要并显示列于这些提要中的条目。还讨论了 Zend 框架的一些表单处理功能,如验证数据和清除提要条目等。而 第 4 部分 则解释了如何创建代理以从不含提要的站点中提取数据。
本系列的其余各部分涉及到为 Chomp 应用程序增值的相关内容。第 5 部分 中介绍了如何使用 Zend_PDF 模块,允许用户为已保存的文章、图像及搜索结果创建一个定制的 PDF。在 第 6 部分 中,我们使用 Zend_Mail 模块提醒用户有新文章。第 7 部分 探讨了搜索已保存内容并返回排列好的结果。在 第 8 部分 中,我们将 Amazon.com、Yahoo! 和 Flickr 的在线资源链接到应用程序以创建一个健壮的混合体,从而为提要阅读器增加了一个额外的维度。
本文是系列的最后一篇文章,我们将使用 Ajax 及 Zend 框架的 Zend_Json 组件,以所请求的特定信息自动地对搜索结果页面进行局部更新。
通常,网上冲浪者通过单击链接来获取更多的信息。大多数情况下,这将导致替换并重新装载整个页面,即便是该页面的大部分内容并无变化。无论如何,毕竟有所变动。
很多时候,仅仅因为要更新一个侧栏就替换整个页面是不合理的。如果保持页面不动,而仅仅添加或更改原有信息,会使该过程变得相当方便。为此,开发人员综合使用了 HTTP、JavaScript 和 XML(在某些情况下)。多年以来,这种编程混合体的使用一直比较含糊,但它有一个很酷的名字 Ajax(表示 Asynchronous JavaScript and XML)并逐渐成为一种现象。Ajax 处理涉及到浏览器在后台发送一个 HTTP 请求。接收到数据时,就将数据添加到页面中(通常作为一个 div 元素的内容)。
尽管总体而言 XML 非常适合于 Web 服务请求,但并非总是在浏览器中执行此类请求的最佳方法。JavaScript 在内部将对象表示为简单字符串,而我们可以轻松地通过手动方式创建和解释这样的字符串。例如,您可能会创建如下对象。
清单 1. 创建一个对象
var theFeed = new Object(); theFeed.title = "Chaos Magnet"; theFeed.url = "http://feeds.feedburner.com/ChaosMagnet"; |
也可使用如下 JSON 字符串表示。
{"title":"Chaos Magnet",
"url":"http://feeds.feedburner.com/ChaosMagnet"}
|
这似乎不是什么大问题,但请考虑一下这样一个事实:实际上,每种编程语言都具有一个将其本地对象转换成为这种形式的例程。此功能使我们可创建一个 JavaScript 对象,将其作为 JSON 字符串发送给一个 PHP 例程,此例程将该对象转换为一个本地的 PHP 对象。这个 PHP 对象随后又被转换回 JSON 字符串,该字符串作为 Web 服务的结果返回给 JavaScript,JavaScript 再将其转换回本地 JavaScript 对象。
我们甚至能够将这些字符串作为 Ajax 请求的方式来发送和接收。但首先,我们需要对 Chomp 应用程序略加重构。
事实上,当用户执行一个搜索时,FeedController 中的 viewSearchResults 行为不仅创建搜索结果,还会执行对 Amazon 和 Flickr Web 服务的请求,并将其全部嵌入单个视图中。与此相关的问题之一就是性能,因为 Web 服务请求往往较慢,将两个请求包含在同一个页面中必然会对速度造成负面影响。
创建 Ajax 请求的第一步是将这个页面拆分为独立的几部分。例如,我们可以将 Amazon 和 Flickr 结果从主视图 viewedSearchResults.php 中移至其各自的文件 amazonView.php 和 flickrView.php 中。
清单 2. amazonView.php
<h4>Were you looking for any of these books?</h4>
<ol>
<?php
foreach ($this->amazonHits as $result) {
echo "<li><img src='" .
$result->SmallImage->Url->getUri() .
"' width='" . $result->SmallImage->Width .
"' height='" . $result->SmallImage->Height . "' /> ";
echo "<a href='" . $result->DetailPageURL . "' title='" .
htmlentities($result->Title, ENT_QUOTES). " at Amazon.com'>";
echo "<strong>" . htmlentities($result->Title) .
"</strong></a>";
echo " (Ranked #" . $result->SalesRank . ")</li>";
}
?>
</ol>
|
这段代码与原来的主视图中实现同一功能的代码完全相同。我们只是将其移动到单独的文件中。对于 Flickr 的结果也可以进行同样的处理(参见清单 3)。
清单 3. flickrView.php
<table>
<caption>Photos from <a
href="http://flickr.com/">Flickr</a></caption>
<tbody>
<?php
foreach ($this->flickrHits as $index=>$result) {
// Begin column
if ( $index % $this->columns == 0 ) {
echo '<tr>';
}
$thumbnail = '<img src="' . $result->Square->uri .
'" width="75" height="75" />';
echo '<td><a href="' . $result->Large->clickUri .
'" title="to Flickr">' . $thumbnail . '</a><br />';
echo '<small>by <a href="http://www.flickr.com/photos/' .
$this->escape($result->ownername) .
'" title="Owner">' . htmlentities($result->ownername) .
'</a> on ' . $result->datetaken .
'</small></td>';
// Close column
if ( $index % $this->columns == $this->columns - 1 ) {
echo '</tr>';
}
}
?>
</tbody>
</table>
|
将这两个文件(amazonView.php 和 flickrView.php)放到视图目录中。
当然,我们并不想彻底失去移出此页面的数据。而是希望显示呈现这些独立视图的结果。为此,可以将所呈现的文本包含进来(像包含其他数据一样)。
清单 4. 简化 viewSearchResults.php
...
$title = $feedTitle;
if($channelTitle != '')
$title = "$title > $channelTitle";
echo "<tr><td>#" . $i++ . ":</td>";
echo "<td><a href=\"$url\">$title</a></td>";
echo "<td>$score</td></tr>";
}
?>
</table>
<?php echo($this->renderedAmazon) ?>
<?php echo($this->renderedFlickr) ?>
</body>
</html>
|
为使此数据可用,我们需要更新 FeedController。
清单 5. 更新 FeedController.php
public function viewSearchResultsAction()
{
...
$view = Zend::registry('view');
$view->title = "Search Results for: $query";
$view->hits = $hits;
$amazonView = Zend::registry('view');
require_once 'Zend/Service/Amazon/Query.php';
$key = 'YOUR_AMAZON_KEY_HERE';
$amazonQuery = new Zend_Service_Amazon_Query($key);
$amazonQuery->Category('Books')
->Keywords($filterGet->getRaw('query'))
->ResponseGroup('Medium');
$amazonView->amazonHits = $amazonQuery->search();
$view->renderedAmazon = $amazonView->render('amazonView.php');
$flickrView = Zend::registry('view');
$key = 'f50c3c5b6384493f20e69b70b9ff7d29';
$flickrQuery = new Zend_Service_Flickr($key);
$tags = explode(' ', $filterGet->getRaw('query'));
$flickrView->flickrHits = $flickrQuery->tagSearch($tags);
$view->renderedFlickr =
$flickrView->render('flickrView.php');
echo $view->render('viewSearchResults.php');
}
|
在这里,我们创建了两个新的 view 对象,并用之前发送给主视图的数据呈现它们。这个呈现过程仅提供文本,所以我们可以将所呈现的文本设置为主视图中的一项属性。
结果将得到一个与原有页面极其相似的页面,如图 1 所示。
图 1. 更改后的结果
区别在于,现在开始,我们可以操纵使用这些独立数据的方式了。
下一步是将数据从视图中彻底移除,替换以可随需调用的链接。为此,需要编辑 viewedSearchResults.php 文件,如下所示。
清单 6. 用链接替换内容
...
echo "<tr><td>#" . $i++ . ":</td>";
echo "<td><a href=\"$url\">$title</a></td>";
echo "<td>$score</td></tr>";
}
?>
</table>
<center><p>
<a href="javascript:getMashup('amazon')"
target="_blank">Show related books</a>
|
<a href="javascript:getMashup('flickr')"
target="_blank">Show related photos</a>
</p></center>
</body>
</html>
|
这些链接现在还不是有效的。稍后我们将构建 getMashup() 函数,但若您重新装载搜索页面,将看到如图 2 所示的结果。
图 2. 显示新链接
创建用于请求新数据的 JavaScript 函数之前,需要首先创建该函数将调用的行为。在本例中,我们创建了一个新行为 mashupAction,该行为接受混合体的类型及查询,并返回合适的数据,如下所示。
清单 7. 将新行为添加到 FeedController.php
...
public function viewSearchResultsAction()
{
...
$view = Zend::registry('view');
$view->title = "Search Results for: $query";
$view->hits = $hits;
$view->query = $query;
echo $view->render('viewSearchResults.php');
}
public function mashupAction()
{
$filterGet = Zend::registry('fGet');
$type = $filterGet->getRaw('type');
if ($type == 'amazon'){
$amazonView = Zend::registry('view');
require_once 'Zend/Service/Amazon/Query.php';
$key = 'YOUR_AMAZON_KEY_HERE';
$amazonQuery = new Zend_Service_Amazon_Query($key);
$amazonQuery->Category('Books')
->Keywords($filterGet->getRaw('query'))
->ResponseGroup('Medium');
$amazonView->amazonHits = $amazonQuery->search();
echo $amazonView->render('amazonView.php');
}
if ($type == 'flickr'){
$flickrView = Zend::registry('view');
$key = 'YOUR_FLICKR_KEY_HERE';
$flickrQuery = new Zend_Service_Flickr($key);
$tags = explode(' ', $filterGet->getRaw('query'));
$flickrView->flickrHits = $flickrQuery->tagSearch($tags);
echo $flickrView->render('flickrView.php');
}
}
...
|
首先,请注意我们从 viewSearchResultsAction 中移除了额外的信息。随后创建了 mashupAction,该行为使用 GET 过滤器来接受 type 和query 这两个参数。该行为根据 type 的值来呈现 Amazon 或 Flickr 视图。
可以通过将浏览器指向 http://localhost/feed/mashup?type=flickr&query=pentagon 或一个有着类似查询的 URL 来查看这个行为。结果与图 3 类似。
图 3. 显示新混合体行为
还可将 type 参数改为 amazon 来查看其他数据。我们将在搜索结果页面中包含这一数据,但仅包含需要的数据。
现在我们已经做好了准备,可以将 Ajax 结合进来了。
向页面添加 Ajax 的过程曾经非常痛苦。仅浏览器差别一项就足以使众多的开发人员汗颜。幸运的是,这一过程已经被大量不同的免费开源工具所简化。在本文中,我们要使用 Prototype 框架。从 http://prototype.conio.net/ 下载 “just the JavaScript”,并将其保存到 Web 服务器的文档根目录中。在本文写作时,最新的版本是 1.4.0。
可以通过将这个库包含到页面中并引用 Ajax 对象来使用该库,如下所示。
清单 8. 将 Ajax 添加到 viewSearchResults.php 中
<html>
<head>
<script src="/prototype.js"></script>
<title><?php echo $this->escape($this->title);
?></title>
<script type="text/javascript">
function getMashup(mashupType){
var url = 'http://localhost/feed/mashup';
var myAjax = new Ajax.Request
(
url,
{
method: 'get',
parameters:
'type='+mashupType+'&query=<?php echo $this->query
?>',
onSuccess: renderResults
}
);
}
function renderResults(response){
var renderDiv = document.getElementById('mashupResults');
renderDiv.innerHTML = response.responseText;
}
</script>
</head>
<body>
[<a href='/'>Back to Main Menu</a>]<br>
<h1><?php echo $this->escape($this->title); ?></h1>
...
<center><p>
<a href="javascript:getMashup('amazon')">Show related books</a>
|
<a href="javascript:getMashup('flickr')">Show related photos</a>
</p></center>
<div id="mashupResults"></div>
</body>
</html>
|
首先,我们将该库添加到页面中。(为便于维护,我将其改名为 prototype.js。)将其放至文档根目录下使我们无需硬编码实际的位置就能引用该库。
接下来,创建 getMashup() 函数,用以创建新的 Ajax.Request。此请求接受作为实参的 URL(请注意它指向的是 FeedController 的混合体行为)和一组形参,其中包括使用哪个方法(GET 或 POST)、随请求一起发送的参数,以及在响应返回时要执行的 JavaScript 行为。
在本例中,执行该行为的是 renderedResults() 函数,该函数接受 HTTP 响应作为参数。该函数只是获取了我们添加的新 div 元素及 mashupResults 的一个引用,并将其内容设为该响应的文本。
因为该文本是 HTML 形式的,所以用户可单击两个链接中的一个并查看添加到页面的结果,如图 4 所示。
图 4. Ajax 结果
该过程肯定没有以前那么痛苦。事实上,这个过程从头到尾都很简单。这里惟一的难题是,到达浏览器的东西实质上是一个二进制大文本。现在这无关紧要,因为我们只是将其显示在页面中,但在很多时候,那并不是我们想要的结果。所以在结束前,需要看一下处理这些请求的第二种方式:JSON。
我们将在 JavaScript 端开始。不再将混合体请求参数作为名称值对发送,而是创建一个将这些请求参数作为属性包含的 JavaScript 对象。
清单 9. 创建 JavaScript 对象请求
<html>
<head>
<script src="/prototype.js"></script>
<script src="/json.js"></script>
<title><?php echo $this->escape($this->title);
?></title>
<script type="text/javascript">
function getMashup(mashupType){
var requestObject = new Object();
requestObject.type = mashupType;
requestObject.query = '<?php echo $this->query ?>';
var jsonRequest = requestObject.toJSONString();
alert(jsonRequest);
var url = 'http://localhost/feed/mashup';
var myAjax = new Ajax.Request(
url,
{
method: 'get',
parameters:
'request='+escape(jsonRequest),
onSuccess: renderResults
}
);
}
...
|
我们正在添加第二个库,可通过 http://www.json.org/js.html 获得。这个库向 JavaScript 添加了两个新函数:toJSONString() 和 parseJSON()。
接下来创建一个简单的对象并设置其属性值。与之前一样,该查询是通过 PHP 视图呈现过程硬编码的。随后可以直接将此对象转换为一个 JSON 字符串并将其作为参数添加到请求中。如果将页面设定为在文本框中输出该字符串,就能看到如图 5 所示的结果。
图 5. JSON 文本
随后需要更新 mashupAction() 以查找这样的新数据。
清单 10. 在
mashupAction() 中接受 JSON 文本
...
public function mashupAction(){
$filterGet = Zend::registry('fGet');
$request = $filterGet->getRaw('request');
$requestObject = Zend_Json::decode($request,
Zend_Json::TYPE_OBJECT);
$type = $requestObject->type;
$query = $requestObject->query;
if ($type == 'amazon'){
$amazonView = Zend::registry('view');
require_once 'Zend/Service/Amazon/Query.php';
...
|
这次我们不按 type 和 query 拆分请求,而是提取出单个文本字符串 $request,并使用 Zend_Json 组件将其转换为一个本地的 PHP 对象。(如果忽略 decode() 方法的第二个参数,则返回一个数组而不是一个对象。)随后即可提取 type 和 query 属性,就像从任何 PHP 对象中提取属性一样。
可以看出,返回一个 JSON 对象有一点技巧,但这并不是因为这项技术本身有多复杂。Zend_JSON 组件也包括了一个 encode() 方法,所以看起来似乎只需完成如下操作即可。
echo Zend_Json::encode($amazonQuery->search()); |
遗憾的是,尽管这段代码起到了作用(它的确返回了结果的一个 JSON 表示),但不够深入。例如,最终得到的对象将具有对图像 URI 对象的一个引用,而不是它的 url 属性。很多时候这或许不会成为一个问题,但因为 Amazon 响应对象相当复杂,我们最好将创建对象时所需的信息直接提取出来。
首先从创建 Amazon 类入手,如下所示。
清单 11. 创建 Amazon 类
class AmazonResults
{
public $results = array();
}
class AmazonResult
{
public $salesrank = null;
public $imgUri;
public $imgWidth;
public $imgHeight;
public $detailPage;
public $title;
public $salesRank;
}
|
将这些类放到恰当的位置使其可用。从 PHP 的观点来看,可将这些类放到 FeedController.php 文件中(当然,这超出了类定义的范围),但从 Zend 框架的角度来看,您也许希望将它们放到模型目录下,此目录应与控制器及视图的位置相同。
这些类并不提供任何函数;它们的存在目的只是为其所表示的对象提供名称。可以创建匿名对象,但将几个匿名对象转换为一个 JSON 字符串会很困难。
也就是说,现在可以创建新对象,如下所示。
清单 12. 创建 Amazon 结果对象
...
if ($type == 'amazon'){
require_once 'Zend/Service/Amazon/Query.php';
$key = 'YOUR_AMAZON_KEY_HERE';
$amazonQuery = new Zend_Service_Amazon_Query($key);
$amazonQuery->Category('Books')
->Keywords($query)
->ResponseGroup('Medium');
$responseObject = new AmazonResults();
foreach ($amazonQuery->search() as $result) {
$resultObject = new AmazonResult();
$resultObject->imgUri =
$result->SmallImage->Url->getUri();
$resultObject->imgWidth =
$result->SmallImage->Width;
$resultObject->imgHeight =
$result->SmallImage->Height;
$resultObject->detailPage = $result->DetailPageURL;
$resultObject->title = htmlentities($result->Title);
$resultObject->salesRank = $result->SalesRank;
array_push($responseObject->results, $resultObject);
}
echo Zend_Json::encode($responseObject);
}
if ($type == 'flickr'){
$key = 'YOUR_FLICKR_KEY_HERE';
...
|
首先,将整个对象创建为 AmazonResults 类的一个实例。在每条搜索结果中完成这样的操作,从每条结果中提取我们想要的信息并将其添加到一个新的 AmazonResult 对象中。每个结果对象都被添加到主对象的结果数组中。随后可以对主对象进行解码并将其发送回浏览器。
目前,页面仅仅输出我们发送回的字符串。如果执行这个请求,可以在页面中看到实际的 JSON 文本,如图 6 所示。
图 6. 返回的 JSON 文本
现在要做的就是调整 renderResults() 函数,使其处理我们发送给它的对象。
清单 13. 处理最终响应
...
function renderResults(response){
var responseObject = response.responseText.parseJSON();
var outputString = '<ol>';
for (i=0; i < responseObject.results.length; i++) {
thisResult = responseObject.results[i];
outputString += '<li><img src="' + thisResult.imgUri;
outputString += '" width="' + thisResult.imgWidth;
outputString += '" height="' + thisResult.imgHeight + '" /> ';
outputString += '<a href="' + thisResult.detailPage + '">';
outputString += '<strong>' + thisResult.title +
'</strong></a>';
outputString += ' (Ranked #' + thisResult.salesRank +
')</li>';
}
outputString += '</ol>';
var renderDiv = document.getElementById('mashupResults');
renderDiv.innerHTML = outputString;
}
...
|
首先,我们必须获取实际对象的引用。为此,可以在响应文本上使用 parseJSON() 方法。一但有了这个对象,就可以像操纵其他 JavaScript 对象一样操纵它了。在本例中,我们创建了一个输出强度(output strength)。为此,要遍历结果数组,每次获取一个单独元素的引用并检索它的属性。检查完所有结果后,就可按照之前的方法直接将它们输出到页面。
在本例中,使用了和上文中相同的 HTML,但重要的区别在于:如果愿意,就可以在脚本中用不同的方式使用该信息。
我们的项目到这里就要结束了。在本系列的学习中,我们从无到有,使用 Zend 框架简化了创建 Chomp! 在线提要阅读器的过程。应用程序执行传统的与 Web 相关的任务,如处理表单,以及一些算是传统的任务,诸如解释 RSS 和 Atom 提要,还有一些不那么传统的任务,诸如动态生成 PDF 文件。
当然,应用程序本身并未完成。我们只是创建了一个主干,还需要更加优秀的图形设计、一些可能的数据库更改以及一些新功能(如系统中目前尚未提供的一项非常必要的功能:动态添加新提要)。
但总体而言,整个过程非常有趣,而且比起不用 Zend 框架的情况,已经节省了非常非常多的工作量。Zend 框架本身也在不断变化和发展。在您阅读本文时,Zend 框架很有可能又包含了新功能,使编程过程更加轻松了。
| 描述 | 名字 | 大小 | 下载方法 |
|---|---|---|---|
| Part 9 source code | os-php-zend9.source.zip | 12KB | HTTP |
学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。
-
本系列开始于 “理解 Zend 框架,第 1 部分:基础”。
-
Zend 框架 的 Web 站点中包含 最新文档 和 安装 FAQ。
-
您可以从 “Ajax RSS 阅读器” 一文中看到有关 Zend 框架为您节省了多少时间的一个对照,这篇文章使用 PHP 从零开始构建了一个 RSS 阅读器。
-
查看对 Ajax 的简介性文章,请阅读 “掌握 Ajax” 系列文章。
-
Java 开发人员请务必阅读 “面向 Java 开发人员的 Ajax: Ajax 的 Java 对象序列化” 一文。
-
阅读 “通过 PHP 和 Sajax 使用 Ajax” ,这是学习 Ajax 和 PHP 的另外一份教程。
-
开发人员 Chris Laffra 在 “审视 Ajax,第 2 部分: 使用 mashup 改变您的生活” 中提出了一些关于 Ajax 和混合体创建方面的有趣想法和实验。
-
“面向资源与面向活动的 Web 服务” 中对无状态架构和会话架构进行了更为深入的探讨。
-
“了解 Web 服务规范,第 1 部分:SOAP” 概述了 SOAP、SOAP 服务器及 SOAP 客户机。
-
要获取如何与 Amazon 进行通信的更加技术化的描述,请阅读关于 Amazon E-Commerce Service 的题为 “使用 Amazon Web 服务推动应用程序的开发,第 1 部分:如何使用 Amazon 电子商务服务” 的文章,其中使用了 Java Development Kit(JDK)。
-
为什么不开发能提供本文中这些服务的站点呢?Amazon 的书籍和零售商品数据库、Yahoo! 的 Web 索引以及 Flickr 的高品质图片都是老网虫的必备工具。
-
要更好地理解提要阅读器项目的目标,请阅读 Vincent Luria 所写的 “RSS 简介”。
-
请务必阅读 Zend 框架教程。
-
系列教程 “学习 PHP” 带您从最基础的 PHP 脚本学起,直至操作数据库和文件系统中的流。
-
PHP 函数参考 是一份极有价值的参考资料。
-
Thought Storms ModelViewController 解释了 MVC 以及围绕其展开的辩论和困惑。
-
phpPatterns Web 站点 从 PHP 的角度解释了 MVC。
-
查看说明如何安装 Apache 和 PHP 的 屏幕截图。
-
访问 IBM developerWorks 的 PHP 项目资源,了解更多有关 PHP 的知识。
-
关注最新的 developerWorks 技术活动和 webcast。
-
查找 IBM 开放源码开发人员感兴趣的近期研讨会、行业展、webcast 以及世界范围内的其他 活动。
-
访问 developerWorks 开放源码专区,获取广泛的 how-to 信息、工具和项目更新,以帮助您使用开放源码技术进行开发,并将其与 IBM 产品结合使用。
-
要聆听为软件开发人员而设的精彩访谈和讨论,请登录 developerWorks podcasts。
获得产品和技术
-
下载 Zend 框架。
-
从 PHP.net 下载 PHP V5.x。
-
下载 Prototype Framework,简化 Ajax 操作。
-
获取更多关于 JavaScript Object Notation(JSON) 的信息,或下载一个 JavaScript 实现。
-
使用 IBM 试用软件 改进您的下一个开放源码开发项目,可以通过下载或从 DVD 中获得这些软件。
讨论
-
通过参与 developerWorks blogs 加入 developerWorks 社区。
Nicholas Chase 曾经参与过许多公司网站的开发,包括 Lucent Technologies、Sun Microsystems、Oracle 及 Tampa Bay Buccaneers。他曾当过中学物理教师、低级放射性废弃设备管理员、在线科幻杂志编辑、多媒体工程师、Oracle 讲师以及一家交互式通信公司的首席技术官。他写过几本书,其中包括 XML Primer Plus。