13.3 万维网脚本
Scripting the World Wide Web
万维网上的大量内容都是静态的——特别是那些搜索引擎可以看到的东西——网页实际上很少变化。但超链接(作为万维网基础的一个抽象概念)却被人们描述为一种表达“复杂、多变和非确定性”的方式 [Nel65]。今天web功能的大部分来自于它发布的页面能够活动、发出声音、响应用户动作,或者(可能是更重要的)包含了一些信息,可以在响应页面提取请求时根据要求而创建或格式化。
从程序设计语言的观点看,简单地回放录好的音频或视频信号并没有什么意思,因此这里将把注意力集中到其中的程序(脚本),它们被关联于互联网URI(统一资源标识符)7,在运行中生成各种各样的内容。假定我们在一台客户机的浏览器里输入了一个URI,浏览器就会给适当的web服务器送一个请求。如果与之相应的内容是动态创建的,那么就出现了一个明显的问题:创建相关内容的脚本是在服务器上运行,还是在客户机上运行?这两种选择分别称为服务器端脚本和客户端脚本。
当服务提供方希望完全控制页面的内容,但又不能(或者不希望)事先创建好这些内容时,它通常就使用服务器端脚本。这方面的例子包括由搜索引擎、互联网零售商、拍卖网点,以及任何为其客户提供在线访问其个人帐户功能的组织返回的页面。客户端脚本通常用于一些不需要访问专有信息,而且在客户端执行起来更有效的工作。例子包括交互式动画、填充表单的错误检查,以及其他各种各样的自足计算。
|
13.3.1 |
13.3.1 CGI脚本
CGI Scripts
服务器端web脚本的最原始机制就是Common Gateway Interface(通用网关接口,CGI)。一个CGI脚本也就是一个可执行程序,放在web服务器程序知道的某个特殊目录下。当客户请求的某URI对应于这样一个程序时,服务器就执行该程序并把它的输出送给客户。当然,这一输出必须是客户浏览器可以识别的,通常就是HTML。
|
例13.29 用CGI脚本做远程监视 |
CGI脚本可以用任何在服务器端机器上可用的语言书写,但在这里用Perl的情况特别多,因为它的字符串处理功能和“粘接”机制特别适合生成HTML,而且在web发展的早期Perl就已经广泛可用了。作为一个简单的有些臆造的例子,假定我们希望能监视由某个用户社团共享的服务器机器的状态。图13.10的Perl脚本将创建一个web页,其标题是服务器的机器名,内容包含uptime和who两个命令的输

图13.10 一个用Perl写的简单CGI脚本。如果这一脚本的名字是status.perl,它被安装在服务器的cgi-bin目录,那么在互联网上任何地方的用户通过键入 hostname/cgi-bin/status.perl,就可以得到一个情况总结和一个当时登录这一服务器的用户名单。
|
例13.30 使用CGI脚本的Adder web表单 |
出(两种最简单资源的状态信息)。这一脚本开始的print命令生成HTML消息的头部,指明下面是一个HTML。执行这一脚本的示例输出在图13.11。
CGI脚本通常用于处理在线表单,图13.12是一个简单实例。该HTML文件里的FORM元素描述了这一CGI脚本的URI,在用户点击Submit按钮时将会调用它。以前键入在INPUT域的信息或者作为这一URI的结尾部分传给相应脚本(对于get类型的表单),或者在标准输入流上传给脚本(对于post类型的表单,如这里给出的)8。对任何一种方式,我们都能用Perl库里标准CGI的param例程访问这些值,有关的库在脚本开始处已经装载。
|
13.3.2 |
13.3.2 嵌入式服务器端脚本
Embedded Server-Side Scripts
虽然使用很广泛,但CGI脚本有一些重要缺点。
¨ web服务器必须把每个脚本作为一个独立程序启动,这可能带来很大的开销(虽然把CGI编译为本地代码后运行时可能非常快速。)
¨ 由于服务器对于脚本的行为几乎无法控制,脚本通常必须由可信任的管理员安装在可信任的目录下,不能像其他常规页面那样放在任何位置。
¨ 脚本的名字出现在URI里,通常还有可信目录的名字作为前缀,因此最终用户看到的动态网页和静态网页是不同的。

