出处:微软文档
在理想情况下,XSLT 筛选器是一个“黑匣子” - 这个筛选器将获取一个或多个输入并创建一个或多个输出。您并不需要知道“黑匣子”里发生了什么。在函数中,将信息传递到黑匣子中是由参数完成的。在 XSL 文档中,这一过程也由参数 - 具体来说是由 <xsl:param> 元素来完成。这些 XSLT 元素在声明环境中(数据在这里流入,被转换,然后流出)的一些作用与函数参数在程序环境中(在这里,日常代码占绝对优势)的作用相同。但是两者并不完全相同。
XSLT 也包含变量。变量与参数之间的主要区别在于:参数旨在将内容从一些外部源分配到它当前的模板,而变量是在内部分配。在传统参数中同样有一种类似情况:在函数主体中声明的变量通常是私有变量,调用函数的代码不需要知道或关心是否存在这样的变量。从这方面来看,XSLT 变量类似于私有变量。
考虑 xsl:param 或 xsl:variable 元素的最好方法是:一旦将参数与名称和内容联系起来,这个元素就不可改变了;您也可以将它写入到一个一次写入多次读取的 CD-ROM 中。这个位置本质上已经被占有,如果您尝试创建另一个名称相同的参数(至少在相同范围内;在下文中会详细讲述),在这个位置上会遭到分析器的拒绝。这与大多数程序语言中的参数区别很大,大多数程序语言中的参数可以很容易地在函数内部进行修改。
另外,xsl:param 与 xsl:variable 元素事实上有两种截然不同的赋值方法。这种情况可在 W3C规范中 xsl:param 对象的正式描述中找到:
<xsl:param name = qname select = expression> <!-- Content: template --> </xsl:param> <xsl:variable name = qname select = expression> <!-- Content: template --> </xsl:variable>
设置 xsl:param 元素内容的最明显的方法是:将元素分配到它内部的内容(<xsl:param> 和 </xsl:param> 标记之间的内容)中。举一个简单的例子,可以只将文本分配到参数中。例如,如果要将一个人的姓传递到一个参数中,您只将它作为内部内容包含:
<xsl:param name="last_name">Cagle</xsl:param>
一旦用这种方法定义了参数,以后就可以通过为它的名称添加美元符号前缀 ($) 在 XSLT 中引用这个参数了(顺便提一下,这种方法同样支持变量)。例如,要将参数插入到模板的 <last_name> 元素中,您将使用 xsl:value-of 元素:
<last_name><xsl:value-of select="$last_name"/></last_name>
但是,也可以将 XML 标记分配给参数。例如,假设想要创建一个人的完整记录。您可以通过 param 语句来完成:
<xsl:param name="person"> <record> <first_name>Kurt</first_name> <last_name>Cagle</last_name> <vocation>Writer</vocation> </record> </xsl:param>
处理这种格式中的结构有些复杂,但并不是非常复杂。如果尝试使用表达式 <xsl:value-of select="$person">,那么所有将写入输出流的是:
KurtCagleWriter
这很可能不是您希望得到的。相反,<xsl:copy-of select="$person> 语句使您输出了整个 XML 结构:
<record> <first_name>Kurt</first_name> <last_name>Cagle</last_name> <vocation>Writer</vocation> </record>
进行到这里就变得很有趣了。关于参数(和它们的同辈,即变量)要记住的重要一点就是:分析器遇到参数或变量时会自动解释它们,而不是存储匹配模式,以后再进行解释。如果分析器在变量或参数中遇到一个 XSL 表达式,表达式则会在此时自动计算,并写入到与该参数名称相关联的空间中。 这个奇特的小特性使参数和变量非常有用。
例如,以下面的内容为例:
<xsl:param name="first_name">Kurt</xsl:param> <xsl:param name="last_name">Cagle</xsl:param> <xsl:param name="vocation">Writer</xsl:param> <xsl:param name="record"> <first_name><xsl:value-of select="$first_name"/></first_name> <last_name><xsl:value-of select="$last_name"/></last_name> <vocation><xsl:value-of select="$vocation"/></vocation> </xsl:param>
记录参数计算前面三个参数的值,所以不管这些参数设置为什么,该记录都将反映这些参数的更新值。现在将简单的参数输入到 XSL 文档中以创建更复杂的对象就变得非常容易了。
Result Tree Fragment
但是,它有一个主要的局限性。 W3C XSLT 委员会中一个比较激烈的争论涉及如何处理变量内容的问题。变量内容是指存在于元素标记中的那部分元素(换言之,也就是元素的“文本”)。当执行 XSLT 变量的 select 语句时,select 语句将一个节点集分配给该变量,操作这个节点集的方式就好像操作文档中其他节点集一样。
另一方面,W3C 决定,即使节点中的内容格式完整,那些被设置为错误格式的内容所构成的危险也足以将该内容指定为它自己的唯一类型,称为result tree fragment。在 XSLT 中这样的段被当作二等公民。虽然能够执行一些操作(例如获取最高节点的名称),但是不能对它应用大多数 XPath 定位运算符。
所以根据 XSLT 规范,对于上面所描述的记录,表达式:
<xsl:value-of select="$record/first_name"/>
仍然不合法,尽管已经分配了作用于现有输入流 select 值中的记录:
<xsl:variable name="record select="//record[1]"/>
那么相同的语句将完全合法。
注意 MSXML Technology Preview 分析器的当前行为并没有认识到这个局限性。从变量元素内容中创建的段被认为与 select 语句中的段具有相同的权限。这在很大程度上是一个很重要的功能,因为它提供了一种从一个指定的模板操作结果中访问元素的方法(模板能像函数一样被调用,而不能通过搜索模式进行匹配;这一点在后面的专栏中即将讨论)。例如,如果模板对元素进行了排序并筛选了元素,会使您对这些已排序的元素进行迭代,而不是对未筛选的集合进行迭代 - 这是创建“分页”输出的一个重要功能。
在这方面为了与 W3C 行为协调一致,Microsoft 最后很可能会在最终的 MSXML Web 版本中弃用这种主要功能,但可能通过如下所示的一个扩展函数使这种功能能够用于最终的 MSXML Web 版本:
<xsl:value-of select="msxsl:node set($record)/first_name"/>
另外,Microsoft 之所以急切地将这种功能包含在还处于讨论中的 XSLT 2.0 规范中,是因为将内容视为全状态节点集的能力在很大程度上扩展了 XSLT 的功能。
Select Vintage
Select 属性提供了第二种用来填充参数或变量的方法。带有参数的 select 语句与 select 语句在任何其他地方的工作方式完全相同:如果能将该属性中的表达式解释为一个 XPath 表达式,分析器将试图找到满足这个特定表达式的所有节点,然后将它们放到变量中。例如,假设您的输入流包含了显示许多人名字和职业的大量记录。您可以设置一个变量(这里并不是真正需要一个参数)以检索初始流中所有职业为作家的人:
<xsl:variable name="writers" select="//record[vocation='Writer']">
一旦拥有了这个变量,您实际上就拥有了一个节点段,这个段将 $writers 作为根,并只将职业为作家的那些人当作子节点。如果想要循环访问这个集合,只要记住 $writers 是根节点,就可以用 <xsl:apply-templates> 或 <xsl:for-each> 元素轻易地将上下文设置到每个节点:
<xsl:template match="/"> <ul> <xsl:for-each select="$writers/*"> <li><xsl:value-of select="first_name"/><xsl:text> </xsl:text></xsl:value-of select="last_name"/></li> </xsl:for-each> </ul> </xsl:template>
这样就会创建一个带项目符号的 HTML 列表,其中列出了初始流中的全部作家的。注意,这样做的一个效果是上下文(您在文档中的位置)不再依赖模板的即时上下文。顺便提一下,表达式 <xsl:text> </xsl:text> 在输出中放入了一个空格字符。记住 XSLT 为生成 XML 进行了优化,而没有对于原始文本进行优化,这一点很恼人,但是非常重要。
Select 语句将自动重写变量或参数标记的内容,尽管在技术上将同时使用两者认为是 XSLT 冲突。很庆幸,它没有在 MSXML 分析器中引发错误,因为有时候拥有这两者很方便。
您也能使用 select 语句来计算表达式或包含文本 - 但是在后一种情况下必须十分小心。XSLT 分析器将自动在 select 属性中找到的任何文本解释为 XPath 表达式的一部分,除非该文本被专门添加了双重引号(双引号内有单引号,或者单引号中有双引号)。所以,能将 “writer” 的职业字段设置为:
<xsl:param name="vocation" select="'writer'"/>
计算表达式比较复杂,但并不是很复杂。基于元素的当前上下文,将 select 属性当作到任何 XPath 表达式的窗口非常有用。除了能够定位主 DOM,XPath 还包含许多函数,它们能够用于帮助计算表达式,这些表达式可以包含算法、字符串、以及大大扩展 XSLT 功能的其他运算。例如,假设有一个能提供发票项目列表的 XML 结构,其中包含了任何特定项目的数目和其中任一项目的价格(如下所示)。
<lineItems> <lineItem> <code>42AC5</code> <title>Loopy Fruit Cereal</title> <amount>12</amount> <cost>4.25</cost> </lineItem> <lineItem> <code>H343A</code> <title>MicroSecond Rice</title> <amount>14</amount> <cost>2.35</cost> </lineItem> <lineItem> <code>EA198</code> <title>Crescent Toothpaste</title> <amount>18</amount> <cost>1.95</cost> </lineItem> </lineItems>
然后就可以通过使用一些 XPath 固有的内置函数,将整个行项目集合的总价格分配给一个变量:
<xsl:variable name="lineItemSubTotals"> <xsl:for-each select="//lineItem"> <subTotal><xsl:value-of select="number(amount)*number(cost)"/></subTotal> </xsl:for-each> </xsl:variable> <xsl:variable name="lineItemsTotal"> <xsl:value-of select="sum($lineItemSubTotals/subTotal)"/> </xsl:variable>
在这种情况下,变量 $lineItemSubTotals 创建一个由 <subTotal> 元素组成的从属 XML 结构并将它们分配到 lineItemTotals 变量中。对于以上示例,本质上等同于下列内容:
<xsl:variable name="lineItemTotals"> <subTotal>51</subTotal> <subTotal>32.9</subTotal> <subTotal>35.1</subTotal> </xsl:variable>
第二个变量 $lineItemsTotal,使用sum() XPath 函数将项目总数加到一起,然后将得到的值 119 赋予 lineItemsTotal 变量。这种方法与程序方法的不同之处并不在于算法(这很简单)方面,而是因为在这个事务处理中没有使用包含每个行项目条目之和的变量 $lineItemsTotal。您还可以更改记录的输出,以便它会读取行项目,并创建一个添加了该字段的新行项目,还保留了小计信息以计算总和:
<xsl:variable name="newLineItems"> <xsl:for-each select="//lineItem""> <lineItem> <xsl:copy-of select="*"/> <subTotal><xsl:value-of select="number(amount)*number(cost)"/></subTotal> </lineItem> </xsl:for-each> </xsl:variable> <xsl:variable name="lineItemsTotal"> <xsl:value-of select="sum($newLineItems/subTotal)"/> </xsl:variable>
这套代码会创建一套新的行项目并执行求和,从而阐释了 XSL 筛选器能够在本质上增大(或者同样也能减小)一个现有的 XML 文档以合并中间处理信息的方法。
设置参数
要使用参数,就需要设置参数。假设这里所描述的 XSLT 是为了在 ASP 环境中运行,您还必须在 ASP 中设置参数。设置参数有许多方法,它们首先取决于您如何向服务器传输信息,尽管在多数情况下会涉及 XML DOM。
在几乎所有的情况中,设置实际参数都涉及设置参数的 select 属性或者将文本分配到该属性中。例如,通过使用 Visual Basic 脚本版本 (VBScript) 中的一些 DOM 调用,您能够设置属性,以创建一个用于 addRecord.xsl 筛选器的新记录 - 假设 first_name、last_name 与 vocation 属性是作为查询字符串进行传送的:
<% firstName=request.queryString("first_name") lastName=request.queryString("last_name") vocation=request.queryString("vocation") set addRecordFilter=createObject("MSXML2.DOMDocument") addRecordFilter.async=false addRecordFilter.load server.mapPath("addRecord.xsl") set firstNameNode=addRecordFilter.selectSingleNode("//xsl:param[@name= 'first_name']") firstNameNode.setAttribute "select","'"+firstName+"'" set lastNameNode=addRecordFilter.selectSingleNode("//xsl:param[@name= 'last_name']") lastNameNode.setAttribute "select","'"+lastName+"'" set vocationNode=addRecordFilter.selectSingleNode("//xsl:param[@name= 'vocation']") vocationNode.setAttribute "select","'"+vocation+"'" set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" response.write stubDoc.transformNode(addRecordFilter) %>
例如,表达式 "//xsl:param[@name='first_name']" 会访问名称为 first_name 的参数,并用从查询字符串(括在单引号中以强制将它们作为字符串进行计算,而不是作为 XPath 表达式计算)检索到的值替换 select 属性的内容。
通过使用 XSLTemplate 能在一定程度上简化该过程,XSLTemplate 是 Technology Preview 分析器的一部分。下面的处理程序(从该模板中派生)为更新参数和对象提供了许多接口:
<% firstName=request.queryString("first_name") lastName=request.queryString("last_name") vocation=request.queryString("vocation") set addRecordFilter=createObject("MSXML2.DOMDocument") addRecordFilter.async=false addRecordFilter.load server.mapPath("addRecord.xsl") set template=createObject("MSXML2.XSLTemplate") set template.stylesheet=addRecordFilter set processor=template.createProcessor processor.AddParameter "first_name",firstName processor.AddParameter "last_name",lastName processor.AddParameter "covation",vocation set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" processor.input=stubDoc processor.transform processor.output=response %>
注意,XSLT 文档实际上并不关心与它正在转换的 XML 文档有关的任何事情。文档的唯一的目的就是强制输出。这说明使用 XSLT 时并不明确需要使用主输入流。另外,使用指定的模板比使用变量更有意义,以后的专栏将讨论这个话题)。实际的工作是用 document() 和 persistDocument() 函数完成的,这两个函数会在记录结构中进行加载,向记录中添加节点(已排序),然后保存那些结构。
这里的目的是尽量减少特定于应用程序的代码,这样就能避免大量的代码重写工作。实现这个目的的一种途径是枚举 queryString 和 Form 集合以检索可能的参数值,如果这样的参数存在的话,就将值分配给参数。
这也引出了一个有趣的可能性。您能够将 XSL 筛选器的名称作为参数以这种相同方式进行传递,该名称包含在变量 ”filter” 中。这样,相同的 ASP 文件能够以更普通的方式处理任何数目的 XSLT 筛选器的运行了。
<% ' XSLServer.asp function main() xslFilter=request("filter") if right(xslFilter,4,1)<>"." Then xslFilter=xslFilter+".xsl" end if set xslDoc=createObject("MSXML2.DOMDocument") xslDoc.async=false xslDoc.load server.mapPath(xslFilter+".xsl") for each key in request.form set paramNode=xslDoc.selectSingleNode("//xsl:param[name='"+key+"']") if not (paramNode is nothing) then paramNode.setAttribute "select","'"+request(key)+"'" end if next for each key in request.queryString set paramNode=xslDoc.selectSingleNode("//xsl:param[name='"+key+"']") if not (paramNode is nothing) then paramNode.setAttribute "select","'"+request(key)+"'" end if next set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" response.write stubDoc.transformNode(xslDoc) end main ? main %>
注意,只有在 XSLT 参数已经存在的情况下,查询字符串或表单参数才会映射给一个 XSLT 参数。这也提供了一种将 XSL 文件名称传递到筛选器代码的方法。设置一个名为 filter 的参数即可。注意,如果没有另外提供扩展名,代码还将字符串 .xsl 追加到筛选器名称中。这种方法只是使筛选器看起来更像命令而非文件名。
这样,查询字符串:
http://www.myserver.com/xslserver.asp?filter=addrecord &first_name=Kurt&last_name=Cagle&vocation=Writer
将将信息传递到 addRecord.xsl 筛选器。这为功能非常强大的基于 URL 的API 约定提供了基础 - 方法是使用一个筛选器,其中包含要使用方法的名称,以及其他包含相关参数的所有其他查询字符串参数。
您也能够将 XML 节点、XML 节点集(通过选择接口),甚至整个 DOM 文档作为参数传递。在这种情况下,您将元素作为子节点追加到参数中,并删除 select 语句即可:
<% ' XSLServer.asp function main() set newUserDoc=createObject("MSXML2.DOMDocument") newUserDoc.load "newUser.xml" set xslDoc=createObject("MSXML2.DOMDocument") xslDoc.async=false xslDoc.load server.mapPath("addRecord.xsl") set newRecordNode=xslDoc.selectSingleNode("//xsl:param[name='record']") newRecordNode.appendChild newUserDoc.documentElement newRecordNode.removeAttribute "select" set stubDoc=createObject("MSXML.DOMDocument") stubDoc.loadXML "<stub/>" response.write stubDoc.transformNode(xslDoc) end main ? main %>
这是另一种情况,其中使用 XSL 处理器与使用 transformNode 函数意义等同甚至比使用这个函数更有意义。使用 XSLT 的主要问题是每次分析样式表时,您都需要执行转换,这样非常耗费资源 - 尤其是假设在许多情况下,您要处理在每种情况下使用的相同 XSLT 脚本。用 Processor对象(保存在对话变量中)呈现的相同功能也按照下面的内容进行编写:
<% ' XSLServer.asp function main() if typename(session("addRecord"))="IXSLProcessor" then set addRecord=session("xslProc") set stubDoc=session("stubDoc") else set xslDoc=createObject("MSXML2.DOMDocument") xslDoc.async=false xslDoc.load server.mapPath("addRecord.xsl") set xslTemplate=createObject("MSXML2.XSLTemplate") set xslTemplate.stylesheet=xslDoc set addRecord=xslTemplate.createProcessor session("addRecord")=addRecord set stubDoc=createObject("MSXML.DOMDocument") stubDoc.async=false stubDoc.loadXML "<stub/>" session("stubDoc")=stubDoc end if set newUserDoc=createObject("MSXML2.DOMDocument") newUserDoc.load "newUser.xml" addRecord.addParameter "record",newUserDoc addRecord.input=stubDoc addRecord.output=response addRecord.transform end main ? main %>
这与前两个示例功能相同,只是它将一个 DOM 树(而非单个字符串)传递到 XSL 样式表中。当您想要将处理过的节点传回到 XSLT 筛选器时会很方便。
有关 XSLTemplate 与 XSLProcess 对象的更多信息,请参阅 Chris Lovett 所著的《 Inside MSXML3 Performance》。
小结
通过提供一种保存临时信息的机制,为不传递的属性提供默认值,用作外部文档的根节点,以及公开使样式表更动态的方法,变量和参数能极大地扩展 XSLT 所能实现的功能。通过提供对其他组件的引用或允许计算设备位置的各种方法,您尤其能够将参数(特别是与脚本一起配合使用)用作使用这些组件的工具。应该将参数当作任何 XML 开发人员指令系统的关键部分。