jQuery2.2 iframe 脚本注入的上下文 Bug
最近在iframe中注入脚本,发现jQuery.append()
方法和DOMappendChild()
方法的区别:
- DOM API
appendChild()
方法插入的<script>
会在iframe所在上下文中运行 - jQuery(2.2)
.append()
方法注入的<script>
脚本,其执行上下文总是在当前window
事实上,jQuery.append()
方法对<script>
做了特殊处理:
获取脚本内容并通过eval()
在当前作用域下执行,同时禁用了浏览器默认的脚本调度执行。
先看例子
设置父容器的window.id="parent"
,在注入到iframe的脚本中把它打印出来。
如果是与父容器共享上下文则会打印出"parent"
,否则应是undefined
。
window.id = 'parent';
var idocument = $('iframe').prop('contentDocument');
var injected_script = 'console.log("window.id ==", window.id)';
创建一个<script>
标签,使用document.body.appendChild()
方法插入:
var el = idocument.createElement('script');
el.text = injected_script;
idocument.body.appendChild(el);
输出为:window.id == undefined
,说明作用域是隔离的。改用jQuery.append()
方法插入:
var el = idocument.createElement('script');
el.text = injected_script;
$(idocument.body).append(el);
输出为:window.id == parent
,显然jQuery不是单纯调用appendChild()
,还做了别的处理。
下面来看jQuery源码。
禁用脚本标签
在Github可访问jQuery源码,看这个文件:manipulation.js。
在真正插入<script>
标签之前,先进入domManip
方法。
获取所有<script>
脚本,并通过disableScript
函数禁用它们。
function domManip( collection, args, callback, ignored )
// ...
if ( first || ignored ) {
scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
hasScripts = scripts.length;
// ...
callback.call( collection[ i ], node, i );
disableScript
如果做到禁用脚本的呢?请看:
function disableScript( elem ) {
elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
return elem;
}
对于设置了type="text/javascript
的脚本,其type
被重写为
type = "true/text/javascript"
;对于未设置type
的脚本,其type
被重写为false/
。
总之,浏览器不再把该标签识别为页面脚本,从而禁止了浏览器对<script>
的调度执行。
DOM 节点插入
接下来从domManip
的callback
才真正进入.append()
方法。
可以看到.append()
是通过DOM APIappendChild
来实现的。
这时<script>
的type
已经被重写了,浏览器在插入elem
的时候不会自动执行该脚本。
jQuery.fn.extend( {
// ...
append: function() {
return domManip( this, arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.appendChild( elem );
}
} );
},
eval 脚本执行
callback
返回后再次回到doManip
函数中,调用DOMEval
来执行脚本内容。
function domManip( collection, args, callback, ignored ) {
// ...
callback.call( collection[ i ], node, i );
// ...
if ( node.src ) {
if ( jQuery._evalUrl ) {
jQuery._evalUrl( node.src );
}
} else {
jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) );
}
jQuery.globalEval()
最终调用了JavaScripteval
来执行脚本,
所以脚本上下文在jQuery所在的window
里。源码:core.js
jQuery.extend( {
// ...
globalEval: function( code ) {
var script, indirect = eval;
code = jQuery.trim( code );
if ( code ) {
if ( code.indexOf( "use strict" ) === 1 ) {
script = document.createElement( "script" );
script.text = code;
document.head.appendChild( script ).parentNode.removeChild( script );
} else {
indirect( code );
}
}
},
在jQuery最新的master分支中,上下文的问题已经被修正了。
domManip()
方法中最后会调用DOMEval而不是globalEval
,
同时globalEval
也被实现为domEval
了(见master/core.js),
不再使用JavaScripteval()
。
// manipulation.js
DOMEval( node.textContent.replace( rcleanScript, "" ), doc );
// core.js
globalEval: function( code ) {
DOMEval( code );
}
本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/04/07/jquery-script-context-bug.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。