图13.11 图13.10的脚本的示例输出。上面是HTML原文,下面是绘制出的页面。
¨ 每个脚本不但要生成动态内容,还要生成为了格式化和显示这些内容所需的HTML标签。这种额外的“样板”给书写脚本带来很大困难。
为了克服这些缺点,今天的大多数web服务器都提供了一种“模块装载”机制,把一种或几种脚本语言的解释器结合到服务器本身。这一(这些)脚本语言的脚本可以嵌入在“常规的”web页里,web服务器直接解释这些脚本而不再启动外部程序。在把页面送给客户之前,web服务器用脚本生成的输出替代这些脚本。客户甚至没办法知道脚本的存在。
嵌入式的服务器端脚本包括PHP,Visual Basic(在微软的Active Server Page里)、Cold Fusion(来自Macromedia公司),以及Java(通过“Servlets”在Java Server Page里运行)。其中使用最多的是PHP。虽然PHP是从Perl发展起来的,但现在它已经为自己的领地而广泛深入地进行了定制。除其他功能外,PHP提供的内部支持包括email和MIME编码、所有互联网通信协议、认证和安全性、HTML和URI操作,以及与数十种数据库的互操作。

|
例13.31 远程监视的PHP脚本 |
图13.12 一个交互式的CGI表单。开始的web页显示在上部左边,绘制出的页面在右边,其中用户已经在两个文本域中输入了12和34。当用户按压Submit按钮时,客户端浏览器将把一个请求送给服务器的URI /cgi-bin/add.perl。值12和34也包含在这个请求里。中间显示的是相应的Perl脚本。它用这些值生成一个新页面。在下面左边是这个页面的HTML形式,右边是绘制出的页面。
图13.13里给出了与图13.10等价的PHP脚本。这个图里的大部分内容都是标准的HTML,PHP代码嵌在分隔符 <?php 和 ?> 之间。这些分隔符并不是HTML里的东西,它们只是标出页面中的一些部分,说明这里的东西需要用PHP解释器执行并生成替代正文。这样,页面的“样板”部分就可以照抄,不需要通

图13.13 一个简单的嵌入PHP脚本的web页。在启动了PHP系统的宿主机上,这一脚本的等价于图13.10的CGI脚本。

图13.14 一个PHP脚本片段。这里的if和for如我们期望的那样工作,虽然它们与HTML内容交织在一起。当一个浏览器请求它的时候,这一页面将显示0到19的数,其中的奇数用黑体显示。
|
例13.32 一个PHP脚本片段 |
过print(Perl)或echo(PHP)生成。请注意,相互分离的脚本片段是同一个程序的不同部分。举例说,例子里的
$host变量在第一个片段里设置,在第二个片段里再次使用。
PHP脚本甚至可以在结构化语句的中间分段。图13.14里给出了一个脚本,其中的if和for语句都被分成几段。从效果看,位于一段之后另一段之前的HTML正文的行为就像是用echo命令产生的输出。web设计师可以自由采用任何方式(采用echo或回到原始的HTML内容),只需要看哪种形式更符合手头工作的需要。
|
例13.33 使用PHP脚本的Adder web表单 |
自发送表单
通过修改FORM元素的action属性,我们可以安排图13.12的Adder页调用PHP脚本,而不是调用CGI脚本:

