IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Open source  >

理解 Zend 框架,第 9 部分: 用 Ajax 和 JSON 添加交互性

developerWorks
文档选项

未显示需要 JavaScript 的文档选项

样例代码


级别: 中级

Nicholas Chase (ibmquestions@nicholaschase.com), 自由作者, Backstop Media
Gina Deol (gdeol@binaryits.com), 副总裁,电子商务开发, Binary IT Solutions

2006 年 10 月 26 日

在整个 “理解 Zend 框架” 系列中,我们使用了 PHP Zend 框架来创建 Chomp 在线提要阅读器,现在是时候进行最后的调整以提高实用性了。本文介绍了如何使用 Ajax 在无需重新装载整个页面的情况下为页面添加信息,以及如何使用 Zend 框架将数据与 JavaScript Object Notation(JSON)互相转换,从而轻松流线化这些请求。

简介

在本系列的 第 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 结果




回页首


添加 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 文本

随后需要更新 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 文本

现在要做的就是调整 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 codeos-php-zend9.source.zip12KBHTTP
关于下载方法的信息


参考资料

学习

获得产品和技术

讨论


作者简介

Nicholas Chase 曾经参与过许多公司网站的开发,包括 Lucent Technologies、Sun Microsystems、Oracle 及 Tampa Bay Buccaneers。他曾当过中学物理教师、低级放射性废弃设备管理员、在线科幻杂志编辑、多媒体工程师、Oracle 讲师以及一家交互式通信公司的首席技术官。他写过几本书,其中包括 XML Primer Plus


Gina Deol 正在 Sacramento State 大学攻读计算机科学与会计信息系统专业的理科学士,现在已经是最后一年。她曾是一名 Web 开发工程师兼项目协调员,目前她在位于 Sacramento 的 Binary IT Solutions 公司担任电子商务开发副总裁。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款