jQuery动态载入JS文件研究
在前端开发过程中,有时候会遇到插件化设计的需要。在运行过程中动态的加载一些资源文件。而在动态加载js文件的时候,遇到一些有趣的现象。以此让我们简单的探讨一下。
假设我们现在有一个页面,会动态加载一段html片段,并将其显示在页面上。而这个片段中会携带<script>。
index.html |
<!DOCTYPE html> <html lang="en"> <head> <!-- jQuery --> <script src="//code.jquery.com/jquery-1.11.2.min.js"></script> </head> <body> <div id="cntr"></div> <script> $(function() { $("#cntr").load("htmlWithJS.html"); }); </script> </body> </html> |
htmlWithJS.html |
<script> alert("include!"); </script> |
顺利载入:
接着,我们改写一下htmlWithJS.html。将script改成引用一个test.js文件。内容不变:
htmlWithJS.html |
<script src="test.js"></script> |
test.js |
alert("include!"); |
顺利载入,但是出现了warning:
这行文字表述的意思是,文档中包含了同步的http请求。而这个请求会阻塞当前的主线程那个,从而降低用户体验(因为载入过程中会导致页面卡顿)。
看起来问题似乎很好解决,我们只要添加html5的属性async即可:
htmlWithJS.html |
<script src="test.js" async="async"></script> |
运行结果:
问题依旧没有被解决,而查看网络请求,则会发现test.js跟了一串尾巴:
看来是jQuery搞的鬼。让我们看看jQuery的load方法描述:
Whencalling .load() using aURL without a suffixed selector expression, the content is passed to .html() prior toscripts being removed.
jQuery会进行html转换。为了更好的理解,让我们看一下jQuery源代码:
jQuery.ajax({ url: url, // if "type" variable is undefined, then "GET" method will be used type: type, dataType: "html", data: params }).done(function( responseText ) { // Save response for use in complete callback response = arguments; self.html( selector ? // If a selector was specified, locate the right elements in a dummy div // Exclude scripts to avoid IE 'Permission Denied' errors jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : // Otherwise use the full result responseText ); }).complete( callback && function( jqXHR, status ) { self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); }); |
可以看出,如果没有selector则直接调用了html方法。
接着,我们读一下html方法的文档:
By design, anyjQuery constructor or method that accepts an HTML string — jQuery(), .append(), .after(), etc. — canpotentially execute code. This can occur by injection of script tags or use ofHTML attributes that execute code (for example, <img οnlοad="">). Do not usethese methods to insert strings obtained from untrusted sources such as URLquery parameters, cookies, or form inputs. Doing so can introducecross-site-scripting (XSS) vulnerabilities. Remove or escape any user inputbefore adding content to the document.
好像没有什么关于script的具体内容。我们接着看看源代码:
html: function( value ) { return access( this, function( value ) { var elem = this[ 0 ] || {}, i = 0, l = this.length; if ( value === undefined ) { return elem.nodeType === 1 ? elem.innerHTML.replace( rinlinejQuery, "" ) : undefined; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && ( support.htmlSerialize || !rnoshimcache.test( value ) ) && ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && !wrapMap[ (rtagName.exec( value ) || [ "", "" ])[ 1 ].toLowerCase() ] ) { value = value.replace( rxhtmlTag, "<$1></$2>" ); try { for (; i < l; i++ ) { // Remove element nodes and prevent memory leaks elem = this[i] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; } } elem = 0; // If using innerHTML throws an exception, use the fallback method } catch(e) {} } if ( elem ) { this.empty().append( value ); } }, null, value, arguments.length ); }, |
可见,先调用empty清空,再用append将元素添加进去。我们继续看看append代码:
append: function() { return this.domManip( arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.appendChild( elem ); } }); }, |
domManip方法内容较多,我就只挑一部分出来:
…… fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); …… scripts = jQuery.map( getAll( fragment, "script" ), disableScript); |
domManip中会调用buildFragment方法创建一个dom片段,将文本转化成dom元素,之后遍历script元素并更改其type属性:
// Replace/restore the type attribute of script elements for safe DOM manipulation function disableScript( elem ) { elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; return elem; } function restoreScript( elem ) { var match = rscriptTypeMasked.exec( elem.type ); if ( match ) { elem.type = match[1]; } else { elem.removeAttribute("type"); } return elem; } |
在domManip的回调函数中,会正常的添加元素。但是如我们所见,script的type已经被更改,所以不会真的载入这段脚本。让我们回到,domManip方法本身,看看接下去发生了什么:
// Reenable scripts jQuery.map( scripts, restoreScript ); // Evaluate executable scripts on first document insertion for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl ) { jQuery._evalUrl( node.src ); } } else { jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); } } } |
好了,快到目的地了。我们看看_evalUrl里面是什么内容:
jQuery._evalUrl = function( url ) { return jQuery.ajax({ url: url, type: "GET", dataType: "script", async: false, global: false, "throws": true }); }; |
是否恍然大悟?虽然我们在插入的script中指定了async属性,但是jQuery不会读取这个属性,而是创建一个async为false的ajax请求再次发送。唯一读取的,只是url而已。
我们继续看看条件分支的另一边:
jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); |
jQuery会将内联的script代码通过eval方式执行。
* 在jQuery 1.8.x及之前版本中,script片段不会被添加进文档中,因而如果使用了type="text/template"的script也会被忽略掉,例如underscore、lodash中的如下模板会被append无视掉(所以请尽量保持jQuery库的更新)。
好了,好了。言归正传,如果我们想使用async来载入script怎么办呢?让我们动动手自己实现一个async吧:
$.fn.extend({ asyncLoad: function(url) { var $cntr = this; $.get(url, function(data) { // Parse HTML and modify script type var $content = $("<div>").html(data); var scripts = $content.find("script[src]"); scripts.each(function(i, script) { script.type = "tmpType"; $.getScript(script.src); }); $cntr.html($content.children()); $content.find("script[src]").removeAttr("type"); }); return this; } }); |
但是使用getScript方法有一个缺点,通过该方法引入的js文件无法被debug。
所以我们可以做如下修改:
$.fn.extend({ asyncLoad: function(url) { var $cntr = this; $.get(url, function(data) { // Parse HTML and modify script type var $content = $("<div>").html(data); var scripts = $content.find("script[src]"); scripts.each(function(i, script) { script.type = "tmpType"; var _script = document.createElement('script'); _script.type = 'text/javascript'; _script.src = script.src; document.head.appendChild(_script); _script.onload = function() { $(_script).remove(); }; }); $cntr.html($content.children()); $cntr.find("script[src]").removeAttr("type"); }); return this; } }); |
(test.js出现在了Sources Tab中)
以上就是这次的jQuery动态载入文件的研究内容。建议可以自己通过断点调试的功能一步步跟踪jQuery代码来加深了解。