图13.15 一个交互式PHP页。上面的脚本用在图13.12中间脚本的位置。本图下面的脚本取代了图13.12上面的web页和中间的脚本。它检查是否得到了完整的一集参数,如果没有就显示需要填充的表单,如果得到了就显示结果。
|
例13.34 自发送的Adder web表单 |
![]()
相关PHP脚本在图13.15的上半部给出。脚本通过一个名为 _REQUEST的关联数组(散列表)取得表单值。这里没有用任何特定的库。
由于这里的PHP脚本直接由web服务器执行,因此它可以安全地放在任何web目录里,包括放在Adder页所在的目录里。事实上,通过检查一个页面是否已经请求,我们还可以把表单和页面合并起来,让它自己为自己的请求服务!我们在图13.15的下半部展示了这种可能性。
13.3.3 客户端脚本
|
13.3.3 |
Client-Side Scripts
虽然嵌入式的服务器端脚本通常比CGI脚本更快速,但至少启动它们就占用了运行代价中主要的部分,而且跨越互联网的通信对真正的交互式网页也显得太慢。如果我们希望页面的行为和观感能随着用户移动鼠标、点击、键入以及窗口的隐藏或暴露而迅速变化,那么就需要在客户机器上执行某种类型的脚本。
|
例13.35 用JavaScript写的Adder web表单 |
由于是在web的设计师所在的站点上运行,CGI脚本,或者更广泛地说,其他嵌入式的服务器端脚本,完全可以采用不同的语言写出。客户端脚本的情况就不同了,因为客户端能看到的也就是HTML,这种脚本必须能在客户端机器上进行解释。这一情况对客户端脚本语言的集中产生了很强的动力,因为大多数设计师都希望他们的页面能被尽可能多的受众看到。Visual Basic在一些特殊组织内广泛使用,那里的设计师知道有兴趣的客户都用Internet Explorer。然而,希望有最广泛受众的页面几乎都用JavaScript提供交互式特征。
图13.16给出了一个带有内嵌JavaScript脚本的页面,它(在客户端)模拟图13.12和图13.15里Adder脚本的行为。函数doAdd在该页的头部定义,因此在整个页里都可以用。特别的,当用户点击Calculate按钮时就会调用它。按默认规定,输入值都是字符串,我们用parseInt函数把它们转换为整数。在最后的赋值语句里 (argA + argB) 被括号括起,这里就是强制性地要求做整数加法,脚本出现的其他 + 都是字符串拼接。为了屏蔽当用户按压enter或return键时将数据发送给服务器的默认行为,我们为这一表单的onsubmit属性设定了一个什么都不做的动作。
这里没有用输出正文取代原来的页面,与像前面的CGI或PHP脚本的做法不同,这个JavaScript版本采用的方式是把输出附加在页面下部。HTML的SPAN元素在文档中提供了一个命名位置,我们可以把输出插入这里。JavaScript的getElementById方法提供到指定元素的引用。由万维网联盟标准化的HTML文档对象模型(Document Object Model,DOM)里规定了一大批其他元素、属性和用户动作,这些都是JavaScript可以访问的。通过这些元素,脚本就可以在适当的时候检查或修改页面的内容、结构或风格的几乎任何方面。
|
13.3.4 |
13.3.4 Java小程序
Java Applets
小程序也就是那种其设计就是为了在另外的程序里运行的程序。这一术语最常用于指那些把自己的输出显示在web页(的一个部分)里的Java程序。

