前阵子帮人修一个老项目,数据源是 XML,页面靠 XSLT 在服务端生成。问题很典型:XML 里的记录顺序是乱的,前端一个for-each直接把节点遍历输出,表格数据就跟着文档顺序走,结果报表看起来像随手甩上去的。这种场景下,XSLT 里专门负责排序的<xsl:sort>标签就是核心武器。这篇文章我就把<xsl:sort>从挂载位置、属性细节到实战踩坑完整拆一遍,适合正在写 XSLT 模板、或者在老系统里维护数据导出的朋友参考。
1. 先搞明白:sort 标签到底解决什么问题
1.1 没有 sort 时,输出顺序由什么决定
XSLT 对 XML 的处理有一个底层规则:默认情况下,节点输出顺序就是 XML 文档顺序(document order),即节点在源文件里出现的先后顺序。你写<xsl:for-each select="staff/employee">,它就会从上到下把employee节点挨个取出来,压根不看里面的值是什么。
这在小数据量下没什么感觉,但一到真实业务就露馅了。比如 CRM 导出的客户列表、订单流水、日志记录,很多源 XML 本身是数据采集端拼出来的,顺序完全是写入顺序,而不是业务想要的顺序。你想按金额从大到小展示,或者按时间倒序排列,直接对节点集操作是做不到的,必须让 XSLT 处理器先把节点重新排序,然后再进入输出流程——这就是<xsl:sort>的角色。
1.2 sort 的工作位置:它是 for-each 和 apply-templates 的"指挥棒"
<xsl:sort>不是一个独立的顶层模板元素,它不能单独出现在某个<xsl:template>里向输出流写东西。它是一个修饰性指令,只能挂在两个地方:
<xsl:for-each>内部<xsl:apply-templates>内部
位置必须紧跟在所在元素的开头,也就是在这两个指令的所有其他子元素之前。它就像队伍前面那个叫号员:for-each和apply-templates负责按照规则抓一批节点,sort则决定这批节点以什么顺序走到你面前。理解了这层关系,后面所有用法都是围绕"先选节点,再排顺序,最后输出"这个流程展开的。
提示:如果你把
<xsl:sort>写在<xsl:for-each>中间,比如先写了一个<xsl:value-of>再写<xsl:sort>,XSLT 处理器会直接报编译错误,而不是忽略它。这一点在第六章《排序失效排查手册》里再细说。
2. 最常用的两种挂载方式:for-each 里的正序与倒序
2.1 最小可运行示例:按 hireDate 排员工名单
先看最常见的场景:用for-each遍历节点并排序。假设 XML 结构是这样的:
<?xml version="1.0" encoding="UTF-8"?> <staff> <employee> <name>张伟</name> <department>研发部</department> <salary>12000</salary> <hireDate>2021-03-15</hireDate> </employee> <employee> <name>李娜</name> <department>市场部</department> <salary>9500</salary> <hireDate>2020-11-02</hireDate> </employee> <employee> <name>王强</name> <department>研发部</department> <salary>15000</salary> <hireDate>2019-07-08</hireDate> </employee> </staff>如果直接for-each,输出顺序就是张伟、李娜、王强。但我想按入职日期从早到晚排,就写成:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <ul> <xsl:for-each select="staff/employee"> <xsl:sort select="hireDate" order="ascending"/> <li> <xsl:value-of select="concat(name, ' 入职于 ', hireDate)"/> </li> </xsl:for-each> </ul> </xsl:template> </xsl:stylesheet>输出顺序变成:王强(2019-07-08)、李娜(2020-11-02)、张伟(2021-03-15)。
这里有一个关键逻辑:select="hireDate"表示用当前节点(employee)下的 hireDate 子元素的值作为排序键。XPath 路径是相对for-each选中的当前节点来解析的,不是相对根节点。这一点新手特别容易写错,比如在排序键里写成select="/staff/employee/hireDate",虽然也能选到值,但含义完全不同,而且性能差一大截。
2.2 倒序的写法与常见错用
倒序就是把order属性改成"descending":
<xsl:for-each select="staff/employee"> <xsl:sort select="salary"><xsl:template match="/"> <xsl:apply-templates select="staff/employee"/> </xsl:template> <xsl:template match="employee"> <p> <xsl:value-of select="concat(name, ':', salary)"/> </p> </xsl:template>这段代码的输出顺序与 XML 文档顺序保持一致,无法实现按工资排队。
3.2 用 sort 控制节点处理顺序
解决方式是在apply-templates内部挂sort:
<xsl:template match="/"> <xsl:apply-templates select="staff/employee"> <xsl:sort select="salary"><xsl:sort select="salary"><xsl:sort select="name" lang="zh-CN"/>想法是让中文按拼音或者笔画排。但我要泼一盆冷水:XSLT 1.0 里的lang能否真正实现中文排序,完全取决于你用的处理器。浏览器内置的 XSLT 引擎(比如老版 IE)基本不认zh-CN;xsltproc(libxslt)也要看系统 locale 有没有装中文支持;Saxon 对 locale 的支持倒是比较完整,但 XSLT 1.0 时代的 Saxon 中文排序也谈不上好用。
lang属性最常见的结局是:处理器忽略它,退化成按 Unicode 码点排序。而汉字在 Unicode 里的顺序和拼音没有任何关系,所以你会看到"张"排到"李"前面这类完全没道理的结果。
稳妥的做法是在源 XML 里增加拼音字段,或者预处理时转成拼音首字母作为排序键。我在后面第五章实战里会给出一个更通用的方案。
4.4 多级排序:多个 sort 标签的优先级
业务里几乎没有"只按一个字段排"的需求。最常见的是"先按部门排,再按工资排,最后按姓名排"。XSLT 的多级排序非常简单:在同一个for-each或apply-templates里连续写多个<xsl:sort>,顺序就是优先级顺序。
<xsl:for-each select="staff/employee"> <xsl:sort select="department" order="ascending"/> <xsl:sort select="salary"><xsl:sort select="translate(level, '高中低', '123')"><?xml version="1.0" encoding="UTF-8"?> <staff> <employee> <name>张伟</name> <department>研发部</department> <salary>12000</salary> <hireDate>2021-03-15</hireDate> <city>北京</city> </employee> <employee> <name>李娜</name> <department>市场部</department> <salary>9500</salary> <hireDate>2020-11-02</hireDate> <city>上海</city> </employee> <employee> <name>王强</name> <department>研发部</department> <salary>15000</salary> <hireDate>2019-07-08</hireDate> <city>深圳</city> </employee> <employee> <name>赵敏</name> <department>财务部</department> <salary>8000</salary> <hireDate>2022-01-20</hireDate> <city>北京</city> </employee> <employee> <name>陈杰</name> <department>市场部</department> <salary>10000</salary> <hireDate>2018-05-30</hireDate> <city>广州</city> </employee> <employee> <name>刘洋</name> <department>研发部</department> <salary>13000</salary> <hireDate>2020-06-18</hireDate> <city>杭州</city> </employee> </staff>5.2 完整的 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" encoding="UTF-8"/> <xsl:template match="/"> <html> <head><title>员工工资报表</title></head> <body> <table border="1"> <tr> <th>部门</th> <th>姓名</th> <th>工资</th> </tr> <xsl:for-each select="staff/employee"> <xsl:sort select="department" order="ascending"/> <xsl:sort select="salary"><xsl:for-each select="staff/employee"> <xsl:value-of select="salary"/> <xsl:sort select="salary"><xsl:variable name="sortedEmployees"> <xsl:for-each select="staff/employee"> <xsl:sort select="salary">