3.3 实现:组合电子阅读器
要构建这个令人兴奋的Booker应用程序,最好的方式就是先将其化整为零。下面就是需要加入到这个应用程序中的三个主要部件:
XML文件(book_alicewonderland.xml、book_frankenstein.xml等)
HTML网页(booker.html)
PHP服务器端脚本(booker.php)
下面几节将对Booker应用程序的这几个部分进行详细讨论,同时揭示各个部分的相关代码。
注意
又要学习另一种新技术PHP了。尽管最好是懂PHP,但绝对不是只有PHP程序员才能理解和自定义这个或者本书中其他的应用程序。为此,我有意将本书所有示例应用程序中的PHP代码都保持在极其简单而且代码量也最少的程度。你所碰到的许多PHP代码都是“样板文件”,也就是说根本不需要动它们就能够自定义应用。假如真有机会需要你微调PHP代码,我也会明确指出应该修改哪里和为什么需要那么改。
XML电子书
在原先的Booker示例中,电子书是以纯文本文件保存的,它们都带有我们熟悉的.txt扩展名。而应用程序则将每本电子书的全部数据,作为一个巨大的文本块打开来显示。本章的新Booker应用程序则需要电子书保存为XML数据格式,通过该格式可以清晰地表示章和其他信息(比如图书名称、图书作者、章名称等)。与文本文件类似,XML文件也可以通过任何文本编辑器软件打开和编辑。实际上,你可以将XML文件想像为一个HTML网页文件,只不过它使用的是定制的词汇表(标签集),而不是<html>、<p>、<div>等这些HTML中的标签。XML文件的扩展名都是.xml。
注意
在讨论XML时,你经常会看到文件和文档这两个术语。一般而言,它们的含义是相同。而当一段XML代码并不与硬盘中的某个文件相关时,这两个术语的差别就会体现出来。例如,XML代码完全可以(有时候也是必须的)由程序动态生成,对动态生成的代码就无所谓文件的概念了。此时,使用更抽象的术语—文档会更贴切。在本书中,我将使用同样的术语。
那么你打算使用哪种XML词汇表来表现一本电子书呢?虽然当前有一些标准的电子书词汇表,但这些词汇表对本章的示例来说明显过于复杂,所以我们还是自己创建为好。与其逐个地介绍每个标签的含义,不如直接看一看实际的XML电子书的部分代码:
<?xml version="1.0" encoding="utf-8"?> |
第一行可能略显神秘一些,实际上,其中的代码表示的是该XML文件遵守XML 1.0标准,而且使用的是标准字符编码方案(UTF-8)。这行代码几乎是在所有XML文档中都会出现的标准代码。除此这外,其他代码都与具体的电子书相关,其中的标签和属性名非常容易理解。
具体来说,每本电子书的代码都以分层方式包含在一对ebook元素中,ebook元素由下面这样的开标签和闭标签组成:
<ebook> |
|
一本电子书中的每一章都以一对<chapter>标签表示,chapter元素还有两个保存章编号和章名称的属性:num和title。除了这两个属性之外,<chapter>标签本身还作为保存章文本的容器。因此,单独一章的代码如下所示:
<chapter num="Chapter II" title="The Pool of Tears"> |
章文本中古怪的“'”表示一个撇号('),使用这种特殊的编码是为了避免XML文档解析程序将撇号与其他代码混淆。对这个元素(在XML中也称实体)还可以使用标准的撇号实体来表示,即“'”。在电子书中还需要进行类似编码的是引号字符,在XML文件中引号要使用“"”,而不是实际的引号(")。
在理解了一本电子书的XML文档结构,并且也知道了如何将电子书中每一章的内容编码为XML文档的基础上,理解整本书的XML代码就比较容易了。以下就是H. G. Wells所著The War of the Worlds中的部分XML代码:
<?xml version="1.0" encoding="utf-8"?> No one would have believed in the last years of the |
但愿你能看懂这些XML代码,毕竟我们已经见过使用相同格式的两本书的代码了。对于The War of the Worlds被分成两“册”的特殊情况也很容易处理。不用为了适应标识章的不同方式而修改全部数据格式,在这里我们把书的编号整合到了章编号中。与其他电子书不同,这本书同时还利用了章编号以及章名称,这也证明了chapter元素的这两个属性是必要的。
虽然这并不是什么有趣的事,但我还是通过手工把上一章中的那五个示例电子书,转换成了刚刚看到的XML格式。转换开始时并不费劲,直至遇到非人性的(简直就是!)Moby Dick—这本书居然分了102章!这让我连对War and Peace发脾气的机会都没有了。War and Peace是一本规模宏大的小说,它为Booker应用程序提供了极好的压力测试。在根据自己的需要定制Booker应用程序的过程中,你最终会发现把电子书编码为XML格式是其中最耗时的一个部分。
在本章前面我们曾提到过,尽管XML文件实际上就是文本文件,也必须使用.xml扩展名。下面就是要在更新后的Booker应用程序中使用的示例电子书,以及它们各自的文件名:
Alice誷 Adventures in Wonderland—book_alicewonderland.xml |
"book_" + AbbrevTitle + ".xml" |
下一节内容揭示了使用这个文件名连同章索引一起生成对服务器的Ajax请求的Booker应用程序的代码。
Booker的网页部分
尽管Booker应用程序中有服务器端组件,但大多数工作量都发生在客户端,这就是应用程序的HTML网页部分—booker.html。图3-4中显示了新Booker应用程序在Web浏览器中运行的界面,在经过更新的用户界面中提供了用于导航章的按钮。
注意
如果你奇怪在这个Booker应用程序中的Ajax诊断栏到哪里去了,答案是我把它隐藏起来了,以免它分散你的注意力。事实上,诊断栏在本书其余的应用程序中都不会再出现。不过,可以在这个网页的HTML代码中删除Ajax状态div(ID值为ajaxState)的样式值dispaly:none,或者把该样式值改为display:block来启用它。如果你想留着这个样式,那么在后面可以再禁用它。
|
这幅插图展示了应用程序中一些新的界面元素。首先,章名称正好显示在电子书选择列表的下方。这个区域实际上可以同时显示章编号和章名称,但在显示War of the Worlds中前言的情况下,则只显示章编号(INTRO)。
在章标题(编号+名称)下方,是两个导航按钮,分别用于导航到第1章和前一章。不过这两个按钮现在在插图中是被禁用的,这是一种细微但却有用的用户界面提示,表明读者当前正在阅读电子书的第1章。再往下是章文本,章文本的后面又是两个按钮,用于导航到下一章和最后一章。
为了测试方便,我最初把所有这些导航按钮都集中放在了章文本的上方。但后来我想到当读者看到一章的结尾时,必须要把页面滚动回顶部才能导航到下一章,这样导航的作用就被削弱了。所以,最终我把Next和End(>>)按钮放在了章文本的下方。
界面外观介绍的差不多了,下面我们继续看一下页面主体中的代码,这些代码定义了用户界面并连接起了脚本化的事件:
<body style="background:#EEEEEE" |
同时,我知道对于一本不需要你懂JavaScript的书来说,这些代码中的JavaScript有点过份。不过,请记住本书的宗旨是为了让你在自己的应用中重用这些代码。而重用这些代码完全可以在你不理解每一行代码含义的情况下做到。虽然如此,还是有必要从大体上讲解一下,以便你进行修改时有的放矢。而且,不要忘了所有这些代码(以及本节中其余的代码)都能在本书附带的Live Linux CD中booker.html文件中找到。
首先,用户界面被组织到一些div元素中,这些div元素为清晰地组织章信息和导航按钮提供了良好的手段。与前一版的应用程序相似,包含电子书的选择列表使用HTML中的<select>标签定义。这一版中更有趣的是导航按钮,它们是使用<input>标签定义的。
Previous和Next导航按钮的功能是通过调用prevChapter()和nextChapter()这两个JavaScript函数实现的,稍后我们会讲到这两个函数。Home(<<)和End(>>)按钮也使用了相同的函数,但同时配合使用了跟踪当前章索引的变量curChapIndex。在后面我们讲解与应用相关的JavaScript代码时,会讨论这个变量的工作原理。
Booker网页主体中另一个非常重要的代码块,是基于选择列表中选项的变化加载电子书的代码:
loadChapter(this.options[this.selectedIndex].value, 0) |
这行代码与前一版应用程序中的选择变化代码是相同的,只不过这行代码除了传递当前选择的电子书(this.options[this.selectedIndex].value)之外,还传递了章索引(0)。而且,函数的名称也从loadBook()修改为loadChapter(),用以表明只会加载一章的内容。在<body>标签的onload事件处理程序中同样也调用了loadChapter()函数,这里的调用目的是当第一次打开页面时,加载列表中第一本电子书的第一章内容。
下面就是loadChapter()函数的代码,该函数通过Ajax请求给定的简略电子书名称和章索引,加载单独一章的内容:
function loadChapter(title, chapIndex) {// 设置当前的章索引 // 清除上一次加载的章文本 // 发送Ajax请求加载新一章的内容 |
这个函数的前几行代码对导航按钮进行了初始化,以便适应第一本书的第1章来启用和禁用相应按钮。要保存当前的章索引是因为需要根据它来成功地从一章导航到另一章 。章文本区域具有双重用途,当章的内容被加载期间,该区域会提示正在加载中,其内容被设置为“Loading...”。而当加载完成后,其内容又被替换成实际的章文本。最后,通过将简略名称和章索引绑定到URL中,并通过GET请求方法来发送实际的Ajax请求。注意,我们熟悉的handleRequest()函数仍然被指定为请求处理程序,也就是说当请求完成时会调用这个函数。
在这个Booker应用程序中,handleRequest()函数承担了最大的工作量,在这里它要负责取得XML图书数据、解析出重要的信息并更新页面中相应的元素。在讨论这个函数的代码之前,我们再简单地看一下在Ajax响应中由服务器发送回来的XML数据的格式:
<bookData> // 存储总章数 // 显示章信息 // 显示章文本 |
totalChapters = |
totalChapters = |
这就是一个示范在不必深究DOM函数具体细节的条件下,如何修改本应用程序中的XML处理代码的例子。
注意
在这个Booker应用程序中使用的getText()和replaceText()函数不是标准的JavaScript DOM函数,而是包含在本书domkit.js文件中的辅助函数。前一章解释了这两个函数以及其他Ajax工具箱中函数的关系。
handleRequest()函数中的其余代码涉及到从XML数据中提取出信息段,然后将它们显示到页面中的每个div元素中。这些代码非常聪明,它们检查章编号和章名称是否同时存在,如果同时存在就会将这两段信息组合起来。否则,只显示实际存在的信息段。这种方法巧妙地解决了有的图书只有章编号,有的只有章名称,而有的既有章编号又有章名称的情况。
在这个网页中我们唯一还没有看到的函数是响应导航按钮单击事件的函数—prevChapter()和nextChapter()。我们从prevChapter()函数开始,该函数的代码如下:
function prevChapter() { |
这个函数中的大部分代码都致力于根据当前章是否是第1章来灵活地启用或者禁用导航按钮。在有关导航按钮的逻辑执行完成后,代码调用了loadChapter()函数,并用当前章索引指定了要加载哪一章。
nextChapter()函数与prevChapter()非常相似,但目标集中在最的一章而不是第1章。换句话说,如果当前章是最后一章,那么nextChapter()函数必须禁用Next按钮。下面的代码显示了如何以totalChapters变量为基础,来决定当前章是不是最后一章:
function nextChapter() { |
以上这些代码构成了改头换面的新Booker应用程序的客户端。现在所剩的只有服务器端脚本了,那是与客户端相对的另外一个主要部分。
Booker的服务器端脚本
在Booker应用程序中,服务器端的PHP脚本负责读取包含整本书的XML文件,然后动态生成只包含客户端网页所需章信息的XML文档。最后,包含章信息的XML文档又通过Ajax响应被传递回客户端。下面就是服务器端PHP脚本booker.php的代码:
<?php // 将电子书的XML数据解析为结构化数据 $chap = $xml->chapters->chapter[intval($_REQUEST['chapIndex'])]; // 生成包含图书章信息的XML文档 |
如果你没有任何PHP经验,那么这些代码看起来可能很陌生。与其在这里教你一门新语言(当然这肯定超出了本书的范围和领域),不如当一回讲解员。代码在一开始确定了要以XML格式返回数据—这很关键,因为服务器会在通过Ajax响应将数据交付回客户端时,对数据进行适当打包。
脚本中第一处重要的代码块负责从一个XML文件中读取整本电子书,并保存到一个可以进一步处理的数据结构中。其中代码$_REQUEST['title']就是用于取得在Ajax请求的URL中发送的title参数的PHP脚本。回忆一下相应的代码:
booker.php?title=warofworlds&chapIndex=0 |
$_REQUEST['chapIndex'] |
回到解析XML电子书的脚本代码中,该代码构建了一个文件名(比如book_warofworlds.xml),然后将其读入到一个XML数据结构中。在此基础上,代码的主要部分深入到这个XML数据结构中,根据脚本取得的chapIndex参数来查找指定的章。然后,有效地读取并写入XML—在整本电子书中查找指定的章信息,将这些章信息写入一个新的XML文档是件乏味的苦差事。最终的结果就是我们在上一节中看到的带有<bookData>标签的简洁的XML响应数据。
要真正理解这些服务器端脚本的工作原理,最好是通过文本编辑器打开其中一本电子书的XML文件,然后在你一行一行地查看脚本中代码的同时研究其中的标签。你会发现一种系统地操作每个标签和属性的模式,而且与这些标签和属性相关的数据会被提取并打包到新XML标签中。
结果,脚本构建起一个包含章信息的新XML文档,又通过下面这一行代码将其返回到客户端:
echo $xmlChapter; |
虽然脚本的工作量显得有几分虎头蛇尾的味道,但这正是PHP的工作方式。真正叫绝的还要等到下一节你通过应用程序不停地执行这个脚本块时才会体现出来。不过,在此之前,我们先简单介绍一下如何测试PHP脚本。
我们知道Booker应用程序中的Ajax请求,是由一个向PHP脚本传递参数的URL组成的,PHP脚本负责向请求返回作为响应的XML文档。而在Ajax请求的环境之外研究响应是非常有益的,也就是说我们可以偷看一下PHP脚本在Web浏览器中会直接生成什么内容。为此,我们可以在浏览器中简单地输入一个Ajax请求的URL作为网页地址,像下面这样:
http://www.yourdomain.com/booker.php?title=warofworlds&chapIndex=0 |
|
| 回书目 上一节 下一节 |
|
· 上周Linux系统命令的使.. · 上周真题冲刺测试获奖.. · 全国计算机等考四级模.. · 08年3月各大网上书店及.. · 网络工程师模拟测试获.. · 全国计算机软考考试指.. |
· 3月24日WCF聊天活动 积.. · 全国计算机等级考试四.. · 软件项目估计:第2版 · 系统分析师基础知识自.. · 构建可扩展的Web站点的.. · 2008年全国计算机等级.. |
|
||||
| · 技术人求职简历完备手册 · 华为员工自杀频频拷问.. · 视频访谈:网管员如何踏.. · 首届中国IT工程师生态.. · 思科全球CEO钱伯斯第七.. · 北漂技术人90天求职纪实 · 2007年互联网大会 · 龙芯要做中国的“奔腾” |
· IPv6协议--拓展网络无.. · 国际文档格式标准开战 · 微软出价446亿美元收购.. · 贝恩资本携手华为22亿.. · Linux——从菜鸟到高手 · SOA 面向服务架构 · 2008年4月全国计算机等.. · 微软Forefront企业安全.. |
|||
|
||||
| · SQL Server 2008/2005.. · SOA 面向服务架构 · SQL Server 2008/2005.. · iSCSI应用与发展 · RAID——磁盘阵列基础 · 中间件应用技术专题 · SQL Server入门到精通 · 病毒查杀专题 |
· 国际文档格式标准开战 · 路由器设置与口令恢复 · Linux防火墙 · 打造安全服务器 · SOA 面向服务架构 · PHP开发应用手册 · ADSL应用面面俱到 · 入侵防护系统(IPS)初探 |
|||
|
||||
| · iSCSI应用与发展 · 中间件应用技术专题 · SQL Server入门到精通 · SQL Server 2008/2005.. · SOA 面向服务架构 · iSCSI应用与发展 · RAID——磁盘阵列基础 · 病毒查杀专题 |
· 路由器设置与口令恢复 · SOA 面向服务架构 · 了解统一威胁管理(UTM).. · ADSL应用面面俱到 · ADSL应用面面俱到 · 反垃圾邮件技术应用 · PHP开发应用手册 · 中间件应用技术专题 |
|||