图13.16 一个交互式的JavaScript web页面。源代码在左边,右边显示的是用户输入两个数并按压Calculate键之后绘制的结果,用户按键导致输出新型出现在页面里。通过再次输入和击键,用户可以计算任意多个求和值。每个新计算将取代原输出信息。
|
例13.36 在web页里嵌入一个小程序 |
与JavaScript类似,Java小程序也可以用于创建动画的或交互式的页面。由于语言的名字类似,而且许多事情都可以用这两者中的任何一种来做,因此在两者间造成了很大的混淆(见第710页的旁白)。事实上,它们是非常不同的东西。
要把一个小程序嵌入页面里,按照传统需要用一个APPLET标签:
![]()
要看到嵌入页面的这个元素,客户端浏览器将向服务器请求Clock.class的URI。假定服务器返回这个小程序,浏览器就会运行这个小程序,并将其输出显示在页面上。
与JavaScript不同,小程序并不产生要求浏览器去绘制的HTML输出。它的做法是直接控制页面实际状况中的一个部分,调用Java的图形用户接口(GUI)库(通常是AWT或Swing)里的例程,去显示它希望显示的东西。APPLET元素的width
和height属性告诉浏览器需要在页面里给这一小程序预留多大一块区域。
|
例13.37 在web页里嵌入对象 |
从效果上看,小程序使设计师完全跑到HTML之外去建立一种准确的“观感”,与嵌入浏览器里的设计选择完全无关。当然可以设想暂时离开HTML的其他方式,为了静态或简单的动画内容使用其他种类的嵌入对象(Flash或QuickTime格式的影像是最常见的例子)。大部分浏览器都提供了“插件”机制,允许安装任意格式的解释器。为了支持这些功能,HTML 4.0标准提供了一个通用OBJECT元素,就是想用于浏览器自身不能绘制的任何嵌入内容。现在APPLET元素已经被官方确定为过时特征了,建议采用的是下面形式:
![]()
![]()
需要给小程序强加一些限制,以防它们去破坏客户端的机器。当然,极端地说,当小程序完全可以使用完整的Java语言时,把一个小程序转换为一个标准程序,或者做另一方向的转换,通常都是很容易的。典型的小程序并没有与浏览器或其他程序的重要交互作用。由于这一情况,人们通常并不把它们看作脚本。
设计和实现
JavaScript和Java
虽然名字相近,JavaScript与Java除了某些表面的语法类似之外并无其他联系。最初的JavaScript是由Brendan Eich于1995年在Netscape公司开发的。Eich称自己的语言为LiveScript,但公司在其公开发布前选择对它重新命名,作为与Sun Microsystem的市场协议的一部分。JavaScript名字的商标实际上归Sun所有。
1995年Netscape的浏览器还是市场上的领袖,而JavaScript的引入又使它高速增长。为了竞争的需要,微软的开发者把对JavaScript的支持加入Internet Explorer,使用的名字是JScript,而且引进了若干与该语言的Netscape版本不兼容的东西。欧洲标准化组织在1997年标准化了一个称为ECMAScript的公共版本,但一些主要的不兼容性仍存在于不同浏览器提供的文档对象模型中。由于万维网联盟的一系列标准化努力,这些问题已经被逐渐解决了。但原来遗留下来的页面和浏览器仍继续折磨着web开发者。
检查你的理解
26. 请解释服务器端脚本与客户端脚本之间的不同。
27. 请列出CGI脚本和PHP脚本之间的利弊权衡。
28. 为什么CGI脚本通常只安装在特定的目录下?
29. 请解释一个PHP页面如何为自己的请求服务。
30. 为什么我们有时更愿意在服务器上执行一个脚本,而不是在客户端?而有时又更愿意在客户端执行脚本?
31. 什么是HTML文档对象模型(Document Object Model)?它对客户端脚本的重要性何在?
32. JavaScript与Java有什么关系?
设计和实现
沙箱
如果要执行的代码来自其他地方,安全性就成为一个重要问题。web服务器通常总是在很受限的访问权下安装的,用户只能看到服务器机器上文件系统里非常受限的一部分。这样,服务器只能提供受到限制的一集页面,是用户直接登录服务器机器时能看到的东西中的一个定义良好的子集。由于CGI脚本是独立的可执行程序,它们能以安装人的特权运行。为了防止服务器机器的用户无意或有意把他们的特权传递给互联网的任意用户,大多数系统管理员都配置自己的服务器,使CGI脚本只能驻留在一个特定目录里,由完全可信任的用户安装。嵌入式的服务器脚本可以驻留在任何文件里,因为可以保证它们只能在服务器(的受限权限下)运行。
在客户机器从互联网下载并执行的代码中隐藏着更大的危险。由于这种代码通常是不可信任的,因此必须在一个仔细控制的环境里执行,以防它们造成任何破坏,这就是所谓的沙箱。作为一般性规则,JavaScript不能访问局部文件系统、存储管理系统、网络,也不能操作来自其他站点的文档。Java小程序的情况与此类似,它们只能有限地访问外部资源。当然,现实情况更复杂一点:有时脚本需要存取一个大小受限的临时文件,或者到访问可信服务器的网络连接。存在一些验证可信站点的机制,也有这样的机制,可以用可信站点去验证来自其他站点的页面的真实性。我们可以给通过可信机制获得的页面上的脚本更大权限。但这种机制必须谨慎使用。在安全性和功能性中间找到正确的平衡仍然是Web领域的一个核心挑战,也是更一般的分布式计算的核心挑战。
33. 什么是小程序?为什么通常不把小程序看作脚本?
|
13.3.5 |
13.3.5 XSLT
XSLT
大多数读者都无疑地有写(至少是读)用于组合web页面的HTML(超文本标记语言)的经验。HTML的主要部分是一个嵌套结构,其中的文档片段(元素)被一些标签分隔开,这些标签指明有关片段的用途或表现形式。在13.2.2节里我们已经看过这方面的例子,最高层的标题由 <H1> 和 </H1> 界定。HTML的设计受到一个较早的称为SGML(standard generalized markup language,标准通用标记语言)的启发,该语言一直被广泛用在商务领域,用于表示结构化的数据。由于web一直以一种非官方的方式发展和进步,而且时时出现相互竞争的厂商的互不兼容的和特定的扩充,HTML的标准化已经成为一个漫长而复杂的过程。浏览器的不兼容性一直困扰着web设计师,一些已经被HTML的最近标准作为过时9特征的东西还仍然在广泛使用。另外一些特征虽然没被归入过时之列,但经过认真回顾也被广泛认为是一种错误。
|
例13.38 HTML的内容和表现形式 |
或许HTML最大的问题在于它不能适当地区分文档的内容与其表现形式。作为一个简单的例子,web设计师常常用 <I>...</I> 标签要求把正文设定为斜体,其实这时 <EM>...</EM>(强调)可能更合适。一个浏览器为了视觉上更明显,有可能选择采用斜体之外的其他方式强调这段正文,也可能把书名(常常也被用
<I>...</I> 标记)用其他风格绘制出来。更重要的,许多web设计师用表格(<TABLE>...</TABLE>)控制一个页面里不同元素的相对位置,即使其中的内容根本就不是表格。随着越来越多的提供商希望把web内容送给手机、电视、手持计算机,或者送给只支持声音的设备,对于区分内容和表现形式(展示)的需求也变得越来越紧迫了。SGML总区分这两个方面,但人们大多认为它做得太过分,用到web上过于复杂了。
这也就是XML的发祥之地。XML(extensible markup language,可扩充标记语言)是SGML的一个经过慎重研究的流行后裔,它至少在三个方面优于HTML:(1)其语法和语义更规范而统一,能更统一地跨平台实现;(2)它可以扩充,这意味着用户可以定义新的标签;(3)它只描述内容,把表现形式留给一个名为XSL(extensible stylesheet language,可扩充风格表语言)的伴生标准。XSLT是XSL中的一个部分,专用于转换XML:选择、重组、修改其中的标签和它们界定的元
素,从效果上看,它也就是处理用XML表示的数据的脚本。
互联网字符集Soup
学习web标准可能是一个令人气馁的任务:这里有无穷无尽的流行词语、标准和多字母缩写词。记住三个标记语言族可能会有些帮助——SGML、HTML和XML,还要记住其中每一个都有与之相关的风格纸语言:DSSSL、CSS和XSL。风格纸语言用于控制文档的展示,将展示的形式与内容分离。风格纸语言对于SGML和XML都是必不可少的东西,没有它们就没办法知道 <RECORD> 代表的是一个数据库项、一个作为收藏品的唱片集,还是一项奥林匹克记录,更不知道如何去显示它。HTML对风格纸的依赖性不强,但web站点正在越来越多地使用CSS,以便为一集页面创建一种统一的“观感”,不采用直接在每个页面里嵌入冗余信息的方式。
SGML和DSSSL在商务领域仍然非常重要,但在web领域用得很少。HTML看来还会持续存在很长时间,但是它的缺乏可扩充性,以及把内容和展示混在一起的方式,正在被越来越多的人认为是一种本质性的缺陷。XML已被广泛认为代表着记法形式的未来,即使文档还是用HTML,设计师也喜欢移师XHTML(可扩充超文本标记语言),这是HTML的一个几乎(但不全)向后兼容的符合XML标准的变形。
XML和XHTML
|
例13.39 良形式的XHTML |
一个XML文档必须是良形式的:标签必须或者是构成正确的嵌套,或者是以 /> 分隔符结束的单体。例如,下面的片段就是良形式的(虽然不完全)XHTML:
![]()
这里的引号元素(<q>...</q>)嵌套在强调元素(<em>...</em>)内部。进一步说,可以作为一个链接的目标的锚元素(<a.../>)显然是个单体,在它的闭结束符“>”之前有一个斜线符。(为避免把一些遗产浏览器搞乱,有时需要在斜线符之前加一个空格。)如果没有这个斜线符,或者开标签
<em><q> 被误写为 <q><em>,这一实例片段就是不良形式的。
良形式是一种简单的语法规则,就像在Lisp里要求括号配对一样。但它将使XML(从而也使XHTML)比普通的HTML更容易分析和自动处理。细心的读者可能已经注意到,我们一直用小写字母写XHTML的标签,而先前的HTML例子用的是大写字母。HTML对大小写是不敏感的,任何写法都可以接受,虽然标准文档里的习惯是用大写字母。XML是大小写敏感的,因此 <em> 和 <EM> 是不同的。XHTML必须选择其一。采取与现存约定(但不是现存规则)不同的选择,在能帮助阅读者辨认这可能是符合新标准的东西的同时,也有利于向后兼容性。
用在XML文档里的标签集合或者用一个文档类型定义(document type definition,DTD)描述,或者用一个XML Schema描述。DTD也继承自SGML,它说明了在文档里允许用哪些标签,它们是成对的还是单体的,它们是否允许有属性(如例13.39里的id = "favorite-quote" 那样的名字-值对),以及是否有某个属性是必须的。DTD里的规则采用XML声明的形式,也就是由“<!”分隔符界定的一些元素。可以直接把它们包含到XML文档里,但更常见的方式是将其保存在另一个有自己的URI的文件里。XML文档以 <!DOCTYPE> 声明开头,在其中描述相应的URI。(注释的形式就像声明:<!-- 忽略的正文 -->。)如果在一个XML文档没有显式声明相关的DTD(无论是在线的或外部的),那就说它是依靠实际使用的标签隐含地定义了一个DTD。
XML Schemas是一种新机制,其用意是取代DTD。Schema用XSD(The XML Schema Definition Language,XML Schema定义语言)写出,它本身就是用DTD定义的良形式XML的一个例子。由于XML Schemas是用XSD写出的,因此可以用支持XML的编辑器创建,用XML分析器做语法分析,用XSLT做变换。与DTD相比,XSD为刻画语法规则提供了丰富得多的词汇表。除其他功能外,设计者可以用它在细节程度上刻画元素和属性的数据类型,提供在DTD里不可能做到的水平的自动检查。XSD还支持继承,因此可以把一个XML Schema定义为另一个的扩充。可惜的是,在本书撰写的时候,DTD的使用还是比XML Schema更普遍。特别的,XHTML就是用一集DTD正式定义的,对应的XML Schema还在工作过程中。在本节剩下的部分,我们将继续用DTD。
|
例13.40 用XHTML显示所喜爱的引号 |
由于XML的标签必须是嵌套的,一个文档自然地具有一种树形结构。图13.17显示的是一个短小但完整的XHTML文档,还有它所表示的树。在这种树中有三类结点:元素(在源文本里用标签界定)、正文和属性。所有内部(非叶)结点都是元素,嵌在一个元素的开始和结束标签中的东西或者是一个属性,或者是该元素在树中的一个孩子。
我们的文档以一个 <?xml...?> 处理指示开头,该指示说明XML的版本及文档其余部分所使用的字符编码。包含这一指示是为了方便那些处理文档的工具,它并不是XML文档本身的一部分。(注意,我们在前面的13.3.2节也看到过处理指示,在那里是将它们提供给PHP解释器。)
这一文档的第二行是一个 <!DOCTYPE...> 声明,其中指名了一个在万维网联盟的XHTML DTD。这个文档剩下的部分是数据,根(也就是 "/")下面只有一个孩子,是一个html元素。这个元素又有两个孩子,表示其head和body。head

