開始之前
這一節介紹本教程的主要內容以及如何充分利用它。
關於本系列教程
本系列教程共分為 5 部分,其目的是幫助您參加 IBM 認證考試 142 “XML 及相關技術”,以便通過 IBM CertifIEd Solution Developer——XML 及相關技術認證。通過該認證表明您達到了中級開發人員的水平,能夠使用 XML 及相關技術設計和實現應用程序,比如 XML Schema、可擴展樣式表語言轉換(XSLT)和 XPath。這些開發人員對 XML 基礎有深刻的理解;清楚 XML 概念和相關技術;了解數據和 XML 的關系,特別是與信息建模、XML 處理、XML 呈現以及 Web 服務有關的問題;對與 XML 有關的核心萬維網聯盟(W3C)推薦標准有一個全面的了解;熟悉常見的最佳實踐。
關於本教程
本教程是“准備 XML 及相關技術認證考試”系列教程的第 4 部分,重點介紹 XML 轉換。詳細說明了以各種不同的方式訪問 XML 文檔中的數據。本教程假設您已經閱讀了第 3 部分和第 2 部分,第 3 部分討論了 XML 處理、第 2 部分介紹了 DTD 和 XML Schema 驗證(請參閱參考資料),這兩部分內容為本期教程打下了基礎。沒有這些基礎就無法學習 XML 轉換。
本教程是為那些對 XML 有一定了解、經驗技巧接近中級水平的程序員和腳本作者而編寫的。因此應該對數據類型有一定的了解,特別是數組、圖和樹。還應該熟悉一般的編程技術,如迭代和遞歸。雖然本教程一開始討論了這些技術的基礎知識,但並不是完整的參考資料。不過,只要認真學習,對於掌握 XML 認證考試的轉換部分來說,本教程以及參考資料部分提供的內容提供了足夠的廣度和深度。
目標
完成本教程後,讀者應該能夠:
了解如何使用 XSLT 轉換 XML
能夠用 XPath 進行字符串和數學操作,查找和遍歷 XML
知道如何使用 CSS 設置 XML 的可視化格式
前提條件
本教程是為那些具有編程和腳本背景、了解基本的計算機科學模型和數據結構的開發人員編寫的。應該熟悉下列與 XML 有關的計算機科學概念:樹遍歷、遞歸和數據重用。應該熟悉 Internet 標准和概念,比如 Web 浏覽器、客戶機-服務、文檔化、格式化、電子商務和 Web 應用程序。
系統要求
與本系列第 3 部分一樣,需要安裝 Linux® 或 Microsoft® Windows® 平台,至少 50MB 磁盤空間和安裝軟件的管理訪問權限。本教程還使用了下列工具,這些不是必需的:
Altova XMLSpy(免費的 Home 版就夠了。)
Microsoft™ Internet Explorer 6.0 或更高版本
Mozilla Firefox 1.0.7 或更高版本
請注意,XSLT 文檔本身也是 XML,因此可以使用任何文本編輯器如 Microsoft Notepad 或 Vim 編輯。不過,如果編輯器能夠幫助確保文檔的結構良好性將非常方便;XMLSpy 不僅可以幫助編寫結構良好的文檔,還有很多方便的特性。CSS 文檔不是結構良好的,可使用喜歡的任何文本編輯器編寫。
XML 轉換
轉換 XML 通常涉及到下列技術:
XSLT 1.0,將 XML 文檔轉換為其他形式
XPath 1.0,搜索遍歷 XML 文檔,並提供了數學和字符串操作
CSS,格式化 XML 文檔
XML 實例文檔
本教程繼續使用第 3 部分中的 DVD 目錄示例 XML 文檔(請參閱參考資料)。為了方便起見,清單 1 重新列出了該文檔。本教程中編寫的大部分轉換都是針對這個文檔的。
清單 1. DVD 目錄 XML 實例文檔
<?XML version="1.0"?><catalog><dvd code="_1234567"><title>Terminator 2</title><description>A shape-shifting cyborg is sent backfrom the future to kill the leader of theresistance.</description><price>19.95</price><year>1991<year></dvd><dvd code="_7654321"><title>The Matrix</title><price>12.95<price><year>1999<year></dvd><dvd code="_2255577" genre="Drama"><title>Life as a House</title><description>When a man is diagnosed with terminalcancer, he takes custody of his misanthropicteenage son.</description><price>15.95</price><year>2001</year></dvd><dvd code="_7755522" genre="Action"><title>Raiders of the Lost Ark</title><price>14.95</price><year>1981</year></dvd></catalog>
後面還有一個關於網站地圖的 XML 實例文檔。
XSLT
奇妙的是我們可以看到這些樹,不再感到無所適從。
-- Ralph Waldo Emerson
有時候我們說 XML 不是一種編程語言。如果不包括 XSLT 這樣說就完全 對了 —— XSLT 本身是 XML,並且已經證明是圖靈完整的。這就是說,您可以使用 XSLT 執行現代計算機能夠完成的任何計算。
XSLT 本質上是一種聲明性系統,它聲明了在 XML 文檔中遇到特定的元素類型時會發生什麼。XSLT 不是編譯的,相反,它以及 XML 輸入文檔一起由樣式表處理程序解釋,比如 Xalan 或 Microsoft XML Core Services (MSXML)。可以把它的用法看作一個數學公式:XSLT( XML ) = 輸出。
由於 XSLT 的名稱中有樣式表 這個詞,編程社區中有人就認為 XSLT 不具備編程語言那樣的強大功能,僅僅是某種類似級聯樣式表(CSS)的東西。並不是看不起 CSS,不過啧啧……雖然不像其他多數語言那樣簡潔——很大程度上是因為必須保證結構良好性——XSLT(與 XPath 相結合,後者提供了搜索和遍歷 XML 樹結構以及執行字符串和數學操作的手段)也能提供豐富的功能。後面討論遞歸的時候就會看到也可以寫出非常優美的代碼。
下面的章節將說明如何采用 XSLT 和 XPath 從 XML 文檔中檢索數據。多數例子中,數據最終被格式化為 Html。
Web 浏覽器內部的轉換
雖然下面的示例代碼都假定轉換在服務器上或者 XMLSpy 之類的環境下執行,不過這些轉換只要稍加修改或者不做修改也能在支持 XSLT 的最新版本的浏覽器中完成,比如 Mozilla Firefox 和 Microsoft Internet Explorer。在這些浏覽器中查看 XML 文檔需要類似下面的指令,這條指令應該放在 XML 輸入文檔的文檔序言中,<?XML version="1.0"?> 標簽的下方。將 href 屬性改為適當的值,可以是絕對或相對 URL:
<?xml-stylesheet type="application/XML" href="http://www.ibm/com/xslt/foo.xslt"?>
根元素
任何 XSL 轉換的根元素或頂層元素——所有其他子節點都嵌套在其中,都是 xsl:stylesheet 或 xsl:transform 元素。可以使用其中的任何一個,它們的意義一樣。可以寫為:
<xsl:stylesheet version="1.0" XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
或者:
<xsl:transform version="1.0" XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
然後應該在文檔的最後分別使用 </xsl:stylesheet 或 </xsl:transform> 關閉這些標簽。因此,本教程中交換使用樣式表 和轉換 兩個詞。
模板元素
xsl:template 元素包含一組規則,可應用於 XML 輸入文檔中的特定元素。每個 xsl:stylesheet 或 xsl:transform 必須至少包含一個 xsl:template 元素。XSLT 編程的豐富性主要源於可使用分別服務於不同目的的多個模板作為邏輯模塊。可以通過 match 屬性來觸發模板元素或者通過 name 屬性直接調用。
match 屬性的用法很簡單。遇到 match 屬性值所規定的范式時即執行該規則。比方說,可使用下列規則確定文檔中包含字符串“Another DVD”的每個 dvd 元素:
<xsl:template match="dvd">Another DVD</xsl:template>
轉換的輸出結果如下:
<?XML version="1.0" encoding="UTF-8"?>
<p>Another DVD</p>
<p>Another DVD</p>
<p>Another DVD</p>
<p>Another DVD</p>
要注意,對輸入文檔中每個 dvd 元素都會執行一次模板規則。每次在輸入文檔中遇到 dvd 元素時都會調用該規則。
xsl:apply-templates 和 xsl:value-of
如果能建立與除 dvd 之外的其他元素對應的模板規則將非常有用。事實上,也能建立與其他元素匹配的模板:
<xsl:template match="price"><xsl:value-of select="."/></xsl:template>
<xsl:template match="title"><xsl:value-of select="."/></xsl:template>
無論哪種情況,<xsl:value-of/> 標簽的 select 屬性值都是一種范式,它給出了當前分析元素或者上下文節點 的文本值。上面 select 屬性值中“.”所標記的 XPath 表達式是反身軸。 模板標簽中的 match 屬性值也是一個 XPath 表達式。這裡的上下文節點由模板匹配確定。設置上下文節點的其他方式包括使用 xsl:apply-templates 和 xsl:for-each。
上面兩個模板不僅檢索 title 和 price 的值,XML 輸入文檔中的其他內容也會顯示。如果要僅僅顯示 title 和 price 元素的值,還需要增加一條模板規則:
<xsl:template match="dvd">
<p>
<xsl:apply-templates select="title"/>- $<xsl:apply-templates select="price"/>
</p>
</xsl:template>
這裡還增加了一點 Html 格式化成分(<p/> 標簽)。輸出結果如下:
<?XML version="1.0" encoding="UTF-8"?>
<p>Terminator 2 - $19.95</p>
<p>The Matrix - $12.95</p>
<p>Life as a House - $15.95</p>
<p>Raiders of the Lost Ark - $14.95</p>
調用模板
除上述方法以外,還可以明確調用 顯示 dvd 的子元素 title 和 price 的模板,這種方法帶來了新的可能性。這種情況下,完整的轉換如下所示:
<xsl:stylesheet version="1.0" XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="dvd">
<p>
<xsl:call-template name="Title"/>
<xsl:call-template name="Price"/>
</p>
</xsl:template>
<xsl:template name="Title"><xsl:value-of select="title"/> - </xsl:template>
<xsl:template name="Price">$<xsl:value-of select="price"/></xsl:template>
</xsl:stylesheet>
為了增加可讀性,標題中的連字符和價格中的美元符號移到了命名模板 Title 和 Price 之中。下面的輸出結果與上一個轉換完全相同:
<?XML version="1.0" encoding="UTF-8"?>
<p>Terminator 2 - $19.95</p>
<p>The Matrix - $12.95</p>
<p>Life as a House - $15.95</p>
<p>Raiders of the Lost Ark - $14.95</p>
xsl:call-template 標簽的 name 屬性引用 name 屬性值與其相同的模板,不論是“Title”還是“Price”。通過 name 屬性而不是 match 屬性調用的模板稱為命名模板。通過命名模板可以實現代碼的模塊化。後面將看到,結合使用命名模板和 <xsl:import/> 或 <xsl:include/>(用於添加外部文件)可以根據需要把模板換進換出。XSLT 中命名模板的另一個重要用途是實現遞歸編程。
迭代
繼續使用 catalog.XML 作為例子輸入文檔,我們看如何對一組元素重復使用一個模板規則。這種情況下,上下文節點是根元素 catalog。不用上述通過匹配元素來激活模板規則應用的方法,可以通過 <xsl:for-each/> 標簽使用迭代。
首先,我們來看看如何使用 xsl:for-each 循環遍歷目錄中的每個 dvd 元素:
<xsl:template match="catalog">
<Html>
<body>
<xsl:for-each select="dvd">
<xsl:call-template name="DVD"/>
<xsl:for-each>
<body>
</Html>
</xsl:template>
注意,為了便於在浏覽器中顯示,其中增加了一些 Html。觀察 xsl:for-each 標簽的內容,可以看到調用了一個命名模板 DVD。DVD 模板可能包含下面的顯示邏輯:
<xsl:template name="DVD">
<xsl:variable name="label" select="@code"/>
<p>
<img src="images/{$label}.gif" alt=""/>
<xsl:value-of select="title"/>
</p>
<xsl:template>
上面的例子引出了幾個新的概念。首先,有一個名為 label 的 xsl:variable。要注意,該變量取得了 dvd 元素的 code 屬性值,使用了前綴“@”。此外,還應注意 xsl:for-each 按照順序將上下文節點轉移到每個 dvd 元素。這一點可由上面的 DVD 模板看出來,因為能夠直接引用 @code 屬性和 title 元素值,這兩個都是 dvd 元素的孩子。這些相對 XPath 表達式表明,xsl:for-each 將上下文節點轉移到了每個 dvd 元素。
其次,新建的 label 變量用於創建 <img/> 標簽,靈活地設置新的 Html 格式。(這裡假設每張 DVD 在 images/ 目錄中都有對應的圖片。每幅圖片都是用 DVD 的 code 屬性值作為自己的文件名。)要注意為了得到變量的值在其前面加上了美元符號(“$”),並且和 xsl:value-of 元素類似用花括號(“{”和“}”)包圍起來。(當然,該例中根本不需要創建變量,可以直接在 img 標簽中引用 @code 屬性的值:<img src="images/{@code}.gif" alt=""/>。)
將這些結合在一起:
<?XML version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="Html"/>
<xsl:template match="catalog">
<Html>
<body>
<xsl:for-each select="dvd">
<xsl:call-template name="DVD"/>
</xsl:for-each>
</body>
</Html>
</xsl:template>
<xsl:template name="DVD">
<xsl:variable name="label" select="@code"/>
<p>
<img src="images/{$label}.gif" alt=""/>
<xsl:value-of select="title"/>
</p>
<xsl:template>
<xsl:stylesheet>
上述轉換得到的結果如下:
<Html>
<body>
<p>
<img alt="" src="images/_1234567.gif">Terminator 2</p>
<p>
<img alt="" src="images/_7654321.gif">The Matrix</p>
<p>
<img alt="" src="images/_2255577.gif">Life as a House</p>
<p>
<img alt="" src="images/_7755522.gif">Raiders of the Lost Ark</p>
</body>
</Html>
格式化輸出
您可能注意到,上面的輸出前面沒有了 <?xml?> 標簽。有兩個原因,任何一個都會導致輸出作為 HTML 處理。首先,緊跟在 <xsl:stylesheet> 開始標簽之後增加了 <xsl:output method="html"/>。xsl:output 的 method 屬性通常取值為 html 或 xml。xsl:output 標簽另外一個重要的標簽是 encoding,它定義了輸出的字符集,在 method 屬性值為 xml 時使用。<?XML?> 標簽消失的另一個原因是多數樣式表處理程序都注意匹配模板中是否使用了 HTML 標簽,如 <html/> 和 <body/>。如果有的話,則把輸出 method 自動作為 Html 處理。
排序結果
xsl:for-each 有一個有用附件 xsl:sort。(也可用於 xsl:apply-templates。)可以使用該標簽作為 xsl:for-each 的第一個元素,根據某個鍵使結果按照文檔順序 之外的其他順序排列。比方說,上例中的 xsl:for-each 可使用 xsl:sort 按照 title 屬性值來排列 DVD 列表:
<xsl:for-each select="dvd">
<xsl:sort select="title"/>
<xsl:call-template name="DVD"/>
</xsl:for-each>
將上面粗體顯示的文本 xsl:sort 插入到前例中,可得到如下結果:
<Html>
<body>
<p>
<img alt="" src="images/_2255577.gif">Life as a House</p>
<p>
<img alt="" src="images/_7755522.gif">Raiders of the Lost Ark</p>
<p>
<img alt="" src="images/_1234567.gif">Terminator 2</p>
<p>
<img alt="" src="images/_7654321.gif">The Matrix</p>
</body>
</Html>
可以看到 DVD 現在按照標題的字母順序排列。在第一個之後還可以加上其他 xsl:sort 元素來對結果集進一步排序。
遞歸
通過遞歸實現前面的 DVD 列表轉換看起來有點怪,因為:
上述使用 xsl:for-each 的迭代解決方案,對於熟悉 Java™ 這類命令式 編程語言的程序員來說更直觀。
這個特殊的例子更適合簡單的 for-loop,因為只需要遍歷一組兄弟節點。
但是,在元素可能嵌套任意多層的情況下,遞歸更適合這樣的任務。比如,文件系統以及後面嵌在網站中的主題頁面都是很好的例子。為了便於比較,下面的代碼顯示了上例的遞歸解決方案,沒有排序:
<xsl:stylesheet version="1.0" XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="Html"/>
<xsl:template match="catalog">
<Html>
<body>
<xsl:call-template name="DVD">
<xsl:with-param name="dvdCount" select="count(dvd)"/>
</xsl:call-template>
</body>
</Html>
</xsl:template>
<xsl:template name="DVD">
<xsl:param name="position" select="1"/>
<xsl:param name="dvdCount"/>
<xsl:variable name="label" select="dvd[$position]/@code"/>
<p>
<img src="images/{$label}.gif" alt=""/>
<xsl:value-of select="dvd[$position]/title"/>
</p>
<xsl:if test="$position < $dvdCount">
<xsl:call-template name="DVD">
<xsl:with-param name="position" select="$position+1"/>
<xsl:with-param name="dvdCount" select="$dvdCount"/>
</xsl:call-template>
</xsl:if>
<xsl:template>
</xsl:stylesheet>
該轉換和迭代解決方案的結果相同:
<Html>
<body>
<p>
<img alt="" src="images/_2255577.gif">Life as a House</p>
<p>
<img alt="" src="images/_7755522.gif">Raiders of the Lost Ark</p>
<p>
<img alt="" src="images/_1234567.gif">Terminator 2</p>
<p>
<img alt="" src="images/_7654321.gif">The Matrix</p>
<body>
</Html>
這裡,匹配模板中在轉換的開始通過調用 DVD 模板開始遞歸。然後 DVD 模板再調用自身,直到處理到最後一個 dvd 元素。
這個例子沒有多少值得注意的地方。只有一點,上下文節點沒有轉移到任何一個 dvd 元素上。這是因為 xsl:call-template 沒有像 xsl:for-each 那樣改變上下文節點。因此,DVD 模板中定義的變量必須在 xsl:param、position 的幫助下引用當前 dvd 元素,後者保存了當前處理的 dvd 元素的位置。參數可以通過 <xsl:with-param/> 標簽從模板傳遞給它調用的命名模板。<xsl:with-param> 只能作為 <xsl:call-template/> 標簽的子元素。被調用的模板通過 <xsl:param/> 標簽得到這些參數,後者必須緊跟在 <xsl:template/> 起始標簽之後。
您可能注意到,DVD 模板中的 xsl:param、position 帶有通過 select 屬性提供的默認值。因此,匹配模板中第一次調用 DVD 模板的時候不一定要使用對應的 xsl:with-param。使用 xsl:param 標簽的默認值是一種好習慣,可以避免在被調用模板中發生意料之外的行為。(如果不是為了比較兩個參數,dvdCount 也應該有默認值。)
此外要注意,在 DVD 模板最後的 xsl:call-template 中,將 position 參數傳遞給下一個 DVD 模板的實例之前增加了一。在 <xsl:if/> 的 test 屬性中可以看到,這兩個參數的值用在了遞歸停止條件中。
在 DVD 模板中還使用 position 參數表明當前處理的是哪一個 dvd 元素。這是通過 xsl:variable 和 xsl:value-of 標簽 select 屬性中用於限定 dvd 的謂詞 實現的。兩者都是 XPath 表達式,本教程後面還將進一步討論。XPath 謂詞放在方括號(“[”和“]”)之間,緊跟在所限定的元素之後。該例中,position 參數在謂詞中用於表示當前所處理的 dvd 元素的位置:dvd[$position]。
<xsl:if/> 元素的用法很簡單。它有一個條件,通過計算必需的 test 屬性得到,結果必須是一個 Boolean 值(true 或 false)。如果想知道,我可以告訴您,沒有 <xsl:else/> 或 <xsl:elseif/> 標簽。但是可以使用 <xsl:choose/>,後面還要講到。最後,要注意 <xsl:if/> test 屬性中的小於符號是 XML 轉義的(<>)。如果使用非轉義的小於號(<),樣式表處理程序將拋出異常,因為這個字符以及右尖括號或大於號(>)用於表示 XML 元素的開始和結束。
網站地圖中的遞歸
現在看一個更適合采用遞歸方法的例子。清單 2 中的 XML 示例文檔顯示了一個虛構網站的地圖。我們需要構造一個轉換來呈現站點地圖,從最上層的頁面開始遞歸地列出所有子頁面,以及子頁面的子頁面——無論多少層。轉換的結果是一個 Html 文檔。
清單 2. 網站地圖 XML 實例文檔
<?XML version="1.0"?>
<site>
<page label="A" href="0.Html">
<page label="AA" href="0_0.Html">
<page label="AAA" href="0_0_0.Html">
<page label="AAAA" href="0_0_0_0.Html"/>
<page label="AAAB" href="0_0_0_1.Html"/>
<page label="AAAC" href="0_0_0_2.Html"/>
</page>
<page label="AAB" href="0_0_1.Html"/>
<page label="AAC" href="0_0_2.Html"/>
</page>
<page label="AB" href="0_1.Html"/>
<page label="AC" href="0_2.Html"/>
</page>
<page label="B" href="1.Html"/>
<page label="C" href="2.Html"/>
</site>
清單 2 中的示例站點地圖有 4 層深,並暗示 HTML 頁面是按主題組織的。最上層的三個頁面,label 屬性值分別為 A、B 和 C。頁 A 有三個子頁面(AA、AB 和 AC),它的第一個子頁面又有三個子頁面(AAA、AAB 和 AAC)。最後,頁面 AAA 也有自己的子頁面(AAAA 到 AAAC)。這棵樹需要呈現為 Html。
為了顯示站點地圖,我們首先建立一組嵌套的 Html 無序列表,分別對應每個導航層級。創建這個列表,需要從最上層開始,每次迭代遍歷一個導航層。如果發現某個頁面有子頁面,則將其子頁面的列表添加到當前頁面之後,然後繼續循環遍歷當前導航層次。算法的偽代碼如下:
Build List;
Build List {
For each page {
Write page;
If (page has child pages)
Build List;
}
}
該算法使用很少的代碼(至少對 XSLT 來說)就能處理任意多層深的導航樹。所需 XSLT 代碼如此簡潔表明它非常適合處理此類任務。下面給出了按照上述邏輯編寫的轉換,增加了一點 Html 語法。其中的注釋說明了算法的思想:
<?XML version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="Html"/>
<xsl:template match="site">
<Html>
<body>
<!-- Make the initial template call -->
<xsl:call-template name="BuildList"/>
</body>
</Html>
</xsl:template>
<xsl:template name="BuildList">
<ul>
<xsl:for-each select="page">
<!-- Write the current page -->
<li>
<a href="{@href}">
<xsl:value-of select="@label"/>
</a>
</li>
<!-- If the page has child pages, -->
<xsl:if test="count(page) > 0">
<!-- Build that list of pages, starting the process again... -->
<xsl:call-template name="BuildList"/>
</xsl:if>
<xsl:for-each>
</ul>
</xsl:template>
</xsl:stylesheet>
要注意前面的轉換是尾遞歸的。同樣,如果當前頁面沒有子頁面,則滿足遞歸的停止條件,於是就不再遞歸調用 BuildList 模板了。該轉換的 Html 輸出如下:
<Html>
<body>
<ul>
<li><a href="0.Html">A</a></li>
<ul>
<li><a href="0_0.Html">AA</a></li>
<ul>
<li><a href="0_0_0.Html">AAA</a><li>
<ul>
<li><a href="0_0_0_0.Html">AAAA</a><li>
<li><a href="0_0_0_1.Html">AAAB</a><li>
<li><a href="0_0_0_2.Html">AAAC</a></li>
</ul>
<li><a href="0_0_1.Html">AAB<a></li>
<li><a href="0_0_2.Html">AAC</a><li>
<ul>
<li><a href="0_1.Html">AB</a></li>
<li><a href="0_2.Html">AC</a><li>
</ul>
<li><a href="1.Html">B<a></li>
<li><a href="2.Html">C</a><li>
<ul>
</body>
</Html>
把鏈接展開後,Html 輸出在浏覽器中的呈現結果如下:
站點導航模塊的遞歸
可以增加比較當前頁面(上下文節點)和網站地圖中所有元素的邏輯,從而把上述轉換做成一個樹導航模塊。這是因為網站中的每個頁面都需要根據自身在樹中的位置確定自己的導航模塊版本。還需要一些 JavaScript 來控制兄弟頁面集合的展開與折疊。完成後呈現的模塊看起來類似於 Microsoft Windows Explorer 和 Eclipse Navigator 視圖中樹控件。
開始呈現的時候,代表頂層頁面之外的所有其他頁面的 Html 元素樣式屬性 display 都設置為 "none"(詳見後面的 CSS)。然後使用浏覽器上的 JavaScript 邏輯展開代表上下文節點所有祖先頁面的 Html 標簽,即將其樣式屬性 display 的值設為“block”。為保證這種行為對每個頁面是不同的,可以假設每個頁面元素的 @href 屬性都是惟一的。然後使用 xsl:variable 保存上下文節點 @href 屬性的值。然後,當每個 page 元素通過每一導航層的迭代接收到上下文時,就可以比較其 @href 屬性和保存有正在呈現的頁面 @href 的 xsl:variable。這樣,當前頁面及其上所有頁面的鏈接都會顯示出來。
如果有的話,它也非常適合顯示當前頁面的子頁面。可用上述 HTML 無序列表元素或者 HTML <div/> 標簽來保存每個導航層。這些元素要求惟一的 id 屬性值,以便能夠通過編程使其樣式屬性在 display:none 和 display:block 之間切換。如果使用 <div/> 標簽,則需要大於零的 padding-left 或 margin-left 樣式值來提供縮進。這些實現留給讀者作為練習。為了美觀增加幾幅圖片之後,轉換結果可能看起來類似圖 1 中所示的樹導航控件。該圖是遞歸 XSLT 轉換生成 Html 後的呈現結果,實現了前面給出的大部分提示。不過我沒有使用嵌套元素,而是通過兩個屬性用指向父節點的指針來表示兄弟元素。圖 1 中顯示的輸出是在 Internet Explore 6.0 中的呈現結果。
圖 1. 呈現的導航模塊
在顯示的 Html 中(不是圖 1 所示的圖片),單擊圖 1 中看到的加號和減號可以折疊和展開相應的子樹,單擊節點文本則導航到對應的頁面。(為了顯示整棵樹,圖 1 中的節點全部展開了。)目標頁面加載到浏覽器中以後,最初所有的節點都是折疊的,因此只能看到最上層的頁面。然後,從當前頁面開始向上(實際上是向左)展開樹直到頂層,顯示出當前頁面的所有祖先頁面。如果有子頁面,從當前頁展開樹也將同時顯示其子頁面。
由於 XML 文檔的嵌套特性,再加上 xsl:variable 元素是不可變的,可以發現對 XSLT 的大多數問題按照遞歸的方法思考和編寫代碼大有裨益。一般來說,采用遞歸方法的關鍵在於設計的模板,像 BuildList 模板那樣足夠一般化,能用於所要解決問題的任何情況。只要做到這一點,就可以在需要的時候調用自身。
條件邏輯
再回到 DVD 目錄的例子,假設需要根據價格編寫關於庫存 DVD 的報表。但目的不是顯示價格,而是需要按照價格對 DVD 分類。為此需要用到 xsl:choose。
我們來修改上面使用的迭代解決方案,通過調用前面編寫的 Price 模板來顯示每個 dvd 元素:
<?XML version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
XMLns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="Html"/>
<xsl:template match="catalog">
<Html>
<body>
<xsl:for-each select="dvd">
<xsl:call-template name="DVD"/>
</xsl:for-each>
</body>
</Html>
</xsl:template>
<xsl:template name="DVD">
<p>
<xsl:value-of select="title"/>
<xsl:call-template name="Price"/>
</p>
</xsl:template>
<xsl:template name="Price">$<xsl:value-of select="price"/><xsl:template>
</xsl:stylesheet>
現在修改 Price 模板,讓它顯示三個形容詞(“貴!”、“便宜!”和“一般般”)而不是實際價格:
<xsl:template name="Price">
<xsl:choose>
<xsl:when test="price < 15.00"> - Cheap!</xsl:when>
<xsl:when test="price > 19.00"> - Pricey!</xsl:when>
<xsl:otherwise> - So so</xsl:otherwise>
</xsl:choose>
</xsl:template>
xsl:choose 有兩個子元素:一個是必需的,即 xsl:when,與 xsl:if 很相似;另一個是可選的 xsl:otherwise。這個使用 xsl:choose 的轉換輸出結果如下:
<Html>
<body>
<p>Terminator 2 - Pricey!</p>
<p>The Matrix - Cheap!</p>
<p>Life as a House - So so<p>
<p>Raiders of the Lost Ark - Cheap!</p>
</body>
</Html>
與大多數 XSLT 編寫者一樣,您可能不會感到很興奮,因為沒有 xsl:else 和 xsl:else-if 元素,而 xsl:choose 又這麼羅嗦。XSLT 2.0 提供了一種解決方法,但我不准備在本教程中討論。XML in a Nutshell, 3rd Edition(請參閱參考資料)有很多章節專門討論XSLT 和 XPath 1.0 與 2.0 版的區別。
導入和包含其他文件中的模板
xsl:import 與 xsl:include 這兩個元素可以在轉換中插入其他文件中的模板。這兩個元素只能作為 <xsl:stylesheet/> 或 <xsl:transform/> 元素的孩子出現,而且必須出現在任何其他頂層元素之前。語法如下:
<xsl:import href="URI"/>
和
<xsl:include href="URI"/>
href 屬性的 URI 值指向包含所要添加的轉換的文件的路徑和名稱。URI 值可以是相對路徑也可以是絕對路徑。這兩個元素的區別是,通過 xsl:import 引入的模版允許出現名稱沖突,如果發生這種情況則忽略導入的模板。而屬於由 xsl:include 引入的轉換的命名模板,不能與導入它的轉換中的任何命名模板沖突。這些文件在 xsl:include 出現的位置直接復制到當前轉換中。無論哪個元素,被導入和導入文件之間都不允許存在循環引用。和 XML 中的多數事物一樣,允許通過 xsl:import 和 xsl:include 嵌套文件。