Ajax RSS 阅读器

2014-02-07 13:18
学习如何构建 Ajax(Asynchronous JavaScript and XML)RSS(Really Simple Syndication)阅读器,以及一个可放在任意 Web 站点上的 Web 组件,以查看 RSS 提要(RSS feed)中的文章。

在我读到有关从 Web 页面的 JavaScript 代码请求 XML(Extensible Markup Language)的内容时,我想到的第一件事就是获得一些 RSS 并加以显示。但我立刻就遇到了XML Hypertext Transfer Protocol(HTTP)的安全性问题,www.mysite.com 中的页面无法寻址 www.mysite.com 以外的页面。我打算在该页面中构建一个通用 RSS 阅读器的计划落空了。而 Web 2.0 所主张的正是灵活巧妙,通过 XMLHTTP 解决如何创建 RSS 阅读器这一问题的过程将让我们学到很多关于 Web 2.0 编程的知识。

本文详细介绍了基于 Ajax 的 RSS 阅读器的构造方法,我们使用 XMLHTTP 和 <script> 标记作为传输机制。

构建服务器端

下载本文代码
本文清单中所给出的代码并非构建 RSS 阅读器的完整代码。如需获得完整代码,请参见 下载 部分。

这一综合体的服务器端分为两部分。第一部分是数据库,第二部分是一组 PHP 页面,这些页面允许您添加提要、请求提要列表、获取与特定提要相关联的文章。首先介绍数据库。

数据库

本文使用 MySQL 数据库。清单 1 展示了数据库模式。



清单 1. 数据库模式
        
CREATE TABLE rss_feeds (
        rss_feed_id MEDIUMINT NOT NULL AUTO_INCREMENT,
        url TEXT NOT NULL,
        name TEXT NOT NULL,
        last_update TIMESTAMP,
        PRIMARY KEY ( rss_feed_id )
);

CREATE TABLE rss_articles (
        rss_feed_id MEDIUMINT NOT NULL,
        link TEXT NOT NULL,
        title TEXT NOT NULL,
        description TEXT NOT NULL
);

共有两个表。rss_feeds 表包含提要列表。rss_articles 表包含与各提要相关联的文章列表。系统更新文章时,将删除当前与给定 rss_feed_id 相关联的所有文章,然后以新文章集刷新该表。

数据库包装器

下一步,使用为应用程序构建逻辑的 PHP 类集打包数据库。从用于管理数据库连接的 DatabaseConnection 单元素着手,参见 清单 2。



清单 2. rss_db.php 中的 DatabaseConnection 单元素
        
<?php
require_once 'DB.php';
require_once 'XML/RSS.php';

class DatabaseConnection
{
  public static function get()
  {
    static $db = null;
    if ( $db == null )
      $db = new DatabaseConnection();
    return $db;
  }

  private $_handle = null;

  private function __construct()
  {
    $dsn = 'mysql://root:password@localhost/rss';
    $this->_handle =& DB::Connect( $dsn, array() );
  }
  
  public function handle()
  {
    return $this->_handle;
  }
}

这是一个标准的 PHP 单元素模式。它连接到数据库,并通过 handle 方法返回一个句柄。这段代码中另一个有趣的部分就是两条 require_once 语句。第一条引用连接到数据库的 PHP Extension and Application Repository(PEAR)DB 模块。第二条引用解析 RSS 提要的 XML_RSS 模块。我得承认,之所以在这里使用这些模块,是因为我实在懒得去费心考虑所有各种形式的 RSS 的解析问题。如果您未安装这些模块,可通过命令行安装:

% pear install DB

以及:

% pear install XML_RSS

DB 模块通常都是安装好的,但 XML_RSS 模块没有安装好。

下一步是构建一个用于打包提要列表的类,以使您能够添加提要、获取提要列表,等等。清单 3 展示了此类。



清单 3. rss_db.php 中的 FeedList 类
class FeedList {
  public static function add( $url ) {
    if ( FeedList::getFeedByUrl( $url ) != null ) return;

    $db = DatabaseConnection::get()->handle();

    $rss =& new XML_RSS( $url );
    $rss->parse();
    $info = $rss->getChannelInfo();

    $isth = $db->prepare( "INSERT INTO rss_feeds VALUES( null, ?, ?, null )" );
    $db->execute( $isth, array( $url, $info['title'] ) );

    $info = FeedList::getFeedByUrl( $url );
    Feed::update( $info['rss_feed_id'] );
  }

  public static function getAll( ) {
    $db = DatabaseConnection::get()->handle();
    $res = $db->query( "SELECT * FROM rss_feeds" );
    $rows = array();
    while( $res->fetchInto( $row, DB_FETCHMODE_ASSOC ) ) { $rows []= $row; }
    return $rows;
  }

  public static function getFeedInfo( $id ) {
    $db = DatabaseConnection::get()->handle();
    $res = $db->query( "SELECT * FROM rss_feeds WHERE rss_feed_id=?",
      array( $id ) );
    while( $res->fetchInto( $row, DB_FETCHMODE_ASSOC ) ) { return $row; }
    return $null;
  }

  public static function getFeedByUrl( $url ) {
    $db = DatabaseConnection::get()->handle();
    $res = $db->query( "SELECT * FROM rss_feeds WHERE url=?", array( $url ) );
    while( $res->fetchInto( $row, DB_FETCHMODE_ASSOC ) ) { return $row; }
    return null;
  }

  public static function update() {
    $db = DatabaseConnection::get()->handle();

    $usth1 = $db->prepare( "UPDATE rss_feeds SET name='' WHERE rss_feed_id=?" );
    $usth2 = $db->prepare( "UPDATE rss_feeds SET name=? WHERE rss_feed_id=?" );

    $res = $db->query(
   "SELECT rss_feed_id,name FROM rss_feeds WHERE last_update<now()-600" );
    while( $res->fetchInto( $row, DB_FETCHMODE_ASSOC ) ) {
      Feed::update( $row['rss_feed_id'] );
      $db->execute( $usth1, array( $row['rss_feed_id'] ) );
      $db->execute( $usth2, array( $row['name'], $row['rss_feed_id'] ) );
    }
  }
}

add 方法向列表中添加一个提要,并更新提要。getAll 方法返回所有提要的列表。getFeedInfo 方法返回一个给定提要的信息。getFeedByUrl 方法实现的功能与 getFeedInfo 方法相同,但使用提要的 URL 作为关键字来实现此功能。如果给定提要在之前的十分钟内没有更新,则 update 函数在该提要上调用 update 方法。

清单 4 展示了 Feed 类,这是业务逻辑类中的最后一个类。它拥有处理单个提要的方法。



清单 4. rss_db.php 中的 Feed 类
class Feed
{
  public static function update( $id )
  {
    $db = DatabaseConnection::get()->handle();

    $info = FeedList::getFeedInfo( $id );
    $rss =& new XML_RSS( $info['url'] );
    $rss->parse();

    $dsth = $db->prepare( "DELETE FROM rss_articles WHERE rss_feed_id=?" );
    $db->execute( $dsth, array( $id ) );

    $isth = $db->prepare( "INSERT INTO rss_articles VALUES( ?, ?, ?, ? )" );

    foreach ($rss->getItems() as $item) {
      $db->execute( $isth, array( $id,
        $item['link'], $item['title'],
        $item['description'] ) );
    }
  }

  public static function get( $id )
  {
    $db = DatabaseConnection::get()->handle();
    $res = $db->query( "SELECT * FROM rss_articles WHERE rss_feed_id=?",
      array( $id ) );
    $rows = array();
    while( $res->fetchInto( $row, DB_FETCHMODE_ASSOC ) )
    {
      $rows []= $row;
    }
    return $rows;
  }
}
?>

update 方法使用 RSS 解析器获取提要并更新数据库。get 方法为给定提要返回文章表的当前内容。

PHP 服务页面

您要用到的第一个页面就是 add.php 页面,如 清单 5 所示,用于向列表添加提要。



清单 5. add.php
<?php
require_once 'rss_db.php';

header( 'Content-type: text/xml' );

FeedList::add( $_GET['url'] );
?>
<done />

