8.2 XSLT入门
在XSL的几个部分中,XSL-FO的应用依然比较有限。而XSLT则发展得相当成熟,应用也非常广泛,以至于现在所说的XSL通常都是指XSLT。当然,XSLT总是需要和XPath结合使用。
8.2.1 XSLT转换入门
对许多初学者而言,XSLT转换有点难于理解,其关键在于没有很好地掌握XSLT转换的方法。下面通过一个简单的转换来看一下XSLT转换。
下面是一份简单的XML文档:
程序清单:codes\08\8.2\book.xml
上面的XML文档中的粗体字代码是一条处理指令,用于应用XSLT样式单,应用XSLT样式单和应用CSS并没有太大的差别:只是简单地将type属性改为text/xsl而已。
下面是该XML文档的样式单代码:
程序清单:codes\08\8.2\book.xslt
大部分初学者看到这份样式单会感到既熟悉又迷惑,熟悉是因为这份样式单文件本身是一份有效的XML文档,而且其中包含了大量的HTML标签,迷惑大致是由文档中的几行粗体字代码所引起的。
上面的①号粗体字代码定义了XSLT的根元素,该根元素对所有XSLT都是完全一样的,XSLT可以使用如下两个根元素:
<stylesheet…/>。
<transform…/>。
上面两个根元素的作用和用法完全一样,可以相互替代。习惯上,我们更愿意使用<stylesheet…/>元素作为XSL的根元素。该根元素通常可以接受如下两个属性:
id:指定该根元素的唯一标识,通常没有太大的作用,无须指定。
version:指定该XSLT文档的版本。目前XSLT有两个版本:1.0和2.0,本章主要介绍XSLT 1.0。
XSLT本身也是一份XML文档,因此也需要使用Schema作为语义约束,XSLT语义约束的命名空间是:http://www.w3.org/1999/XSL/Transform,因为定义XSLT文档时在根元素中指定了xmlns:xsl="http://www.w3.org/1999/XSL/Transform",所以使用该Schema里定义的组件时应使用xsl作为前缀。也就是说,上面的代码中②、③、④号代码处所使用的元素都是来自该Schema。
绝大部分时候,我们都选择xsl作为XSLT组件的前缀。当然,开发者也可以选择自己想要的前缀,只要修改此处的xmlns:xsl中的xsl即可。
提示
关于XSLT 1.0和XSLT 2.0的官方介绍和详细说明,读者可通过http://www.w3.org/TR/xslt和http://www.w3.org/TR/xslt20/查阅,前者是XSLT 1.0的详细介绍,后者是XSLT 2.0的详细介绍。
XSLT处理器会将XML文档当成一棵结构化的树进行处理。值得指出的是,XSLT所认为的根节点和XML根节点不同:XSLT所认为的根节点是XML文档本身,而XML文档的根节点是XSLT所认为根节点的子节点之一。因此,在XSLT看来,上面的book.xml文档可转化为图8.1所示结构树。
图8.1 book.xml对应的结构树
学生提问:XSLT为什么不直接将XML根元素当成根元素呢?
答:一份XML文档除了XML根元素之外,还有其他内容:例如上面的文档之中的处理指令,还有XML根节点之前的注释等,这些内容都不在XML根元素之内。如果XSLT直接使用XML根元素作为根元素,而总是根据节点的父子关系来访问XML文档内容,则该XSLT将无法访问XML文档的处理指令和根元素之外的注释等。如果XSLT直接使用XML文档作为根元素,并将处理指令、根元素之外的注释和XML根元素等都当成XML文档的子元素处理,那就可以访问XML文档里包含的所有内容了。
理解了XSLT所处理的结构树,下面来看XSLT提供的两个常用元素。
8.2.2 使用template元素定义模板
一份XSLT文档本质上通常由多个模板规则组成,这些模板规则由<template…/>元素来定义,每个<template…/>元素定义对哪些XML节点、应用怎样的模板进行转换——这句话里包含了2条必要信息:
哪些XML节点:由<template…/>元素的match属性指定。
怎样的模板:<template…/>元素内的内容就是模板。
确定了对某个XML节点该应用哪个模板之后,XSLT的处理规则就非常简单了:直接用模板内容替换该XML节点即可。
例如再定义如下XSLT文档:
程序清单:codes\08\8.2\simple.xslt
上面的XSLT文档中定义了一个模板规则:该模板匹配整个文档节点,模板内容是一个简单的字符串。如果使用该XSLT文档来转换上面的book.xml文档,则处理过程如图8.2所示。
图8.2 XSLT处理示意
从图8.2可以看出,该<template…/>元素的match属性为斜线(/)。match属性的值必须是一个XPath表达式,用于匹配XML文档的指定节点。斜线(/)匹配整个XML文档本身,该模板的内容是一个简单的字符串——XSLT文档所做的转换就是拿这个简单字符串替换该XML文档本身。转换后得到如下内容:
由此可见<template…/>元素所定义的模板处理是非常简单的:它只是进行简单的替换而已。
需要指出的是:如果XSLT仅仅是进行这种简单的替换,那所有XML文档被一份XSLT转换后将得到完全相同的结果。如果这样,那XSLT显然没有存在的意义了,因此<template…/>元素定义的模板内容中可以包含动态的内容——例如上面看到的<apply-templates…/>元素和<value-of…/>元素,这些元素都会随着源XML文档的改变而变化。
提示
从上面的介绍可以看出,我们完全可以把XSLT中的<apply-templates…/>、<value-of…/>等元素当成JSP标签库。如果一份JSP页面中没有脚本、没有标签库,那这份JSP页面将完全是静态的。类似地,如果<template…/>元素定义的模板内容里没有任何XSLT元素,那该<template…/>元素定义的模板内容也将完全是静态的。
8.2.3 使用apply-templates处理子节点
与<template…/>通常总是作为<stylesheet…/>的子元素不同,<apply-templates…/>用于通知XSLT使用模板转换子节点,因此该元素通常总是作为模板内容使用。
由于<apply-templates…/>总是作为模板内容使用,因此使用该元素时总在XML结构树的“上下文”中,例如上面的book.xslt中③号代码处使用了<apply-templates…/>元素,它位于match="/"的<template…/>元素之内,因此可认为它的上下文就是XML文档,它通知XSLT转换XML文档包含的每个子元素,也就是处理指令元素和<计算机书籍…/>根元素。
使用<apply-templates…/>元素时有如下可选的属性:
select:指定一个XPath表达式,用于指定<apply-templates…/>元素只处理该指定表达式代表的子节点。如果没有指定select属性,<apply-templates…/>将依次处理当前节点集内每一个子节点。
<apply-templates…/>元素会自动按节点集的顺序依次处理select属性匹配的所有节点,除非指定了自定义的排序规则。
<apply-templates…/>元素只是通知XSLT转换当前节点集的所有子节点,而实际转换则依赖于<template…/>元素定义的模板。
对于book.xslt中③号代码处的<apply-templates…/>元素,转换流程如下:
(1)转换XML文档内的处理指令元素:<apply-templates…/>到XSLT中搜寻能匹配处理指令的模板定义——没有找到,结束对处理指令的转换。
(2)转换XML文档的<计算机书籍…/>根元素:<apply-templates…/>到XSLT中搜寻能匹配<计算机书籍…/>元素的模板定义——找到XSLT定义中的第2个模板定义。接下来执行的处理也非常简单:用对应的模板内容逐项代替节点集中每个子节点。
经过上面的分析,可以知道book.xslt对book.xml进行转换后将得到如下文档:
<template…/>元素和<apply-templates…/>元素是XSLT中最常见、最重要的两个元素。经过上面的介绍,可以归纳出它们的功能:
<template…/>:定义一个模板替换XML文档中指定元素。
<apply-templates…/>:依次处理当前节点集内的每个子节点,用对应的模板内容逐项代替。
注意
如果<template…/>所定义的模板内容中不使用<apply-templates…/>通知XSLT去处理子节点,则XSLT会按默认的模板规则来处理所有子节点。关于XSLT的默认模板规则请参看后面的介绍。
假设有如下XML文档:
程序清单:codes\08\8.2\list.xml
上述XML文档的<favorite-list…/>节点下包含了3个子节点,对该文档使用如下XSLT进行转换:
程序清单:codes\08\8.2\list.xslt
上面的XSLT中①号代码通知XSLT依次处理每个子节点,当前上下文是XML文档根,当前节点集里只有一个<favorite-list…/>节点,此处的处理流程如下:
(1)开始处理<favorite-list…/>节点。
(2)XSLT开始搜索能匹配<favorite-list…/>节点的模板,找到②号代码处的模板定义,于是将该模板内容插入①处。
而②号代码处的模板定义在③号代码处又包含了一个<xsl:apply-templates/>,于是再次通过XSLT依次处理每个子节点。当前上下文是<favorite-list…/>节点,当前节点集里有3个<item…/>子节点,此处的处理流程如下:
(1)开始处理第1个能匹配<item…/>节点的模板,找到④号代码处的模板定义,于是将该模板内容插入③处。
(3)处理第2个<item…/>子节点,也就是重复前两个步骤。
(4)处理第3个<item…/>子节点。
经过上面的转换,可以得到如下代码:
使用浏览器来浏览上面的list.xml文档,可看到图8.3所示页面。
图8.3 XML转换后的效果
经过上面的介绍,读者应该已经掌握了<template…/>和<apply-templates…/>两个元素的用法,掌握它们的用法之后就可以从整体结构上把握XSLT文档了。
8.2.4 使用value-of输出节点内容
XSLT提供了一个<value-of…/>元素来输出XML节点的内容,使用该元素时可以指定如下两个属性:
select:必填属性,用来指定一个表达式,该表达式对应的内容将被转换成字符串然后输出(本质上是以该字符串创建一个文本节点然后输出)。
disable-output-escaping:可选属性,指定输出文本内容时是否禁用转义。
大多数时候,XSLT使用<value-of…/>来转换包含文本内容的节点,这种转换非常简单,<value-of…/>将直接输出该节点所包含的文本内容。
除此之外,<value-of…/>元素还可处理如下几种类型的内容。
8.2.4.1 转换包含子元素的节点
如果使用<value-of…/>元素来转换包含子元素的节点,它会采用深度优先的法则(第一个子节点、每个孙子节点、下一个子节点……),将每个文本节点所包含的字符串依次累加成一个字符串后返回。
例如有如下XML文档:
程序清单:codes\08\8.2\value-of\book.xml
上面的XML文档是一份简单的文档,该文档以<book…/>为根节点,根节点下依次包含了<name…/>和<price…/>两个子节点。如果直接使用<value-of…/>元素作用于上述文档中的<book…/>元素,例如如下XSLT文档所示:
程序清单:codes\08\8.2\value-of\includeChildren.xslt
上面的XSLT直接使用<value-of…/>元素来转换<book…/>元素,因此会按深度优先的法则来处理每个子元素,得到图8.4所示结果。
图8.4 使用<value-of…/>处理包含子元素的节点
8.2.4.2 转换属性
当使用<value-of…/>元素转换属性时,它会直接输出该属性的文本值。例如用如下XSLT转换上面的XML文档。
程序清单:codes\08\8.2\value-of\attribute.xslt
上面的XSLT直接使用<value-of…/>元素来转换<book…/>元素的isbn属性,因此会直接输出该isbn属性值,得到图8.5所示结果。
图8.5 使用<value-of…/>转换元素的属性
8.2.4.3 转换处理指令
当使用<value-of…/>元素转换处理指令时,它会直接输出该处理指令的内容(返回处理指令去掉处理指令名、<?和?>后剩下的内容)。例如用如下XSLT转换上面的XML文档:
程序清单:codes\08\8.2\value-of\pi.xslt
上面的XSLT直接使用<value-of…/>元素来转换XML文档的处理指令(就是导入样式单那条指令),可得到图8.6所示结果。
图8.6 使用<value-of…/>转换处理指令
8.2.4.4 转换注释
当使用<value-of…/>元素转换注释时,它会直接输出注释的内容(解释输出<!—和—>中间的内容)。例如用如下XSLT转换上面的XML文档。
程序清单:codes\08\8.2\value-of\comment.xslt
上面的XSLT直接使用<value-of…/>元素来转换XML文档的注释,可得到图8.7所示结果:
图8.7 使用<value-of…/>转换注释
通过上面的介绍可以看出,XSLT提供的<value-of…/>是一个功能非常强大的元素。从功能上来看,它有点类似于编程语言的输出语句,而且不仅可以转换文本节点的内容,还可以转换节点的属性和包含子元素的节点等。
经过上面的解释,现在应该可以完全看懂book.xslt的转换处理了,在浏览器中浏览对应的book.xml文档可以看到图8.8所示结果。
图8.8 使用XSLT转换XML文档
8.2.5 匹配节点的模式
前面已经提到,当使用<template…/>定义模板规则时需要指定一个select属性,该属性值是一个XPath表达式,用于匹配需要处理的节点。凡是能与该XPath表达式匹配的节点,XSLT都会使用该模板定义来转换该节点;反之,不匹配select属性值的节点将不会被转换。
本书下一章将详细介绍XPath语言的内容,故此处仅简单地给出几种匹配节点的模式:
匹配XML文档根
XML文档根使用斜线匹配,例如select="/",此处的文档根并不是XML根元素,而是包含XML根元素的文档根。
匹配元素节点
匹配元素节点也是较为常见的情形,例如select="para"就用于匹配当前节点集里的所有para节点。
匹配属性
前面已经看到,匹配属性需要增加@符号,例如select="@class"匹配当前节点里的所有class属性(不是匹配有class属性的子节点)。
匹配子节点
就像在Linux里使用斜线(/)表示子目录一样,XPath也可以使用斜线(/)来匹配子节点,例如select="olist/item"可以匹配当前节点集下父节点为olist的所有item节点。
匹配后代节点
使用双斜线可匹配后代节点,例如select="appendix//para"可匹配当前节点集下祖先节点为appendix的所有para节点。
使用星号(*)作为通配符
上面这几种匹配模式中,都可以使用星号()作为通配符,例如:可以使用select=""匹配当前节点集下的所有节点;也可以使用select="@"匹配当前节点的所有属性;还可以使用select="olist/"匹配当前节点集下父节点为olist的所有节点。
使用点号(.)匹配当前节点
单独一个点号(.)可代表当前节点,例如可以使用select="."匹配当前的任何节点。
使用两个点号(..)匹配上一级节点
连续两个点号(..)可代表上一级节点,例如可以使用select=".."匹配当前节点的父节点。
使用竖线(|)组合多个表达式
如果希望匹配多种类型的节点,可以使用|来组合多个表达式。例如select="chapter|appendix"可以同时匹配当前节点集里的chapter节点和appendix节点,select="chapter|crazyit/@address"可以同时匹配当前节点集里的chapter节点和crazyit节点的address属性。
使用[]对表达式增加限定
[]里可以指定一个布尔表达式,用于对前面所介绍的表达式进行限定,例如:select="para[last()=1]"匹配当前节点集下唯一的一个para元素;select="para[position()=2]"匹配当前节点集下第2个para元素;div[@class="appendix"]匹配当前节点集下class属性值为appendix的所有div元素。
提示
上面的实例中用到的last()和position()都是XPath语言里的函数,其中last()函数返回指定上下文内所包含的节点个数,position()函数返回指定节点在节点集里的位置索引。
使用node()匹配节点
例如select="node()"可以匹配除属性和XML文档根之外的所有节点。
使用text()匹配文本节点
例如select="text()"可以匹配当前上下文中的所有文本节点。
使用comment()匹配注释
例如select="comment()"可以匹配当前上下文中的所有注释。
使用id()根据ID匹配
如果想精确匹配XML文档里的指定元素,可以使用id()表达式根据元素的ID属性值直接进行匹配,其中的id()表达式里可以接受一个ID属性值。例如id("W11")匹配ID类型的属性值为W11的元素。需要指出的是:id()表达式接受的属性必须是ID类型的,不管是用DTD声明该属性也好(参见3.6.2.2),还是用Schema声明该属性也好,只有该属性声明为ID类型时id()表达式才有效。
8.2.6 mode属性
在某些情况下,我们需要对一个元素进行多次转换——相当于有一个数据模型,但需要以不同的视图将它显示出来。这时就需要对同一个元素应用多个模板规则进行转换了,那么如何才能让XSLT有效地区分多个模板规则呢?答案是使用mode属性。
不管是<template…/>元素,还是<apply-templates…/>元素,都可以指定一个可选的mode属性,开发者可以任意指定该属性的属性值,该属性值的唯一作用就是区分多个模板规则。例如如下两行代码:
上面定义的2个模板规则都将对abc节点进行转换,但这两个模板规则使用mode属性进行了区分,因此这两个模板规则是不同的。
同样地,<apply-templates…/>也可以指定mode属性。只有当<apply-templates…/>的mode属性和<template…/>的mode属性相同时,该模板规则才会生效。
对于如下XML文档:
程序清单:codes\08\8.2\mode\book.xml
如果希望将<计算机书籍…/>元素以不同的方式进行转换,就需要为它定义多个转换模板了,下面是我们所使用的XSLT转换文件:
程序清单:codes\08\8.2\mode\book.xslt
上面的XML文档中①、②号代码处两次使用了<xsl:apply-templates/>来指定XSLT处理子元素,前一个<xsl:apply-templates/>元素没有指定mode属性,因此会使用没有指定mode属性值的模板定义来进行转换;后一个<xsl:apply-templates/>元素指定了mode="aa"属性,因此会使用mode="aa"的模板定义来进行转换。在浏览器中浏览上面的book.xml文档,将看到图8.9所示的转换结果。
图8.9 对一个元素进行多次转换
需要指出的是,使用<template…/>定义模板规则时如果没有指定match属性,则也不能指定mode属性。