在前端动态渲染场景中,把一段HTML字符串插入页面几乎是绕不开的操作。不少同学习惯直接用$(selector).html(str)来完成任务,却很少关心这行代码背后发生了什么。jQuery提供了一个更底层的入口jQuery.parseHTML它负责把字符串解析成节点数组,其中对script标签的处理直接关系到页面安全。这篇文章围绕源码把这个过程讲透,重点看keepScripts参数如何决定脚本的生死,以及这个设计在防御XSS上有什么门道。

parseHTML的源码结构与执行流程
先看jQuery 3.x中jQuery.parseHTML的核心实现,函数签名是jQuery.parseHTML(data, context, keepScripts)。三个参数分别代表待解析的HTML字符串、解析所依赖的文档上下文,以及是否保留脚本标签。整个函数的第一步是参数归一化:如果第二个参数是布尔值,说明调用者省略了context,jQuery会自动把参数往后挪一位。
接下来是一段容易被忽略的防御代码:
// 源码节选(简化)
parseHTML: function( data, context, keepScripts ) {
if ( typeof data !== "string" ) {
return [];
}
if ( typeof context === "boolean" ) {
keepScripts = context;
context = false;
}
var base, parsed, scripts;
if ( !context ) {
// 防止toString导致的意外解析
if ( !parsedHtmlCache[ data ] ) {
doc = document.implementation.createHTMLDocument( "" );
base = doc.createElement( "base" );
base.href = document.location.href;
doc.head.appendChild( base );
parsedHtmlCache[ data ] = doc.body.innerHTML;
}
}
...
}这里有个关键细节:当不传context时,jQuery会创建一个脱离主文档的createHTMLDocument沙箱来解析字符串。这个离屏文档不会执行脚本、不加载图片,解析出来的字符串再交给后续流程。这正是防御设计的第一层。最终函数调用jQuery.buildFragment生成文档碎片,并通过jQuery.map(nodes, ...)把碎片中的节点摊平成数组返回。
keepScripts如何决定script标签的命运
buildFragment内部在处理完字符串后,会主动收集所有script标签:scripts = jQuery.map( nodes, function( elem ) { return elem.nodeName.toLowerCase() === "script" ? elem : undefined; } )。随后在返回之前执行这样一段逻辑:
// buildFragment中的关键判断(简化)
if ( !keepScripts && node.parentNode ) {
jQuery.nodeName( node, "script" ) ?
scripts.push( node ) :
node.type.match( rscriptType ) !== null && ...
}
// 脚本被收集后,默认情况下直接从结果中剔除
// 只有 keepScripts 为 true 时才会保留并允许后续执行也就是说,无论keepScripts取什么值,script节点都会被单独抽出来收集。区别在于:keepScripts为false(默认)时这些脚本不会进入返回结果,调用者拿到的节点数组里根本没有script;而为true时,jQuery.ajax等内部调用方会把这些脚本交给jQuery.globalEval执行,后者会把脚本插入head再立即移除,从而在全局作用域跑起来。
还有一点值得注意:$(el).html(str)在内部调用parseHTML(str, el, false),所以通过html方法插入的脚本默认是不执行的。但$(el).append(str)在旧版本中走的是不同路径,行为存在版本差异,升级jQuery后出现"脚本突然不执行了"的问题多半源于此。理解这个参数的历史变化,排查这类诡异现象会快很多。
丢弃脚本不等于绝对安全:XSS防御的正确姿势
很多文章说"parseHTML默认不执行脚本所以是安全的",这个说法只对了一半。丢弃script确实堵住了最直接的执行通道,但XSS的载体远不止script标签。带有onerror属性的img标签、a标签的javascript:伪协议、style中的表达式等,都不依赖script标签就能触发攻击。看一个典型例子:
// 危险!虽然script被过滤了,但事件属性依然生效 var html = '<img src=x onerror="alert(document.cookie)">'; $( "#container" ).html( html ); // onerror 触发,XSS 成功
正确的防御思路是分层处理:第一层,绝不把不可信的富文本直接交给任何HTML解析入口,包括parseHTML和innerHTML;第二层,对必须渲染富文本的场景,先用DOMPurify这类专业的净化库过滤,再交给jQuery:
// 推荐做法:先净化再解析 var clean = DOMPurify.sanitize( untrustedHtml ); var nodes = jQuery.parseHTML( clean, document, false ); $( "#container" ).empty().append( nodes ); // 如果确实需要执行动态脚本,显式创建并插入 var s = document.createElement( "script" ); s.src = "https://example-cdn.ipipp.com/app.js"; document.head.appendChild( s );
此外,如果你确实需要keepScripts为true的场景(例如加载一段需要执行的HTML片段),务必保证这段字符串来源可信,最好来自自己的服务器而非用户输入或第三方接口。在审计代码时,全局搜索parseHTML、.html(和innerHTML三处调用点,逐个确认数据来源,是最有效的自查手段。默认拒绝执行脚本只是jQuery给你的一道保险,真正可靠的安全边界,永远建立在"对输入内容做净化"这条原则上。
jQuery.parseHTMLkeepScriptsXSS防御修改时间:2026-09-16 21:28:42