- 浏览器如何处理 XML DOM
浏览器会把 XML 文本解析成一个完整的 DOM 树,之后你可以像操作 HTML 一样用 DOM API 读写节点,但要记住 XML 更严格(大小写敏感、必须 well-formed、命名空间必须正确)。常用的加载方式是fetch()
+DOMParser
(把字符串解析为 Document),也可以用老式的XMLHttpRequest.responseXML
。下面示例演示用fetch
拉取并解析、同时检测解析错误的通用做法(注释逐行说明):
javascript
// 从服务器拉取 XML,并用 DOMParser 解析与校验
fetch('/data/example.xml') // 发起网络请求(替换为真实 URL)
.then(res => res.text()) // 把响应体读成字符串
.then(text => {
const parser = new DOMParser(); // 创建 DOMParser 实例
const doc = parser.parseFromString(text, 'application/xml'); // 按 XML 解析为 Document
// 检查解析错误:很多实现会在 doc 中包含 <parsererror>
if (doc.getElementsByTagName('parsererror').length > 0) {
console.error('XML 解析错误:', doc.getElementsByTagName('parsererror')[0].textContent);
return;
}
// 成功后像处理普通 DOM 一样操作
const items = doc.getElementsByTagName('item'); // 获取所有 <item>
for (let i = 0; i < items.length; i++) {
console.log('item', i, items[i].textContent);
}
})
.catch(err => console.error('网络或其它错误:', err)); // 网络错误处理
- DOM Level 2 Core 在浏览器中常见的用法
浏览器实现了 DOM 的核心接口(例如Document
、Node
、Element
、命名空间方法等),在跨文档操作时尤其要注意importNode
、createElementNS
、getElementsByTagNameNS
等函数。下面演示在 XML Document 创建命名空间元素并把它导入到当前页面的用法(每行注释):
ini
// 演示 createElementNS 与 importNode 的典型场景
const xml = '<root xmlns:ex="http://example.com/ns"><ex:a>hello</ex:a></root>'; // 示例 XML 字符串
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xml, 'application/xml'); // 解析为 XML Document
// 在 xmlDoc 内创建带命名空间的新元素(注意使用 createElementNS)
const newElem = xmlDoc.createElementNS('http://example.com/ns', 'ex:note'); // 创建 <ex:note>
newElem.textContent = '这是一条来自 XML 的注记';
// 把新节点导入到当前页面 document(跨文档追加必须 importNode)
const imported = document.importNode(newElem, true); // 深拷贝节点
document.body.appendChild(imported); // 插入到页面中以示例
DOMParser
的实际场景与示例
DOMParser.parseFromString(str, mime)
可以把字符串解析成 XML/SVG/HTML Document。XML 解析遇错通常会在返回的 Document 中出现parsererror
,所以解析后务必检测。下面示例同时演示解析 XML、SVG、以及 HTML(注意 MIME 的不同):
javascript
const parser = new DOMParser();
// 解析 XML
const xmlDoc = parser.parseFromString('<?xml version="1.0"?><root><item>OK</item></root>', 'application/xml');
// 检查解析错误
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
console.warn('XML 解析失败');
} else {
console.log('XML first item:', xmlDoc.getElementsByTagName('item')[0].textContent);
}
// 解析 SVG(SVG 是 XML)
const svgDoc = parser.parseFromString('<svg xmlns="http://www.w3.org/2000/svg"><rect width="16" height="16"/></svg>', 'image/svg+xml');
console.log('SVG root:', svgDoc.documentElement.nodeName);
// 解析 HTML(返回 HTMLDocument)
const htmlDoc = parser.parseFromString('<div><span>hi</span></div>', 'text/html');
console.log('HTML fragment outerHTML:', htmlDoc.body.firstChild.outerHTML);
XMLSerializer
把 DOM 变回字符串并保存/传输
当你在客户端修改了 Document 或节点,需要把它转为文本以便上传或保存时,使用XMLSerializer.serializeToString(node)
。注意序列化格式(空格、换行、属性顺序等)在不同浏览器上可能有细微差别。下面同时示范序列化和把结果作为文件下载的做法:
ini
// 假设 xmlDoc 是已存在的 XML Document
const xmlStr = `<?xml version="1.0"?><root><item id="1">hello</item></root>`;
const doc = new DOMParser().parseFromString(xmlStr, 'application/xml'); // 解析为 Document
const serializer = new XMLSerializer(); // 创建序列化器
const out = serializer.serializeToString(doc); // 序列化整个 Document 为字符串
console.log('序列化结果:', out);
// 将序列化字符串保存为文件(Blob + a.download)
const blob = new Blob([out], { type: 'application/xml' }); // 创建 Blob
const url = URL.createObjectURL(blob); // 生成临时链接
const a = document.createElement('a'); // 创建 a 链接
a.href = url; a.download = 'export.xml'; // 设置下载文件名
document.body.appendChild(a);
a.click(); // 触发下载
a.remove();
URL.revokeObjectURL(url); // 释放对象 URL
- 在浏览器中使用 XPath 的常见方法
浏览器通常提供document.evaluate
(或更完整的XPathEvaluator
)来运行 XPath(大多数实现为 XPath 1.0),能返回节点集、字符串、数字或布尔。XPath 在查询父轴、位置、聚合(count)等方面比 CSS 选择器强。最常见的用法示例如下,返回一个节点快照并遍历:
javascript
// 假设当前页面或某个 XML Document 中存在若干 <book> 元素
const expr = '//book/title'; // XPath 表达式
const res = document.evaluate(expr, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); // 请求快照类型
for (let i = 0; i < res.snapshotLength; i++) {
console.log('title:', res.snapshotItem(i).textContent); // 遍历并输出每个 title 的文本
}
- 使用 DOM Level 3 XPath(预编译表达式、命名空间解析器)
如果你需要多次执行同一个 XPath,预编译表达式(createExpression
)可以提升清晰度与复用性;对于带前缀的查询,用createNSResolver
或自定义函数来映射前缀到命名空间 URI。下面示例展示预编译与复用:
ini
// 示例 XML(含命名空间)
const xmlText = `<?xml version="1.0"?><root xmlns:ex="http://example.com/ns"><ex:item id="a">A</ex:item><ex:item id="b">B</ex:item></root>`;
const xmlDoc = new DOMParser().parseFromString(xmlText, 'application/xml');
const evaluator = new XPathEvaluator(); // 创建 XPathEvaluator
const resolver = evaluator.createNSResolver(xmlDoc.documentElement); // 基于根节点自动创建命名空间解析器
// 预编译表达式(含前缀 ex:)
const compiled = evaluator.createExpression('//ex:item', resolver, null); // 生成可复用的 XPathExpression
// 多次复用 evaluate(提高性能与可读性)
const r1 = compiled.evaluate(xmlDoc, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < r1.snapshotLength; i++) {
console.log('ex:item', r1.snapshotItem(i).getAttribute('id'), r1.snapshotItem(i).textContent);
}
- 只要第一个匹配节点的场景(单节点结果)
当你只关心"第一个"匹配项时,请求XPathResult.FIRST_ORDERED_NODE_TYPE
,结果可以直接用.singleNodeValue
取到,更简洁且语义明确。示例:根据属性过滤并拿到第一个匹配节点:
javascript
// 假设文档中有若干 <book id="...">
const res = document.evaluate("//book[@id='b1']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); // 请求第一个匹配项
const node = res.singleNodeValue; // 直接得到单个节点(或 null)
if (node) {
console.log('找到节点:', node.nodeName, node.getAttribute('id'));
} else {
console.log('未找到 id=b1 的 book');
}
- 让 XPath 返回数字/字符串/布尔(简单类型结果)
XPath 内置函数(如count()
、string()
、boolean()
)可以让返回值直接为简单类型,这在只需计算或判断时能把逻辑压到 XPath 表达式里,减少 JS 遍历。示例展示三种类型的读取方式:
javascript
// 数字:统计节点数
const cntRes = document.evaluate('count(//book)', document, null, XPathResult.NUMBER_TYPE, null);
console.log('book 数量:', cntRes.numberValue);
// 字符串:取第一个 book 的 title 文本
const titleRes = document.evaluate('string(//book[1]/title)', document, null, XPathResult.STRING_TYPE, null);
console.log('第一个书名:', titleRes.stringValue);
// 布尔:是否存在价格大于 50 的书
const existsRes = document.evaluate('boolean(//book[price>50])', document, null, XPathResult.BOOLEAN_TYPE, null);
console.log('是否存在 price>50 的书:', existsRes.booleanValue);
- 使用
ANY_TYPE
时如何稳健处理(默认类型结果)
如果你把resultType
指为ANY_TYPE
,实现会选择"最适合"的形式返回;调用者需要根据result.resultType
分支处理。通常推荐明确需要的类型以减少分支逻辑,但如果确实想让实现决定,下面的模板示例展示如何处理所有常见的返回类型:
javascript
const res = document.evaluate('//book', document, null, XPathResult.ANY_TYPE, null); // 让实现选择合适的类型
switch (res.resultType) {
case XPathResult.NUMBER_TYPE:
console.log('number:', res.numberValue);
break;
case XPathResult.STRING_TYPE:
console.log('string:', res.stringValue);
break;
case XPathResult.BOOLEAN_TYPE:
console.log('boolean:', res.booleanValue);
break;
case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
for (let i = 0; i < res.snapshotLength; i++) console.log('snapshot item:', res.snapshotItem(i));
break;
case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
let n;
while ((n = res.iterateNext())) console.log('iter node:', n);
break;
case XPathResult.FIRST_ORDERED_NODE_TYPE:
console.log('single:', res.singleNodeValue);
break;
default:
console.warn('未知的 XPathResult 类型:', res.resultType);
}
- 在 XPath 中正确处理命名空间(关键点)
XPath 中的前缀并不会自动映射到 URI;必须提供nsResolver
(可由createNSResolver(node)
自动生成,也可以写一个函数手动映射)。如果忽略这点,带前缀的元素将匹配不到。示例:自定义 resolver 与自动 resolver 两种方式:
javascript
// 自定义 resolver:把 ex 映射到 URI
const customResolver = function(prefix) {
const map = { ex: 'http://example.com/ns', svg: 'http://www.w3.org/2000/svg' };
return map[prefix] || null;
};
// 使用自定义 resolver 查询 ex:note
const r = document.evaluate('//ex:note', document, customResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
console.log('custom resolver ->', r.singleNodeValue ? r.singleNodeValue.textContent : 'not found');
// 自动 resolver:基于文档根节点读取 xmlns 声明
// 适用于文档自身声明了 xmlns:ex="..." 的情况
const autoResolver = document.createNSResolver(document.documentElement);
const r2 = document.evaluate('//ex:note', document, autoResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
console.log('auto resolver ->', r2.singleNodeValue ? r2.singleNodeValue.textContent : 'not found');
- 浏览器端对 XSLT 的支持(该怎么用与注意点)
现代主流浏览器都提供了浏览器端 XSLT 支持(通常基于 XSLT 1.0),可用XSLTProcessor
来导入样式表并对 XML 做转换。适合轻量/交互式的模板化转换;若需要大批量或更复杂的 XSLT 功能(例如 XPath 2.0),更适合放到服务器端处理。使用时要注意样式表来源的同源与 CORS,以及移动端的性能影响。
XSLTProcessor
的典型使用(导入、转换并插入页面)
以下示例描述了完整流程:解析 XML/XSL、导入样式表、给样式表传参(见下一条)、执行转换并把结果插入当前页面。示例逐行注释、可直接运行:
xml
// 源 XML 与 XSL 示例
const xmlText = '<?xml version="1.0"?><catalog><book><title>Example</title></book></catalog>';
const xslText = `<?xml version="1.0"?><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/"><div class="out"><xsl:apply-templates/></div></xsl:template>
<xsl:template match="book"><p><xsl:value-of select="title"/></p></xsl:template>
</xsl:stylesheet>`;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'application/xml'); // 源 XML
const xslDoc = parser.parseFromString(xslText, 'application/xml'); // XSL 样式表
const proc = new XSLTProcessor(); // 创建 XSLTProcessor
proc.importStylesheet(xslDoc); // 导入样式表(必须是 Document)
const frag = proc.transformToFragment(xmlDoc, document); // 执行转换,得到 DocumentFragment
document.body.appendChild(frag); // 将结果插入当前页面
- 用
setParameter
给 XSLT 传参数(让样式表更灵活)
在样式表声明<xsl:param name="..."/>
后,JS 可以用setParameter(namespaceURI, name, value)
传入值(第一参数为参数的命名空间 URI,若无命名空间可传null
或''
)。下面演示 XSL 中声明参数与 JS 设置参数的完整流程:
xml
// XSL 包含参数 greeting
const xslWithParam = `<?xml version="1.0"?><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="greeting" select="'Hello'"/>
<xsl:template match="/"><div><h1><xsl:value-of select="$greeting"/></h1><xsl:apply-templates/></div></xsl:template>
<xsl:template match="book"><p><xsl:value-of select="title"/></p></xsl:template>
</xsl:stylesheet>`;
const xml = '<?xml version="1.0"?><catalog><book><title>Param Example</title></book></catalog>';
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xml, 'application/xml');
const xslDoc = parser.parseFromString(xslWithParam, 'application/xml');
const p = new XSLTProcessor();
p.importStylesheet(xslDoc);
p.setParameter(null, 'greeting', '来自 JS 的问候!'); // 给 greeting 传值(无命名空间)
const resultFrag = p.transformToFragment(xmlDoc, document);
document.body.appendChild(resultFrag); // 能看到带变量值的输出
注意:参数常被当作字符串传入;如果需要复杂对象,建议在样式表内通过字符串解析或在 JS 侧先处理完再传简单值。
clearParameters
与reset
的使用场景(避免参数残留或替换样式表)
clearParameters()
会移除已设置的参数,但保留已导入的样式表;reset()
则会把处理器恢复到初始状态(移除样式表和参数)。在复用同一个XSLTProcessor
对象、但希望每次转换采用不同参数或样式表时,这两个方法非常有用。示例展示典型流程:
scss
// 假设 proc 已经导入过某个样式表并设置过参数
proc.clearParameters(); // 清除所有参数(样式仍然保留)
proc.setParameter(null, 'page', '2'); // 重新设置新的参数
// 执行转换...
// 若要完全换样式表:
proc.reset(); // 清除样式表与参数,回到初始状态
proc.importStylesheet(anotherXslDoc); // 导入新的样式表并继续使用
调用 reset()
后如果忘记重新 importStylesheet()
就无法继续转换,所以在逻辑上要注意顺序。