这是一个非常简单的包装器,打包了 FeedList 类的 add 方法。最后的 <done> 标记满足了返回某种表示处理成功与否的 XML 这一需求。

下一个页面就是 list.php 页面,如 清单 6 所示,它返回数据库中的提要列表。



清单 6. list.php
<?php
require_once 'rss_db.php';

$rows = FeedList::getAll();

$dom = new DomDocument();
$dom->formatOutput = true;

$root = $dom->createElement( 'feeds' );
$dom->appendChild( $root );

foreach( $rows as $row )
{
  $an = $dom->createElement( 'feed' );
  $an->setAttribute( 'id', $row['rss_feed_id'] );
  $an->setAttribute( 'link', $row['url'] );
  $an->setAttribute( 'name', $row['name'] );
  $root->appendChild( $an );
}

header( "Content-type: text/xml" );
echo $dom->saveXML();
?>

为了使正确编写 XML 更轻松,我使用 PHP 核心中的 Document Object Model(DOM)函数来动态创建一个 XML DOM。随后使用 saveXML 函数将其格式化,以便输出。

假如我用 Firefox® 浏览器浏览此页面,会看到如 图 1 所示的输出。



图 1. 提要列表 XML 页面

当然,在此之前,我已经向列表添加了 8 个提要。

在着手处理系统客户端之前,我需要构建的最后一个页面就是 read.php 页面,如 清单 7 所示,它返回与给定提要 ID 相关联的文章。



清单 7. read.php
<?php
require_once 'rss_db.php';

FeedList::update();

$rows = Feed::get( $_GET['id'] );

$dom = new DomDocument();
$dom->formatOutput = true;

$root = $dom->createElement( 'articles' );
$dom->appendChild( $root );

foreach( $rows as $row )
{
  $an = $dom->createElement( 'article' );
  $an->setAttribute( 'title', $row['title'] );
  $an->setAttribute( 'link', $row['link'] );
  $an->appendChild( $dom->createTextNode( $row['description'] ) );
  $root->appendChild( $an );
}

header( "Content-type: text/xml" );
echo $dom->saveXML();
?>

这在形式上与 list.php 页面极其类似。我使用 Feed 类来获取文章列表。随后使用 XML DOM 对象来创建 XML 并进行输出。在 Firefox 中浏览此页面时,输出如 图 2 所示。



图 2. read.php 页面的 XML

至此,我们已完成了这一综合体的服务器端。现在需要将一个利用 Ajax 来使用这些 PHP 页面的 DHTML(Dynamic Hyper Text Markup Language)页面加入进来。

构建客户机

接下来要完成的任务就是构建一个使用 PHP 页面的客户机。为使您能循序渐进地理解,我将分三个阶段完成构建。第一个版本如 清单 8 所示,展示了一个显示提要列表的控件。



清单 8. index2.html
<html> <head> <title>Ajax RSS Reader</title>
<style>
body { font-family: arial, verdana, sans-serif; }
</style>
<script>
var g_homeDirectory = 'http://localhost/rss/';

var req = null;
function processReqChange( handler ) {
  if (req.readyState == 4 && req.status == 200 && req.responseXML ) {
    handler( req.responseXML ); }
}

