内容


理解 Zend 框架,第 9 部分

用 Ajax 和 JSON 添加交互性

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 理解 Zend 框架,第 9 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:理解 Zend 框架,第 9 部分

敬请期待该系列的后续内容。

简介

在本系列的 第 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 组件,以所请求的特定信息自动地对搜索结果页面进行局部更新。

Ajax 和 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 过滤器来接受 typequery 这两个参数。该行为根据 type 的值来呈现 Amazon 或 Flickr 视图。

可以通过将浏览器指向 http://localhost/feed/mashup?type=flickr&query=pentagon 或一个有着类似查询的 URL 来查看这个行为。结果与图 3 类似。

图 3. 显示新混合体行为
显示新混合体行为
显示新混合体行为

还可将 type 参数改为 amazon 来查看其他数据。我们将在搜索结果页面中包含这一数据,但仅包含需要的数据。

现在我们已经做好了准备,可以将 Ajax 结合进来了。

添加 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 的混合体行为)和一组形参,其中包括使用哪个方法(GETPOST)、随请求一起发送的参数,以及在响应返回时要执行的 JavaScript 行为。

在本例中,执行该行为的是 renderedResults() 函数,该函数接受 HTTP 响应作为参数。该函数只是获取了我们添加的新 div 元素及 mashupResults 的一个引用,并将其内容设为该响应的文本。

因为该文本是 HTML 形式的,所以用户可单击两个链接中的一个并查看添加到页面的结果,如图 4 所示。

图 4. Ajax 结果
Ajax 结果
Ajax 结果

添加 JSON

该过程肯定没有以前那么痛苦。事实上,这个过程从头到尾都很简单。这里惟一的难题是,到达浏览器的东西实质上是一个二进制大文本。现在这无关紧要,因为我们只是将其显示在页面中,但在很多时候,那并不是我们想要的结果。所以在结束前,需要看一下处理这些请求的第二种方式: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 文本
JSON 文本
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';
...

这次我们不按 typequery 拆分请求,而是提取出单个文本字符串 $request,并使用 Zend_Json 组件将其转换为一个本地的 PHP 对象。(如果忽略 decode() 方法的第二个参数,则返回一个数组而不是一个对象。)随后即可提取 typequery 属性,就像从任何 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 文本
返回的 JSON 文本
返回的 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 框架很有可能又包含了新功能,使编程过程更加轻松了。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=170907
ArticleTitle=理解 Zend 框架,第 9 部分: 用 Ajax 和 JSON 添加交互性
publish-date=10262006