图13.17 一个完整的XHTML文档和它对应的树。子关系用实线表示,属性用虚线。
有一个孩子title和一个属性xmlns。后者将xhtml声明为这一文档的默认名字空间。XML里的名字空间类似于C99的名字空间或Java的包(3.7节),它们都允许给标签名加上用于区分的前缀,例如:xhtml:table与furniture:table。在这里为xmlns规定了值之后,文档里任何没加前缀的标签都自动解释为位于这个xmlns名字空间里。
XSLT、XPath和XSL-FO
XSL(extensible stylesheet language,可扩充风格纸语言)可以看作是描述需要对一个XML文档做什么的语言。它有三个子语言,分别称为XSLT、XPath和XSL-FO。XSLT是一个脚本语言,它以XML作为输入并产生文本输出,常被用于变换XML或HTML,也可以用于其他格式化工作。
|
例13.41 用XPath命名XHTML元素 |
XPath是一个用于在XML文件里命名各种东西的语言,XPath名字经常出现在XSLT元素的属性里。让我们回到图13.17,在这个文档中被引号括起的元素,用XPath命名就是 /html/body/p/em/q。如果要把这个元素与其正文兄弟一起命名,写出来就是 /html/body/p/em/*。XPath包含了一集很丰富的命名机制,包括绝对(从根出发)和相对(从当前结点出发)的漫游、通配符、谓词、子串和正则表达式操作,以及计数和算术函数。我们将在下面的例子里看到其中一些东西。
XSL-FO(XSL formatting objects,XSL格式化对象)是一集用于描述文档布局(表现形式)的标签,基于页面、区域(例如头部、体、页脚)、块(段落、表格、列表)和行,以及各种内嵌元素(字符、图形)。我们可能用XSLT脚本给

图13.18 在XML里做参考文献。索引(两本书,一篇杂志论文,三篇会议论文)是按任意顺序排列的。Kesper的URI做了折行,以便放入打印页(续图在下页)。

图13.18(续)
XML文档加入一些XSL-FO标签,或者变换一个内部已经有XSL-FO标签的文档。例如把供web用的很长一页文档切分为准备在纸面上打印的多页文档。为了简单起见,我们将不在任何例子里使用XSL-FO,而是直接用XSLT去格式化XML文档,把它们转换到HTML。
一个XML文档可以明确指定用于转换或格式化它的XSLT脚本。这是一种标准的但有些受限的做事方式:把一个风格纸订在这个XML文件上,将使我们实际上丧失了内容和表现形式的分离,而这种分离正是创建XML的第一推动力。另一方式是用一个客户端的JavaScript或服务器端的PHP去调用XSLT处理器,把相应的XML文档和XSLT脚本作为参数传给它。可惜的是,在本书撰写时,这样做的细节在不同服务器和客户平台上都很不统一。
|
例13.42 用XSLT创建参考文献列表 |
扩展示例:参考文献的格式化
作为一项示例性工作,使其中可以实际地用到XSLT,现在考虑一个参考文献列表的创建。图13.18里包含了为这种列表而做的一个XML源文件。(这里采用的域名字借自BIBTEX [Lam94,附录B]。)文档开始于一对处理指示,一个描述XML的版本和字符编码,另一个描述用于格式化这个文件的XSL风格纸。

图13.19 用XSL写出的参考文献风格纸。把这一脚本应用到如图13.18那样的参考文献文档上,可以生成出相应的HTML文件(续图在下页)。

图13.19(续)
在最高一层,bibliography的元素由一系列book、article和inproceedings元素组成,其中每个元素可能包含作者或编纂者的名字、标题、出版商、日期和地址等。有些元素还可以包含URI元素,刻画一个在线链接。不能用ASCII表示的字符用Unicode的字符实体表示,如第313页的旁白里描述的。
图13.19是把这种参考文献格式化到HTML的XSLT风格纸(脚本),得到的HTML文件就可以由浏览器绘制了。在相应XML文档的开始指名了这个脚本(见图13.18)。与其他XML文档一样,这一脚本的开始也是一对处理指示。第一个指示说明XML版本和字符编码,第二个说明XSL版本和名字空间。脚本中余下的部分是XSL和HTML元素的混合物,其中的XSL标签都明确写了xsl: 名字空间,它们将由XSLT处理器识别和处理。来自其他名字空间的元素都被当作常规的正文,遇到了就直接复制到输出。
XSLT的最基本结构是template(模板),它刻画了一集需要应用到XML源树的结点上的指令。这种模板通常通过在其他模板里执行apply-template或call-template指令的方式调用。每个调用都有当前结点的概念。整个执行是从把源树的根(/)作为当前结点去调用初始模板开始。在我们的参考文献实例中,初始模板就是在脚本最上面的那个,因为它的match属性是XPath表达式 "/"。初始模板的体开始于一串HTML元素和一些正文。这一串东西都直接复制到输出。随后的for-each元素是一个XSLT指令,因此要执行它。
这个for-each的select属性中用了一个XPath表达式("bibliography/*"),用于构造一个结点集合,其中包含参考文献里所有的顶层项。可以用其他形式的表达式表示需要做选择,如 "bibliography/*[year>=2000]" 将匹配最近的项目,"bibliography/*[note]" 只匹配带有note的项目,"bibliography/ article|bibliography/book" 只匹配杂志论文和书籍。
嵌套的sort指令把选出的结点集合按标题的字典序排列,而后把选出的各个项作为当前结点执行for-each的体。这个体里包含了一个对apply-template的递归调用,括在一对HTML列表标签里面(<li>...</li>)。两个标签将直接复制为输出,递归调用的结果嵌入它们之间。
现在来看这一递归调用做些什么。递归调用的select属性像前面的for-each一样用XPath构造一个结点集合。此处用的是只包含 "." 的平凡结点集合,也就是for-each的当前迭代的当前结点。XSLT检索与这个结点匹配的模板。我们为此建立了三个模板,针对每种参考文献项有一个。在找到匹配模板后,处理器就会用当时的当前结点去调用它。
在建好的三个主要模板里各包含了一集指令,完成相应的项(杂志论文、书籍和会议论文)的格式化。这些指令里的大多数又包含了一个对apply-template调用,用于完成该项中各个部分(作者、标题、出版商等)的格式化。这些指令中点缀着一些零星文本或HTML元素。有几种情况里用了if指令,只在源文件里包含了给定XML元素时才生成输出。多数递归调用都用XPath函数node() 选择被处理元素的所有子结点。
如果空格出现在一条指令结束与另一指令开始之间,它们就会被忽略。在这种情况下,要想把空格加入输出里,就必须把它们扩在分界符 <text>...</text> 之间。附加的空格(例如在一个句子之后)用“非中断空格”字符项描述,也就是  。
脚本最后还有三个模板,其中最有趣的一个完成作者列表的格式化。它有一个name属性而没有match属性,用的是call-template调用而不是apply-template。这种call-template总以调用者的当前结点作为参数(目前就是表示参考文献项的那个结点)。在内部,这一作者列表模板执行一个for-each指令,选出表示作者或编纂者的所有子结点。这个for-each转而用XPath函数last() 和position() 确定有多少个名字,以及这些名字在表中的位置。它在最后两个名字之间插入一个单词 "and",如果有三个或更多的名字,它还在除最后的名字之外的每个名字后面放一个逗号。
带有属性match="uri" 的模板用于做XML源文本里任何地方出现的URI的格式化。它在输出里创建一个HTML链接,但用XPath函数substring-after从可见文本里摘掉开头的http://。XPath还提供了许多类似的函数,用于字符串和正则表达式操作。value-of指令把所选结点的内容作为字符串复制到输出。
最后一个模板用于默认情况。XPath表达式 "@*|node()" 将与XML源文本的任何属性和其他结点匹配。在模板内部,copy指令把结点的标签(如果有)复制到输出,两个标签之间放进递归调用apply-template的结果。进一步递归调用中的 "@*|node()" 选出的结点集合里包含当前结点的所有属性和子结点。这样做的结果是,如果源文本里的任何XML元素由某个标签界定,而我们没有为它提供特殊的模板,那么它就会被原封不动地重新生成到输出里。这一递归在遇到正文结点或属性时停止,它们都是XML树的叶结点。
这一脚本输出的HTML在图13.20里,绘制出的web页在图13.21中。
虽然从本教科书的标准来看这个例子已经很长了,但它只显示了XSLT功能的一小部分。按照程序设计语言的标准分类,这一记法形式具有很强的说明性:值可以有名字,但不存在可变的变量,也没有副作用。这里有一种受限的循环机制(for-each),但其真正的威力来自递归,特别是对XML树的递归遍历。
检查你的理解
34. 请解释SGML、HTML和XML之间的关系。它们各自对应的风格纸语言是什么?
35. 为什么XML要如此努力地去区分内容和表现形式?
36. XSL的三个主要部分是什么?它们各自的用途何在?
37. 什么是XHTML?它与HTML有什么不同?
38. 请解释XML文档与树之间的对应。
39. 说一个XML文档是良形式的,是什么意思?
40. 什么是文档类型定义(DTD)?XML Schema?对它们做简单比较。
41. 请解释XML的元素、声明和处理指示之间的不同(语法和语义)。再解释元素、标签和属性之间的不同。
42. 请总结一下XSLT的执行模型。简而言之,它怎么工作?
43. 请解释在XSLT里应用(apply)一个模板和调用(call)一个模板有什么不同?

图13.20 把图13.19里的风格纸应用到图13.18里的参考文献得到的结果。