function loadXMLDoc( url, handler ) {
  if(window.XMLHttpRequest) {
    try { req = new XMLHttpRequest(); } catch(e) { req = false; }
  }
  else if(window.ActiveXObject)
  {
    try { req = new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {
    try { req = new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) { req = false; } }
  }

  if(req) {
    req.onreadystatechange = function() { processReqChange( handler ); };
    req.open("GET", url, true);
    req.send("");
  }
}

function parseFeedList( dom ) {
  var elfl = document.getElementById( 'elFeedList' );
  elfl.innerHTML = '';

  var nl = req.responseXML.getElementsByTagName( 'feed' );
  for( var i = 0; i < nl.length; i++ ) {
    var nli = nl.item( i );
    var id = nli.getAttribute( 'id' );
    var link = nli.getAttribute( 'link' );
    var name = nli.getAttribute( 'name' );

    var elOption = document.createElement( 'option' );
    elOption.value = id;
    elOption.innerHTML = name;
    elfl.appendChild( elOption );
  }
}

function getFeedList()
{
  loadXMLDoc( g_homeDirectory+'list.php', parseFeedList );
}
</script> </head> <body>
<select id="elFeedList"> </select>
<script> getFeedList(); </script>
</body> </html>

此页面上有一个控件 ?? <select> 控件。此控件由向服务器请求 ist.php 页面的 getFeedList 函数填充。载入页面时,parseFeedList 函数向 <select> 控件添加项目。

在 Firefox 中浏览此页面时,输出如 图 3 所示。



图 3. RSS 阅读器的第一个版本

为将最初这些提要添加到系统中,我使用 MySQL 接口来手动添加这些提要。

下一步是显示所选提要的内容。清单 9 展示了升级后的代码。



清单 9. index3.html
<html> <head> <title>Ajax RSS Reader</title>
<style>
body { font-family: arial, verdana, sans-serif; }
.title { font-size: 14pt; border-bottom: 1px solid black; }
.title a { text-decoration: none; }
.title a:hover { text-decoration: none; }
.title a:visited { text-decoration: none; }
.title a:active { text-decoration: none; }
.title a:link { text-decoration: none; }
.description { font-size: 9pt; margin-left: 20px; }
</style>
<script>
var g_homeDirectory = 'http://localhost/rss/';

var req = null;
function processReqChange( handler ) { ...  }

function loadXMLDoc( url, handler ) { ...  }

function parseFeed( dom ) {
  var ela = document.getElementById( 'elArticles' );
  ela.innerHTML = '';

  var elTable = document.createElement( 'table' );
  var elTBody = document.createElement( 'tbody' );
  elTable.appendChild( elTBody );

  var nl = req.responseXML.getElementsByTagName( 'article' );
  for( var i = 0; i < nl.length; i++ ) {
    var nli = nl.item( i );
    var title = nli.getAttribute( 'title' );
    var link = nli.getAttribute( 'link' );
    var description = nli.firstChild.nodeValue;

    var elTR = document.createElement( 'tr' );
    elTBody.appendChild( elTR );

    var elTD = document.createElement( 'td' );
    elTR.appendChild( elTD );

    var elTitle = document.createElement( 'h1' );
    elTitle.className = 'title';
    elTD.appendChild( elTitle );

    var elTitleLink = document.createElement( 'a' );
    elTitleLink.href = link;
    elTitleLink.innerHTML = title;
    elTitleLink.target = '_blank';
    elTitle.appendChild( elTitleLink );

    var elDescription = document.createElement( 'p' );
    elDescription.className = 'description';
    elDescription.innerHTML = description;
    elTD.appendChild( elDescription );
  }

  ela.appendChild( elTable );
}

function parseFeedList( dom ) {
  var elfl = document.getElementById( 'elFeedList' );
  elfl.innerHTML = '';

  var nl = req.responseXML.getElementsByTagName( 'feed' );
  var firstId = null;
  for( var i = 0; i < nl.length; i++ ) {
    var nli = nl.item( i );
    var id = nli.getAttribute( 'id' );
    var link = nli.getAttribute( 'link' );
    var name = nli.getAttribute( 'name' );

    var elOption = document.createElement( 'option' );
    elOption.value = id;
    elOption.innerHTML = name;
    elfl.appendChild( elOption );

    if ( firstId == null ) firstId = id;
  }
  loadFeed( firstId );
}

function loadFeed( id ) { loadXMLDoc( g_homeDirectory+'read.php?id='+id, parseFeed ); }

function getFeedList() { loadXMLDoc( g_homeDirectory+'list.php', parseFeedList ); }
</script> </head> <body> <div style="width:600px;">
<select id="elFeedList" 
  onchange="loadFeed( this.options[this.selectedIndex].value )"> </select>
<div id="elArticles"> </div>
<script> getFeedList(); </script>
</div> </body> </html>

我省略了 processReqChange 和 loadXMLDoc 函数,因为它们与之前的代码完全相同。新代码在 loadFeed 和 parseFeed 函数中,这些函数用于从 read.php 页面请求数据、解析数据,随后将其添加到页面中。

图 4 展示了在 Firefox 中浏览此页面的输出。



图 4. 显示文章列表的升级页面

下一步是加入通过 add.php 页面向列表添加提要的能力,以完成页面。页面的最终代码如 清单 10 所示。



清单 10. index.html
<html> <head> <title>Ajax RSS Reader</title>
<style>
...
</style>
<script>
var g_homeDirectory = 'http://localhost/rss/';

// The same transfer functions as before

function addFeed()
{
  var url = prompt( "Url" );
  loadXMLDoc( g_homeDirectory+'add.php?url='+escape( url ), parseAddReturn );
  window.setTimeout( getFeedList, 1000 );
}

function loadFeed( id ) { loadXMLDoc( g_homeDirectory+'read.php?id='+id, parseFeed ); }

function getFeedList() { loadXMLDoc( g_homeDirectory+'list.php', parseFeedList ); }
</script> </head> <body> <div style="width:600px;">
<select id="elFeedList" onchange="loadFeed( this.options[this.selectedIndex].value )">
</select>
<input type="button" onclick="addFeed()" value="Add Feed..." />
<div id="elArticles"> </div>
<script> getFeedList(); </script>
</div> </body> </html>

这里的绝大多数代码都是相同的,但我插入了一个新的 Add Feed... 按钮,用于打开一个对话框,您可通过此对话框向提要列表插入一个新 URL。为简化我自己的工作,我让浏览器等了两秒钟,在添加好提要后,获取新提要列表。

图 5 展示了最终完成的页面。



图 5. 最终完成的页面

现在这看起来非常酷。但我还不够满意,因为 XMLHTTP 安全性使我无法从此页面中获取 JavaScript 代码,无法将其复制到其他人的博客上以使他人能够看到提要。为此,我需要重新设计服务,以使用 <script> 标记和 JavaScript Object Notation(JSON)语法。

从 XML 到 JSON

本文将仅允许通过脚本语法查看提要,但我确实会完整地使用脚本标记作为数据传输机制。为获得提要,首先需要将提要列表编码为 JavaScript 代码。因此我创建了一个 list_js.php 页面,如 清单 11 所示。



清单 11. list_js.php
<?php
require_once 'rss_db.php';

header( 'Content-type: text/javascript' );

$rows = FeedList::getAll();

$feeds = array();

foreach( $rows as $row )
{
  $feed = "{ id:".$row['rss_feed_id'];
  $feed .= ", link:'".$row['url']."'";
  $feed .= ", name:'".$row['name']."' }";
  $feeds []= $feed;
}
?>
setFeeds( [ <?php echo( join( ', ', $feeds ) ); ?> ] );

在命令行中运行此脚本时,输出如 清单 12 所示。



清单 12. list_js.php 的输出
setFeeds( [
{ id:1, link:'http://muttmansion.com/ds/index.xml', name:'Driving Sideways' },
{ id:2, link:'http://slashdot.org/slashdot.rdf', name:'Slashdot' },
{ id:3, link:'http://muttmansion.com/vl/index.xml', name:'Visible Light' },
{ id:4, link:'http://muttmansion.com/sor/index.xml', name:'Socks on a Rooster' },
{ id:5, link:'http://muttmansion.com/dd/index.xml', name:'Doxie Digest' },
{ id:6, link:'http://rss.cnn.com/rss/cnn_topstories.rss', name:'CNN.com' },
{ id:7, link:'http://rss.cnn.com/rss/cnn_world.rss', name:'CNN.com - World' },
{ id:8, link:'http://rss.cnn.com/rss/cnn_us.rss', name:'CNN.com - U.S.' } ] );

这对于 <script> 标记而言很有用。浏览器加载此输出时,将通过提要列表调用 setFeeds 函数。然后依次设置 <select> 控件,加载第一个提要。

还需要与 read.php 功能等同的页面,不过是使用 JavaScript 代码而不是 XML 来返回文章数据。清单 13 展示了 read_js.php 页面。



清单 13. read_js.php
<?php
require_once 'rss_db.php';

function js_clean( $str )
{
  $str = preg_replace( "/\'/", "", $str );
  return $str;
}

FeedList::update();

$id = array_key_exists( 'id', $_GET ) ? $_GET['id'] : 1;

$rows = Feed::get( $id );

$items = array();

foreach( $rows as $row )
{
  $js = "{ title:'".js_clean($row['title'])."'";
  $js .= ", link:'".js_clean($row['link'])."'";
  $js .= ", description:'".js_clean($row['description'])."' }";
  $items []= $js;
}
?>
addFeed( <?php echo( $id ); ?>,
[ <?php echo( join( ', ', $items ) ); ?> ] );

同样,在命令行中运行此脚本后,会看到如 清单 14 所示的输出。



清单 14. read_js.php 的输出
addFeed( 1,
[ { title:'War',
  link:'http://www.muttmansion.com/ds/archives/002816.html',
  description:'The...' }, ... ] );

为简洁起见,我截短了这段代码,但您可以抓住要点。以提要的 ID 和使用 JavaScript 格式编码的文章数据调用 addFeed 函数。

有了这些新的用 JavaScript 实现的服务,现在就可以创建一个新页面来使用这些服务了。清单 15 展示了这一新页面。



清单 15. script.html
<html> <head> <title>Script Component Test</title>
<style>
...
</style>
<script>
var g_homeDirectory = 'http://localhost/rss/';

function loadScript( url ) {
  var elScript = document.createElement( 'script' );
  elScript.src = url;
  document.body.appendChild( elScript );
}

function addFeed( id, articles ) {
  var ela = document.getElementById( 'elArticles' );
  ela.innerHTML = '';

  var elTable = document.createElement( 'table' );
  var elTBody = document.createElement( 'tbody' );
  elTable.appendChild( elTBody );

  for( var a in articles ) {
    var title = articles[a].title;
    var link = articles[a].link;
    var description = articles[a].description;

	// Create elements as before...
  }

  ela.appendChild( elTable );
}

function setFeeds( feeds ) {
  var elfl = document.getElementById( 'elFeedList' );
  elfl.innerHTML = '';

  var firstId = null;
  for( var f in feeds ) {
    var elOption = document.createElement( 'option' );
    elOption.value = feeds[f].id;
    elOption.innerHTML = feeds[f].name;
    elfl.appendChild( elOption );

    if ( firstId == null ) firstId = feeds[f].id;
  }
  loadFeed( firstId );
}

function loadFeed( id ) { loadScript( g_homeDirectory+'read_js.php?id='+id ); }

function getFeedList() { loadScript( g_homeDirectory+'list_js.php' ); }
</script> </head> <body>
<div class="rssControl"> <div class="rssControlTitle">
<select id="elFeedList" 
  onchange="loadFeed( this.options[this.selectedIndex].value )">
</select> </div> <div id="elArticles"> </div> </div>
<script> getFeedList(); </script>
</body> </html>

此页面与最初的 idnex.html 页面非常类似。但未使用 loadXMLDoc 函数,而是使用了名为 loadScript 的新函数,用于动态创建 <script> 标记。<script> 标记随后从指定的 URL 加载 JavaScript 代码。

这些脚本标记调用 read_js.php 和 list_js.php 页面。而这些页面又创建在主页中回调 setFeeds 和 addFeed 函数的 JavaScript 代码。

转到此页面时,浏览器显示效果如 图 6 所示。



图 6. 为数据使用 <script> 标记的 RSS 阅读器

此代码最大的优点就是任何人都可以使用 View Source 命令来查看页面中的脚本,并且能将这些代码复制到自己的页面中。随后其页面将使用返回 JavaScript 代码的 PHP 服务来更新页面。

结束语

在这篇文章中,我演示了如何使用两种不同的技术动态地访问 Web 页面中的数据,从而在页面上创建一个 RSS 阅读器。如果一切顺利,您能够使用本文给出的理念和代码丰富您自己的应用程序,而不必彻底重组您的代码。这正是 Ajax 真正的价值所在 ?? 如果您熟悉 Web 技术,即可轻松通过服务器端的一些新服务和客户端的一点点代码使交互性得以升级。






回页首


下载

描述 名字 大小 下载方法
Source code x-ajaxrss-code.zip 8KB HTTP
关于下载方法的信息 Get Adobe® Reader®