Vincent.Chan‘s Blog

常用链接

统计

积分与排名

网站

最新评论

样式表也能编写样式表:由 XSLT 组件制作 XSLT 样式表

级别: 初级

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 样式表可以使用 importinclude 机制由其它样式表构成。只要适当留意,您可以分别开发一些独立的 组件样式表,这些组件样式表可以一起构成将在运行期间应用于 XML 数据的 显示样式表。这些组件将大致分为三种类型,用于处理:

  • 显示动态 XML 数据(在我的示例中是篮球数据)
  • 可重用的显示标记部分,例如按钮栏
  • 页面残余部分

下例演示了这种解决方案。

用户界面设计

您可能希望您的 Web 应用程序看上去和感觉起来设计得更专业。布局美观的 HTML 页面往往意味着复杂标记、具有各种嵌入式 HTML 表的安排以及精确的格式化和间距命令。在本文中,我假设已有某个表示下面所示 页面设计的静态 HTML。任务是用一个显示样式表重新生成相同的外观和感觉。


目标外观和感觉
设计 HTML 图

虽然这只是一个简单的示例页面,但它也有一些嵌入的 HTML 表。它将只是多个页面中的一个,这些页面显示的数据来自生成 XML 形式的篮球统计信息的应用程序。我将用 篮球 XML 中的数据替换 "Dynamic data"(动态数据),然后使用 "Navigation bar"(导航栏)列出从当前页开始的链接。

采取的步骤

要生成显示样式表,需要:

  1. 从设计 HTML 制作一个样式表。
  2. 制作一个将篮球得分格式化成 HTML 的样式表。
  3. 将二者合并。
  4. 制作一个生成“导航”栏的 XSLT“窗口小部件”。
  5. 具体化这个窗口小部件,以生成页面的最终样式表。




回页首


从设计 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.xslrecap.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.xslruntime.xsl 中的 <xsl:import> 获得新的 skeleton.xsl 。对篮球数据应用 runtime.xsl 后产生:

最后的结果
运行时 HTML 图





回页首


结束语

runtime.xsl 样式表由其它三个样式表 skeleton.xslrecap.xslnavbar.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 组件正是您施展才华的所在。

When to use sidebars

请记住,在运行期间也要管理生成的样式表,并确保对每个给定请求,都应用了正确设备的正确样式表。

IBM WebSphere Transcoding Publisher 可以替您这样做,这样,应用程序的 XSLT 变换在逻辑上(如果您愿意,也可以在物理上)就与产生 XML 的商业逻辑分隔开。

更进一步

pre-process.xsl 样式表独立于它所处理的页面骨架文件中的标记语言。在生成其它设备(例如,用于 WAP 电话的 WML deck)的标记时,也可以应用完全相同的技术。创建新的窗口小部件只需编写一个适当的 XSLT 样式表,然后将一个 <import> 元素添加到 pre-process.xsl 即可。

随着要支持的页面骨架和窗口小部件数目的增加,管理和操纵所有这些组件的问题也随之成倍增加,因此需要一些工具来管理这种复杂的局面。但是,因为每个组件都是 XML,所以,这些工具本身也可以使用 XML。可以将页面骨架和窗口小部件存储在关系数据库中,并使用 document() 函数从 XSLT 访问它们。在构建这种开发工具方面,具有 Java API 的 XSLT 处理器(例如 Xalan)将特别有用。





回页首


参考资料





回页首


关于作者

author

Alan Knox 是英国汉普郡 Hursley Park 的一名 IBM 软件工程师。Alan 于 1997 年从 Civil Service 跳槽到 IBM。从那时起,他参与了一些使现有 Web 应用程序适用于交互式电视和 WAP 电话的项目。可以通过 knox@uk.ibm.com 与 Alan 联系

posted on 2006-03-21 23:49 Vincent.Chen 阅读(459) 评论(0)  编辑  收藏 所属分类: XML


只有注册用户登录后才能发表评论。


网站导航: