
真要较真起来能把你日常工作坑得七荤八素的往往不是那些框架原理、算法设计反而是藏在DOM角落里的小东西。今天要聊的CDATA就是其中之一。写XML、调RSS解析、做XHTML页面甚至搞前端模板的时候你都有可能撞到它撞上了可能还一脸懵这串文字怎么就不按常理出牌了我在实际开发里遇到过不少次跟CDATA相关的怪问题比如说JS代码被XML当成了标签、RSS里的特殊字符没被正确读取还有手写DOM结构时节点类型对不上等等。折腾几轮之后我对这玩意的脾性算是摸透了。这篇文章不搞什么高深理论就把CDATA是什么、在DOM里如何表现、实际用的时候有哪些坑掰开揉碎给你讲清楚希望能帮你省掉一些没必要的排查时间。1. 这玩意儿到底是个啥CDATA的设计初衷1.1 一个“免转义”的文本保护区CDATA是Character Data的缩写翻译成大白话就是“字符数据”。它是XML以及早期XHTML里一种特殊的文本块专门用来放那些不需要转义的字符内容。所谓“不需要转义”意思是你在CDATA区段里写什么解析器就当什么不会把当成标签开始符也不会把当成实体引用开头。我在讲给团队里新人听的时候喜欢用一个很生活化的类比普通文本像是一条得时刻注意语法规范的通道你在里面写hello会被解析器当作一个元素开始标签写amp;会被当作的转义实体而CDATA就像是一个保险箱箱子里的东西是什么样就是什么样解析器只负责抱走不做任何解读。这个设计解决了一个非常实际的痛点当你的XML内容里需要嵌入一段本身就包含大量尖括号、与符号的数据时比如一段HTML代码、一些数学公式、或者一段带泛型的Java代码如果不用CDATA你就要做一堆转义读起来费劲不说写错一个实体符号整个文档解析就崩了。我见过一个最典型的翻车现场有同事直接往XML节点里塞了一段带script标签的HTML结果解析器把script当成了XML元素来处理里面的符号引发了一大堆嵌套错误报错信息完全看不懂。后来换成CDATA包起来整个世界安静了。1.2 为什么必须转义XML的规则和反例搞清楚CDATA的价值得先明白XML的转义规则到底有多磨人。XML里只有五个预定义的实体lt;表示、gt;表示、amp;表示、quot;表示、apos;表示。如果你有一段文本需要同时包含和在普通节点里你得写成lt;pgt;ATamp;T amp;amp; Googlelt;/pgt;原始文本内容是pATT Google/p但这样转义之后可读性直线下降。如果内容再复杂一点嵌套个十几层的标签转义就会变得非常痛苦还极易出现双重转义的问题——转义后的文本经过一次DOM读取变成中间内容再经过一次XML序列化又会被转义一次最终输出结果面目全非。在某些数据传输场景里这种转义就是灾难。比如用XML承载一些动态生成的报表HTML片段后台拿到XML之后得先用XML解析器读一遍把转义解开然后把解出来的文本也就是真正的HTML再交给渲染引擎渲染。这个过程中只要有一个没转义或者多转义了一次轻则样式丢失重则整个HTML直接显示成纯文本。CDATA就是用来终结这个痛苦的它允许你把原始内容直接放进去不去解析里面的任何标签和实体。2. DOM眼中的CDATA节点类型与接口2.1 nodeType4CDATASection是谁的孩子把CDATA的概念落到DOM里它就变成了一个具体的节点对象。DOM标准里CDATA区段对应的节点类型是CDATASection它的nodeType是4。这个数字你可能平时不怎么注意但在调试DOM树结构时格外好使。我记得很清楚有一次在浏览器控制台里排查一段来自XML的数据childNodes打印出来之后发现有个节点跟普通的Text节点表现不太一样看nodeName是#cdata-section再一看nodeType是4一下子恍然大悟这个节点的内容虽然也是纯文本但它有自己的身份和规则。CDATASection在DOM树里的父节点通常是元素节点。它不能直接挂在Document根节点下这一点跟Comment注释节点比较像。跟Text节点相比CDATA节点主要区别在于它的内容不参与XML转义的解算文档序列化时输出规则也不同。你在DOM操作时createTextNode创建出来的是一个普通文本节点而createCDATASection创建出来的才是CDATA节点。DOM标准给这个节点定义的方法和属性大多继承自CharacterData所以data、length、substringData()、appendData()这些都能用。它跟Text节点的关键差异之一是CDATASection节点不能用splitText()去拆分如果你强行调用在不少实现里会直接报错这一点跟Text节点不太一样。2.2 CDATASection的接口与行为特性CDATASection的接口并不复杂但它继承的那个CharacterData接口很值得研究。从Node继承下来的nodeValue属性在CDATA节点上返回的就是CDATA区段内的原始文本不需要你再做任何反转义。举个例子有一个XML文档长这样item content![CDATA[b价格199 包邮/b]]/content /item你用DOM解析之后访问content元素的firstChild.data得到的结果直接就是b价格199 包邮/b不需要做任何字符串替换。这就是CDATA最核心的卖点解析器已经帮你把内容原样送出来了。不过在写代码的时候有一点得留意data和nodeValue返回的内容虽然在绝大多数情况下一致但你别指望把data修改之后nodeValue自动给你搞什么格式转换。它就是个普通的字符串属性改了就是改了。当你把CDATA节点从一个文档迁移到另一个文档时需要使用document.importNode()或document.adoptNode()这两个方法会保留节点的CDATA类型操作完之后别忘把自己原来的引用替换掉。2.3 createCDATASection()怎么用以及什么时候用不上document.createCDATASection()这个API我在实际项目里用得不算多因为大多数业务解析的都是外部XML文件而不是动态创建XML。但一旦用到就说明你确实是在用DOM API生成包含大段未转义内容的XML结构。基本用法很简单const doc new DOMParser().parseFromString(root/, application/xml); const cdata doc.createCDATASection(这段内容里有 div 等原始字符); doc.documentElement.appendChild(cdata); console.log(new XMLSerializer().serializeToString(doc));序列化结果会是root![CDATA[这段内容里有 div 等原始字符]]/root有同学可能会想那我在HTML文档里创建CDATA节点会怎样答案在浏览器里很微妙。你用createCDATASection创建节点本身是可以的但如果把节点插入到一个HTML文档里序列化时它很可能被当成普通文本输出因为现代HTML文档理论上不存在CDATA节点只有XML文档里才有这回事。这算是一个重要的区分HTML文档和XML文档虽然都建立在DOM之上但它们解析和序列化规则并不一致。你在.html文件里写![CDATA[...]]浏览器那一套HTML解析器虽然也认识这个标记但未必会生成nodeType4的节点更多时候它会被当作注释或者直接特殊处理后面章节我细说。3. 从解析器视角看CDATA的“脾气”3.1 XML解析模式下CDATA的规则——定界符的禁忌XML解析器对CDATA的处理有一套精确到字符的规则。一个CDATA区段以![CDATA[开头以]]结尾。开头之后的内容直到]]之前全部被当作纯文本处理不解析任何标签、实体甚至注释标记。这句规则听起来很宽松但实际上有一个非常严格的禁忌CDATA区段内部绝对不能出现]]这个字符串。如果内容本身需要包含]]就必须把]]拆开写比如写成]]]]![CDATA[这种经典的拆法。举个不常见但真实的例子如果你的文本内容是JavaScript代码里的a[b[c]]正常情况下没问题因为这里只有两个]]不是]]。但万一代码里有]]比如某些模板引擎或者正则表达式里出现了这种序列又必须在CDATA里表示你就只能拆成多段CDATA拼接。这是XML语法层面的硬规则没有任何变通余地。我在测试某套消息中间件时向一个XML消息里塞Base64编码后的数据Base64本身就容易产生奇怪的字符组合尤其末尾的补位字符和换行符都还好一旦编码后的结果里出现]]解析过程立刻出事。排查了半天最后发现是编码数据跟CDATA定界符冲突当场调整了编码分段逻辑才把问题解决。3.2 HTML模式下CDATA标记为何经常“失灵”现代HTML文档中![CDATA[开头的写法在script元素里还能起一点作用但在普通HTML内容里基本是无效的。HTML解析器不认为CDATA是一个节点类型它对这串字符的处理方式跟XML解析器完全不同。在HTML文档的普通标签里如果你写div![CDATA[phello/p]]/div浏览器渲染出来的结果大概率不是你想的那样它会把这串![CDATA[...]]当作文本节点的一部分显示出来用户界面上直接出现![CDATA[这些字符。这其实挺好理解的HTML的解析规则跟XML是两套体系HTML5规范里压根没有CDATA节点这个概念。但有一个例外在HTML里的script标签内如果你使用了类似![CDATA[的写法一部分浏览器在兼容XHTML时会把它当作文本内容忽略掉。而且要注意在HTML解析模式下script内部的内容本身就是CDATA式处理——浏览器不会解析里面的标签最多只关心/script这个闭合串所以你在script内部看到![CDATA[其实是在向XML解析器打招呼实际内容该怎么跑还是怎么跑。我把这个归纳为一句话CDATA是XML世界的公民在HTML世界里顶多算个访客。写前端代码时不要指望HTML文档里能用DOM拿到CDATASection节点遇到解析结果跟预期不符合的情况先检查你是不是对HTML用了XML视角。3.3 XHTML夹缝中的CDATA一种妥协的写法XHTML当年为了兼HTML和XML两者之长设计出了一套所谓“兼容”规范。在XHTML里script和style元素的内容在XML解析器看来需要转义或CDATA保护不然里面的就会出问题于是很多老代码里都写了类似这样的结构script typetext/javascript //![CDATA[ function hello() { if (a b c d) { ... } } //]] /script这段结构的目的很简单当XHTML文档以XML模式发送时XML解析器看到![CDATA[就跳过整段内容不会因为代码里的和报错当它被当成普通HTML解析时//可以让JavaScript把这行CDATA标记当作注释。同理样式表里的/*![CDATA[*/也起着类似的作用。这类写法在今天的前端工程中基本见不到了毕竟页面普遍使用真正的HTML5。但在有些老项目、内部系统、或者需要严格输出XHTML的场景你仍然可能遇到这样的代码。知道它是怎么来的排查时就不会对着CDATA标记一头雾水了。4. 开发者最常用的几个CDATA实战场景4.1 RSS/Atom源内容里的CDATA如果要说CDATA在现实世界中使用频率最高的地方RSS和Atom订阅源绝对名列前茅。RSS 2.0的description或content:encoded元素中经常出现大段被CDATA包裹的HTML内容。随便举个我在解析订阅源时遇到的例子item title今日技术分享/title description![CDATA[p文章摘要strongCDATA/strong 真的很重要/p]]/description /item你在写RSS消费端程序时如果用DOMParser解析这个XML拿到description节点的文本内容就是p文章摘要strongCDATA/strong 真的很重要/p不需要自己解码可以直接塞进富文本渲染器。而如果发布方没用CDATA而是转义过了文本节点里的内容就会是lt;pgt;文章摘要...还必须额外做一轮decodeURIComponent之类的处理才能用中间很可能踩到双重重编码的坑。我做一个内容聚合后端时为了稳妥每次拿到一个节点内容都会检查它的childNodes[0].nodeType是不是CDATA类型如果是就直接取文本如果不是就尝试还原实体。这套逻辑跑下来各类RSS源的兼容性提升了不少。4.2 前端模板与JSON-LD中的CDATA前端工程化之后直接接触CDATA的人确实少了但有两个地方非常容易碰到。第一个是老式模板里的静态内容嵌入第二个则是JSON-LD结构化数据。JSON-LD里面某些字段如articleBody会要求按原生HTML存储。你若是用模板引擎直接在script标签里输出这个字段很可能遇到特殊字符被转义的问题。有些开发者就会在HTML模板里写成script typeapplication/ldjson { context: https://schema.org, type: Article, articleBody: ![CDATA[一段包含 emHTML/em 的内容]] } /script但这里有个陷阱我必须提醒你JSON-LD的数据是按JSON语法解析的不是XML/CDATA语法。JSON解析器看到![CDATA[这串字符只会当作普通字符串的一部分而不是把它识别成一个区段。如果你真要把HTML塞进JSON字段正解是对HTML做JSON字符串转义引号、换行、尖括号其实不必转义但要确保字符串闭合正确而不是依赖CDATA。换句话说JSON里没有CDATA。很多人容易把“XML习惯”带到JSON场景结果生成出来的结构看起来有板有眼解析时却全部原样字符串输出。所以模板渲染JSON-LD时正确做法是用语言自带的json序列化函数让HTML作为普通字符串存进JSON浏览器解析后再做一次字符串读取就够了。4.3 给XML文档喂数据时绕过转义的坑服务端发数据时如果目标接收方要求XML格式我们经常在后台用拼接字符串的方式生成文档而不是用DOM API。这时直接用![CDATA[包住内容看起来很方便实际上很容易踩坑。比如有套内部接口业务方要传一段HTML给前端渲染。简化后代码大概是这样的const xml content![CDATA[${htmlContent}]]/content;这段代码有一个严重隐患如果htmlContent本身包含]]生成的XML就是非法的解析端直接挂掉。此外如果htmlContent里包含!DOCTYPE或者其他恶意构造的内容还可能诱导XML外部实体解析带来安全风险。安全一点的方案是先判断内容里是否包含]]有的话就分段拼接。不过工程上更推荐用XML DOM库去生成节点让序列化逻辑自己去处理这些边界情况。你要明白手拼XML字符串一时爽后续维护火葬场。DOM序列化做CDATA编码也许并不完美但至少保证输出是合法XML。5. 我踩过和帮人排查过的细节坑5.1 序列化时CDATA节点被“同化”成普通文本DOM解析器把![CDATA[解析成CDATA节点之后一不留神再序列化回去CDATA区段可能就不见了。这个现象特别容易出现在“读取XML - 修改某个字段 - 重新输出XML”的管道场景里。比如你用DOMParser解析一个XML文档遍历到description节点的CDATA子节点想给它追加一段内容。如果你直接用appendData()或修改nodeValue节点类型不会变化。但如果你一不小心用了replaceChild()或者用createTextNode()新建了文本节点再插进去原CDATA节点可能就被替换成Text节点了。这里有个特别隐蔽的情况在一些底层实现中对CDATA节点做某些文本操作时序列化框架可能默认把CDATA当成普通文本处理导致输出时出现多余的转义。我记得排查过一个代码生成器的问题模板里明明是CDATA内容经过中间层处理输出XML时内容里的全部变成了lt;原因就是中间层把CDATASection转成了Text节点再走通用序列化逻辑。排查这类问题最有效的手段是输出前检查nodeType发现不等于4就及时纠正。另外如果你的中间层是通过innerHTML或者textContent去获取内容的那更要警惕这两个属性在XML DOM里未必把CDATA节点当作特殊类型返回不同引擎实现还有些微差异。5.2 解析器和正则的共同陷阱与“]]”相关的边界条件很多人写解析器时会先replace(/!\[CDATA\[/g, )来手动剥掉CDATA壳。这种做法在简单场景下没问题但遇到嵌套内容或评论里包含CDATA标记时正则就会失灵。更关键的是标准的XML解析器在遇到非法CDATA内容时会直接抛出解析错误而手动正则处理不会告诉你哪儿错了只会留下一个莫名其妙的半截字符串。举一个我处理过的问题一个博客系统迁移数据导出的XML包中有很多工单内容工单内容里恰好就有用户粘贴的代码片段代码里有]]这个序列。导入时整段XML解析失败定位到所在行才发现是CDATA区段内部出现了结束定界符。想直接改内容很麻烦毕竟那是用户数据。最稳妥有两条路一是读取的时候用标准解析器允许它报错并捕获解析失败的具体行号二是用流式解析把CDATA内容按片断读取遇到]]就自然闭合然后再开一个新的CDATA区段。如果只是单纯的字符串处理你也可以用这个思路替换把内容里的]]全局替换为]]]]![CDATA[它就能保证原内容在语义上不变同时在CDATA语法上合法。这个技巧看起来有点绕但在很多语言里都是通用的。5.3 XML声明、编码、CDATA节点值获取的细节有些CDATA里的文本包含非ASCII字符比如中文、日文、韩文或者emoji。当DOM解析器正确识别了解析文档的编码通过XML声明里的encodingCDATA节点里拿到数据通常都正常。但如果你在服务端用字节流读取XML没有声明编码就直接按UTF-8解码CDATA里的中文大概率变成乱码而且这个乱码不会因为读取data而自动恢复。我在处理一个来自老旧系统的XML文件时它的XML声明写的encodinggb2312文件实际内容却是GBK编码。用String方式读取再解析CDATA节点里的文本全是乱码。最终的解决方式是读取原始字节、先用GBK解码整个XML再转成UTF-8字符串交给解析器。这类编码问题如果你没碰到过CDATA可能不会知道它跟普通文本节点的区别——普通文本节点里的实体引用也许能掩盖一部分编码问题但CDATA是原样存储编码错误一点都藏不住。另外从CDATA节点取值时还有一个容易被忽略的点CDATA区段内的空白字符会被完整保留。普通XML解析器在解析元素时会折叠或忽略某些空白但CDATA里的空格、换行、制表符一个都不会少。在写比较字符串、或者做内容哈希时要特别留意两边的空白一致性否则你辛苦处理了半天数据最后还是比对不上。6. 别让工具选错CDATA相关的解析与反序列化经验6.1 DOMParser与XMLSerializer这对组合在浏览器或Node环境里最常用的一对解析/序列化工具是DOMParser和XMLSerializer。DOMParser在text/xml模式下会把CDATA正确解析成CDATASection节点XMLSerializer在序列化时也会主动为CDATA节点生成![CDATA[...]]的包裹。这里有一个细节值得多说一句你在HTML文档里执行new DOMParser().parseFromString(htmlStr, text/html)得到的DOM里是不会有CDATA节点的。而在application/xml模式下只要XML合法CDATA节点就会生成。所以排查问题前先问自己一句我到底是在解析XML还是在解析HTML这个判断能帮你在最短时间内缩小问题范围。Node环境如果涉及到XML功能原生的DOMParser和XMLSerializer在较新版本里也能用但一些老项目里用的是第三方库。第三方库对CDATA的支持参差不齐有的把CDATA直接折叠成Text节点有的遇到]]自动分段处理。我用某个库时发现它在序列化时默认把CDATA当普通文本输出后来只能靠给每个CDATA节点加一个特殊标记属性序列化后手动恢复CDATA包裹麻烦得很。能用标准库尽量别引偏门依赖。6.2 动态生成XML时到底用CDATA还是转义一个决策框架平时写代码什么时候应该用CDATA什么时候应该老老实实转义这个问题没有绝对答案但可以有个决策参考。如果你的内容里包含大量富文本或代码片段且这些片段里有高频的和用CDATA是首选它可读性好、调试方便。如果你的内容是用户输入的纯文本或者短字段且内容不可信那么建议优先转义而不是CDATA。原因很简单CDATA区段虽然不会被XML解析器解析但在某些下游处理环节里CDATA里的内容可能被当成HTML片段直接渲染如果用户在里面输入了恶意脚本或特殊标签安全问题就会暴露出来。举个实际案例一个评论系统把用户提交的Markdown源文按照XML存档内容放进CDATA里然后通过接口返回给后台编辑器再渲染。某次因为解析环节的配置不正确用户提交的img srcx onerroralert(1)被当作HTML渲染触发了XSS。虽然这更多是渲染层的问题但CDATA的保护“只保护不被XML解析器解析不保护不被其他解析器解析”这个边界务必心里有数。用转义方案时内容里的和都会被实体化序列化后只有权威解析器才能还原用CDATA方案时内容保持原样后续使用方必须自己知道这是一段可能含HTML的内容不能直接插入到无保护的渲染环境。选择权在你但责任也要一并担住。7. 走到最后的提醒我这些年跟XML、DOM、各种解析器打交道最大的一个体会是CDATA这个语法本身并不复杂但它的存在感极低低到出了问题你会压根想不起它。可真出了问题排查方向往往就是CDATA边界条件、解析模式、节点类型这三者之间的错位。如果你手头有老系统需要对接XML接口我建议你在代码里加一层针对CDATA的封装读取内容时统一走一个getTextContentSafe()方法内部判断nodeType 4是CDATA就直接返回data否则再做实体还原生成内容时用一个appendCDATA()方法内部做好]]的分割。这层封装虽然只有几十行代码但之后你维护起来会轻松很多。其实还有一个更低调的建议团队代码评审里如果有人跟你讨论“这个地方该用CDATA还是转义”请你认真跟他把场景捋清楚因为无数线上事故恰恰是从这两种方案纠结不清开始的。你如果能按前面说的决策框架走一遍基本能避免九成以上的返工。我当初要是早明白这些也不至于在解析器上报错堆里蹚到凌晨两点。希望大家看完这篇文章能把CDATA这层窗户纸捅破以后看到 XML 里的![CDATA[一眼就知道它在表达什么、有哪些风险、怎么用才稳。