级别: 初级
Alan Knox, 软件工程师, IBM
2001 年 6 月 01 日
XSLT
样式表可用来动态地将 XML 变换成复杂的浏览器显示标记 --
但如果显示复杂,样式表也复杂。因此需要一种能够从简单组件构建复杂样式表的工具。既然 XSLT 本身就是 XML,因此可以用 XSLT 操纵
XSLT;样式表也能编写样式表。本文演示如何从 XSLT 组件构建一个执行某一特定运行时变换的 XSLT 样式表。
另一篇 developerWorks 文章
让你的XML适合所有尺寸的屏幕
讨论了编写与管理那些在很多显示设备上显示同一 XML
篮球统计信息的样式表的问题。该解决方案涉及到编写一个参数化的样式表,该样式表生成具有不同等级的数据内容的
HTML,然后使用 WebSphere Transcoding Publisher
将该样式表产生的输出转换成适合于某个特定设备的代码。虽然这对于很多情况来说是一种有效和简单的解决方案,但却丧失了对出现在用户屏幕上的内容的某些控制。
如果要:
- 完全控制用户所见的内容
- 调整应用程序的显示,以便在每一种设备上实现最佳的可能的效果
- 利用设备的特定特性
那么,您必须解决生成无数复杂的样式表的问题。本文演示了一个使用相同篮球
XML 数据的非折衷解决方案。
|
什么是样式表?
样式表描述文档如何在屏幕上或打印中显示。通过将样式表附加到 Web
上的结构化文档,您可以改变显示,而无需牺牲设备独立性或添加新的 HTML
标记。
什么是
XSL?
XSL 是可扩展样式表语言,一种用于表达样式表的语言。(CSS
是另一种功效较弱的样式表语言。)
什么是
XSLT?
XSLT 代表 XSL 变换,或用于变换 XML 文档的语言。
什么是
XML?
XML 代表用于基于 Web 的文档的可扩展标记语言。可将它看成 SGML
的缩减版。
|
|
样式表的难点
样式表主要有两类问题:
-
复杂性 -
新的应用程序显示往往需要新的样式表。随着标记语言和新设备的激增,所需的样式表的数量也变得令人生畏。如果使用
XML/XSLT 以外的方法(例如 JavaServer Pages (JSP) 或 JavaBean
体系结构),仍然存在同样程度的复杂性。然而,生成 JSP
的可使用的工具支持目前比支持 XSLT 样式表的工具支持要发达得多。
-
技能 - 除了理解 XML
数据之外,样式表作者还必须深入理解多种显示设备的标记语言。另外,在浏览器中生成在外观吸引人的显示也需要设计技能。而这种技能组合并非轻易可以获得。
解决方案
上述问题可以通过采用基于组件的方法开发样式表来解决。XSLT
是一种声明性语言,其中,构成样式表的模板彼此独立。XSLT
样式表可以使用
import
和
include
机制由其它样式表构成。只要适当留意,您可以分别开发一些独立的
组件样式表,这些组件样式表可以一起构成将在运行期间应用于
XML
数据的
显示样式表。这些组件将大致分为三种类型,用于处理:
- 显示动态 XML 数据(在我的示例中是篮球数据)
- 可重用的显示标记部分,例如按钮栏
- 页面残余部分
下例演示了这种解决方案。
用户界面设计
您可能希望您的 Web 应用程序看上去和感觉起来设计得更专业。布局美观的
HTML 页面往往意味着复杂标记、具有各种嵌入式 HTML
表的安排以及精确的格式化和间距命令。在本文中,我假设已有某个表示下面所示
页面设计的静态
HTML。任务是用一个显示样式表重新生成相同的外观和感觉。
目标外观和感觉
虽然这只是一个简单的示例页面,但它也有一些嵌入的 HTML
表。它将只是多个页面中的一个,这些页面显示的数据来自生成 XML
形式的篮球统计信息的应用程序。我将用
篮球 XML 中的数据替换 "Dynamic
data"(动态数据),然后使用 "Navigation
bar"(导航栏)列出从当前页开始的链接。
采取的步骤
要生成显示样式表,需要:
- 从设计 HTML 制作一个样式表。
- 制作一个将篮球得分格式化成 HTML 的样式表。
- 将二者合并。
- 制作一个生成“导航”栏的 XSLT“窗口小部件”。
- 具体化这个窗口小部件,以生成页面的最终样式表。
从设计 HTML 制作一个样式表
我可以只制作一个 XSLT 样式表,然后将 HTML
粘贴到适当位置。然而,当用户界面设计更改时,我希望能相应地快速而轻松地更新显示样式表。一种比较好的解决方案是发明一个可以自动将设计
HTML 转换成样式表的过程。如果设计 HTML 和 XML
一样是有效的(如果不是,可以将它清除),那我就可以用 XSLT
样式表这样做。
页面骨架
首先,在设计 HTML 的
<HTML>
标记周围添加
<layout>
元素。
带标记的设计 HTML -
skeleton.xml
<layout match="game">
<html>
...
</html>
</layout>
|
这不一定使 HTML 变成样式表,但确实可以让我使用
<layout>
元素的属性来存储那些有助于推动此过程的信息。例如,
match="game"
指定了篮球 XML 中的某个元素,我将使用该元素来触发此页面 HTML
的生成。
<layout>
元素内的 HTML
本质上是目标页面的 HTML 模板。因为
模板一词在 XSLT
的上下文中有意义,所以,我称它为目标页面的 HTML
骨架。
预处理样式表
下面是将
skeleton.xml
中的页面骨架转换成样式表的样式表。
pre-process.xsl - 第 1 版
<?xml version="1.0"?>
<xsl:style sheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:out="http://www.hursley.ibm.com/hsandt/et/compile">
<xsl:output method="xml" indent="yes"/>
<xsl:namespace-alias stylesheet-prefix="out" result-prefix="xsl"/>
<!-- template 1 -->
<xsl:template match="/layout">
<out:stylesheet version="1.0">
<out:output method="html" indent="yes"/>
<out:template match="{@match}">
<xsl:apply-templates/>
</out:template>
</out:stylesheet>
</xsl:template>
<!-- template 2 -->
<xsl:template match="*|text()">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="*|text()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
|
该样式表有两个模板:
- template 1 匹配页面骨架中的
<layout>
元素,并输出 XSLT 样式表。
- template 2
匹配任何其它元素或文本节点,并只将它匹配的内容不加更改地复制到输出中。
因为我希望此样式表的输出是一个样式表,所以必须输出 XSLT
语句。但是,XSLT 处理器如何区分要输出的 XSLT 语句和要执行的 XSLT
语句呢?答案是对要复制到输出的 XSLT 语句使用另一个 XML
名称空间。
在上例中,我定义了一个新的名称空间
(
"http://www.hursley.ibm.com/hsandt/et/compile"
),并将其与名称空间前缀
out
关联。因为带有
out
前缀的 XSLT
语句不在 XSLT (
"http://www.w3.org/1999/XSL/Transform"
)
名称空间中,所以,XSLT
处理器将按我的需要输出它们。但这还不够。我希望输出是正确的样式表,XSLT
进程无需作任何进一步更改就可以执行它。在示例的这一点处,我将获得看起来类似于样式表、而实际上却不是的东西。
XSLT 用上面所用的
<xsl:namespace-alias>
元素提供了该问题的解决方案。这将获取
"http://www.hursley.ibm.com/hsandt/et/compile"
(
out
) 名称空间中的所有内容,并在输出时将它们移到
"http://www.w3.org/1999/XSL/Transform"
(
xsl
) 名称空间中。
将
pre-process.xsl
应用到页面骨架的结果看起来类似于:
从
pre-process.xsl 得到的输出 -
skeleton.xsl
<?xml version="1.0" encoding="UTF-8"?>
<out:stylesheet xmlns:out="http://www.w3.org/1999/XSL/Transform" version="1.0">
<out:output indent="yes" method="html"/>
<out:template match="game">
<html>
...
</html>
</out:template>
</out:stylesheet>
|
请注意,名称空间前缀仍然是
out
。这并不重要。重要的是名称空间前缀
out
现在被映射成 XSLT
名称空间:
"http://www.w3.org/1999/XSL/Transform"
。(对
XML 语法分析器很重要的是名称空间,而不是前缀。前缀是使 XML
便于阅读的工具)。某些 XSLT
处理器可以在输出时更改名称空间前缀和名称空间。本文示例所用的 Xalan
XSLT 处理器不更改。
现在,可以将
skeleton.xsl
样式表应用到篮球数据。结果由与数据中
<game>
元素的匹配所触发,它与原始
设计 HTML 文件是同样的
HTML。目前这还不是很有用,因为在我希望看到比赛得分的地方仍然显示字符串
"Dynamic data"。我需要让
skeleton.xsl
样式表触发那些在适当位置插入得分的模板。我通过找到写着 "Dynamic
data" 的标记,然后将它替换成
<xsl:apply-templates>
调用来实现。
skeleton.xml
文件中的以下标记:
...
<table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%">
<tbody>
<tr><td><img src="spacer.gif" width="1" height="50"/></td></tr>
<tr><td height="100%" align="center" valign="top">
<h2>Dynamic data</h2>
</td></tr>
</tbody>
</table>
...
|
变成
...
<table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%">
<tbody>
<tr><td><img src="spacer.gif" width="1" height="50"/></td></tr>
<tr><td height="100%" align="center" valign="top">
<out:apply-templates select="//recap"/>
</td></tr>
</tbody>
</table>
...
|
请注意
out
名称空间前缀;它将导致 XSLT
处理器将元素作为被输出,而不是被执行的事物对待 -- 与上面对
pre-process.xsl
样式表中的类似元素的描述完全一样。正如您将在下一部分中看到的,我只对篮球数据中的
<recap>
元素感兴趣,因此,我可以在这里指定选择标准
select="//recap"
。
现在,我所拥有的 XSLT 代码能够生成所需的静态 HTML
布局,并可以触发那些插入动态数据的模板。接下来,我需要从篮球 XML
数据抽取数据的 XSLT 模板。
篮球数据样式表
为简便起见,我只生成一个在篮球数据的
<recap>
元素中找到的得分数据表,如下所示。
recap.xsl
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'>
<xsl:output method="HTML"/>
<xsl:template match="/">
<HTML>
<HEAD>
<TITLE>Box Score</TITLE>
</HEAD>
<BODY>
<h1>Testing</h1>
<xsl:apply-templates select="//recap"/>
</BODY>
</HTML>
</xsl:template>
<xsl:template match="recap">
<TABLE border="0" cellpadding="2" cellspacing="1" width="80%">
<TR>
<TD BGCOLOR="#c80000"></TD>
<TD BGCOLOR="#c80000">
<CENTER><font color="#FFFFFF"><B>1st</B></font></CENTER>
</TD>
<TD BGCOLOR="#c80000">
<CENTER><font color="#FFFFFF"><B>2nd</B></font></CENTER>
</TD>
<TD BGCOLOR="#c80000">
<CENTER><font color="#FFFFFF"><B>Total</B></font></CENTER>
</TD>
</TR>
<xsl:apply-templates/>
</TABLE>
</xsl:template>
<xsl:template match="recapTeam">
<TR>
<TD BGCOLOR="#ffe0e0"><xsl:value-of select="team"/></TD>
<TD BGCOLOR="#ffe0e0">
<CENTER><xsl:value-of select="firstHalfScore"/></CENTER>
</TD>
<TD BGCOLOR="#ffe0e0">
<CENTER><xsl:value-of select="secondHalfScore"/></CENTER>
</TD>
<TD BGCOLOR="#ffe0e0">
<CENTER><xsl:value-of select="score"/></CENTER>
</TD>
</TR>
</xsl:template>
</xsl:stylesheet>
|
下面的样式表挑选出 XML 片断。
篮球 XML 中的
<recap> 元素
...
<recap>
<recapTeam>
<team>Georgia Tech</team>
<firstHalfScore> 21 </firstHalfScore>
<secondHalfScore> 39 </secondHalfScore>
<score> 60 </score>
</recapTeam>
<recapTeam>
<team>North Carolina State</team>
<firstHalfScore> 24 </firstHalfScore>
<secondHalfScore> 48 </secondHalfScore>
<score> 72 </score>
</recapTeam>
</recap>
...
|
上面的样式表产生以下输出:
Testing
|
1st
|
2nd
|
Total
|
Georgia Tech |
21
|
39
|
60
|
North Carolina State |
24
|
48
|
72
|
合并两个样式表
现在,我有两个样式表:
skeleton.xsl
和
recap.xsl
。我可以通过将这两个样式表导入到名为
runtime.xsl
的新样式表非常方便地合并它们。结果如下所示。
runtime.xsl
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'>
<xsl:import href="recap.xsl"/>
<xsl:import href="skeleton.xsl"/>
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
|
可以在运行期间将该样式表作为 Web 应用程序的一部分应用到篮球 XML
数据。样式表中通过不同 XSLT 模板的流程如下所示:
- 首先匹配篮球数据的根节点。
runtime.xsl
中具有
match="/"
的模板具有较高的导入优先权,并覆盖
recap.xsl
中的等价模板。后者只是用于
recap.xsl
的独立测试中触发其它模板,因此,这就是我想要的行为。XSLT
处理器应用这些模板。
- 要匹配的下一个模板是
skeleton.xsl
中具有
match="game"
的模板。它将持续输出
HTML,直到到达要输出得分数据的
<xsl:apply-templates>
元素为止。
- 接下来要触发的模板是
recap.xsl
中具有
match="recap"
的模板。它构建得分表标题行,然后应用模板来匹配用于构建表数据行的
<recapTeam>
元素。
- 构建得分表之后,就没有更多要匹配的输入 XML
节点了。此时,控制返回到
match="game"
模板,该模板输出
HTML 的其余部分。
现在,我已经实现了具有目标视觉和感觉效果的动态数据部分。下一步是实现导航栏。
导航栏
假设在实际站点上,导航栏在站点上的所有页面中出现,并具有特定于当前页面的标题。在导航栏上有一些从该页出发的固定链接,对于该站点的不同页面,这些链接可能相同,也可能不同。这些链接只取决于当前页面的内容。导航栏不依赖于篮球
XML 数据。
这一功能的实现与我刚刚对动态数据所执行的操作类似:将
skeleton.xml
文件中 HTML 标记的 "Navigation bar" 部分替换成触发 XSLT 代码的某个标记。然而这一次,XSLT 将不由篮球数据触发,并且我希望用于构建导航栏的所有 XSLT 代码在预处理阶段执行,而不要保留到运行期间。
导航窗口小部件
首先,我将在
skeleton.xml
文件中希望出现导航栏的位置上放一个标记。我采用以下片断:
<layout match="game">
...
<table cellspacing="0" cellpadding="0" border="0" height="100%" width="145">
<tr><td><img src="spacer.gif" width="100" height="100"/></td></tr>
<tr>
<td align="center" valign="top">
<font color="white"><h2>Navigation bar</h2></font>
</td>
</tr>
</table>
...
|
并将它编辑成:
<layout id="Recap" match="game"
xmlns:widget="http://www.hursley.ibm.com/widget" >
...
<table cellspacing="0" cellpadding="0" border="0" height="100%" width="145">
<tr><td><img src="spacer.gif" width="100" height="100"/></td></tr>
<tr>
<td align="center" valign="top">
<widget:navbar/>
</td>
</tr>
</table>
...
|
我在这里发明了
navbar
标记,并在新的
widget
名称空间中指定它。通常,当发明标记来以某种新方式标记已有 XML
时,最好将新标记放在新的名称空间中,以防止可能发生的名称冲突。这样做的更有利的原因是:可以通过在
XSLT 模板中指定
match="widget:*"
来以类的形式查找和操纵页面骨架中的“窗口小部件”。
还要注意添加到
<layout>
元素的
id="Recap"
属性。它标识这个页面骨架应用于哪个特定页面。此信息用于确定在那个页面的导航栏上出现的内容。
接下来,我编写了一个 XSLT 样式表,该样式表将由
<widget:navbar/>
标记触发,并将该标记替换成正确的 HTML,以形成所期望的页面导航栏。这样的样式表看起来可能类似于下例。
navbar.xsl
<?xml version='1.0'?>
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0'
xmlns:widget="http://www.hursley.ibm.com/widget">
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="widget:navbar">
<!-- Get the "id" attribute from the page skeleton <layout> tag -->
<xsl:variable name="page" select="ancestor::layout[1]/@id" />
<!-- Navigation bar title: -->
<tr>
<td align="center">
<h2><font color="white"><xsl:value-of select="$page"/></font></h2>
</td>
</tr>
<!-- Navigation bar links: -->
<xsl:apply-templates select="document('')//widget:page[@id=$page]"/>
</xsl:template>
<xsl:template match="widget:page">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="widget:link">
<tr>
<td align="center">
<a href="{@to}"><xsl:value-of select="@title"/></a>
</td>
</tr>
</xsl:template>
<!--
A lookup table of navbar links out from each page:
-->
<widget:linkData>
<widget:page id="Recap">
<widget:link to="nowhere1" title="Team stats"/>
<widget:link to="nowhere2" title="Player stats"/>
<widget:link to="nowhere3" title="etc."/>
</widget:page>
<widget:page id="someOther">
<!-- and so on for the other pages in the application -->
</widget:page>
</widget:linkData>
</xsl:stylesheet>
|
此样式表获取在其中找到
<widget:navbar/>
标记的
<layout>
元素的
id
属性值,并在查表期间使用该属性作为键,以发现导航栏应该包含哪些源自该页面的链接。然后,构造并返回导航栏的
HTML。
最后一步是更新
pre-process.xsl
样式表,以利用 navbar 窗口小部件。我需要添加 XSLT 代码,如下所示:
pre-process.xsl - 第 2 版
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:out="http://www.hursley.ibm.com/hsandt/et/compile"
xmlns:widget="http://www.hursley.ibm.com/widget">
<xsl:output method="xml" indent="yes"/>
<xsl:namespace-alias stylesheet-prefix="out" result-prefix="xsl"/>
<xsl:import href="navbar.xsl"/>
<xsl:template match="/layout">
<out:stylesheet version="1.0">
<out:output method="html" indent="yes"/>
<out:template match="{@match}">
<xsl:apply-templates/>
</out:template>
</out:stylesheet>
</xsl:template>
<xsl:template match="*|text()">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="*|text()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="widget:*">
<xsl:apply-imports/>
</xsl:template>
</xsl:stylesheet>
|
现在,可以对
skeleton.xml
文件重新运行
pre-process.xsl
样式表,以创建新版的
skeleton.xsl
。
runtime.xsl
中的
<xsl:import>
获得新的
skeleton.xsl
。对篮球数据应用
runtime.xsl
后产生:
最后的结果
结束语
runtime.xsl 样式表由其它三个样式表
skeleton.xsl
、
recap.xsl
和
navbar.xsl
得到。构建这三个样式表所需的技能如下:
- 通过对
skeleton.xml
文件应用自动过程来构建
skeleton.xsl
。从静态 HTML 构建
skeleton.xml
文件非常简单,无需任何特殊的 XML 或 XSLT
技能。HTML 需要 Web 设计技能。
-
recap.xsl
需要有关的 XSLT 和应用 XML
的知识。它输出一些 HTML 标记,但不需要太多 HTML 技能,因为静态 HTML
可能包括一个样本数据表(它将全面指定表单元颜色、边框宽度等),而不是
"Dynamic data" 字符串。
-
navbar.xsl
需要 XSLT
知识,并且在更现实的示例中,可能还需要深入了解目标标记语言。
可以仔细地说明、开发和单独地测试样式表。生成动态数据的样式表和生成显示的样式表之间是有区别的。虽然 XSLT 程序员们需要同时理解显示标记和基本的商业 XML,但对于单独的 XSLT 程序员来说,无需同时理解二者。在本例的三个样式表中,技术要求最高的是
navbar.xsl
,但不管怎样,编写可重用的 XSLT 组件正是您施展才华的所在。
更进一步
pre-process.xsl
样式表独立于它所处理的页面骨架文件中的标记语言。在生成其它设备(例如,用于
WAP 电话的 WML
deck)的标记时,也可以应用完全相同的技术。创建新的窗口小部件只需编写一个适当的
XSLT 样式表,然后将一个
<import>
元素添加到
pre-process.xsl
即可。
随着要支持的页面骨架和窗口小部件数目的增加,管理和操纵所有这些组件的问题也随之成倍增加,因此需要一些工具来管理这种复杂的局面。但是,因为每个组件都是 XML,所以,这些工具本身也可以使用 XML。可以将页面骨架和窗口小部件存储在关系数据库中,并使用
document()
函数从 XSLT 访问它们。在构建这种开发工具方面,具有 Java API 的 XSLT 处理器(例如 Xalan)将特别有用。
参考资料
关于作者
|
|
|
Alan Knox 是英国汉普郡 Hursley
Park 的一名 IBM 软件工程师。Alan 于 1997 年从 Civil Service 跳槽到
IBM。从那时起,他参与了一些使现有 Web 应用程序适用于交互式电视和
WAP 电话的项目。可以通过
knox@uk.ibm.com 与 Alan
联系
|