背景
在不支持打包构建的前端历史项目中,我们经常会有动态引入script的诉求,为了书写方便,开发的时候会习惯性的用jQuery生成script标签再append到body中,并将其视为和原生写法功能完全一致
// jQuery
var $script = $('<script src="xxx.js">');
$('body').append($script);
// ECMAScript
var script = document.createElement('script');
script.src = 'xxx.js';
document.body.appendChild(script);
但它们真的一致么?jQuery在这里什么都没做么?不同写法对我们又会有怎样的影响呢?
先说结论
jQuery写法与原生相比差异如下:
- 并非通过标准的jsonp方式加载JS内容,而是通过一个xhr请求
- 此xhr是同步的,而动态script则是强制async的
- 非法的type赋值会让xhr请求不发送( 不写type是合法的,要写就写对
除了那个同步异步的操作,是不是看起来对我们实际开发影响不大?如果真不大我也不会有心情探究这个问题了,这里需要额外补充一个xhr和jsonp请求JS内容的差异点:
- 重点:在iOS环境下,浏览器cache的JS请求并不会被用于xhr,缓存的方式包括刚请求过、提前prefetch等
所以如果对同步插入没有诉求,能不用jQuery插入就不要用
实际场景
上面我提到了,iOS下的xhr请求JS是不会走缓存的,那么这个影响到底大不大呢?
我们的项目因为一些特殊原因,会在页面内容显示前动态插入7-9个script标签,且均是通过jQuery插入的,大小1KB - 200KB不等,不算小但也没有特别夸张,在PC模拟环境下加载和执行耗时如下:
如果允许xhr使用缓存的话,对应的耗时如下:
但这部分JS在移动端用户的实际使用场景下,加载和执行的实际统计时间如下:
样本里安卓和iOS用户数量基本一致,也没有太多极值,可以理解为比安卓多的部分就是本原因导致的。这可不是95线、75线那种较差环境下的指标,光正常的均值和中位数都能干到1s以上,要知道页面打开满共才2-3s,如果碰到网稍微差一点,那这个页面就奔着5s+去了
处理方案
理想的情况当然是把这些JS原位置替换成原生写法即可,但在我们项目里有两个困难点:
- 这些动态script是有同步要求的,必须在引入的地方执行,不然会缺失必要的上下文
- 引入的重复书写非常多(300+),而且因为历史原因,代码还存储在数据库而非代码库中,无法做到安全的全量修改
最后我的方案是: - 修改jQuery源码,拦截这些已确定的script,不让其走xhr请求
- 在原引入逻辑的上方通过script标签正常引入确认拦截的JS
- 将拦截的script的非Function声明部分都装在各自的init方法中,在原来引入的地方执行init。就类似于引入Vuex后需要在合适的时机执行Vue.ues原理一样,基于Vue的库都会有启动函数,我也将我们动态的几个JS的立即执行部分也分别装进了各自的启动函数,并在原位置执行,模拟同步执行的过程,以便减少逻辑影响
最后的效果就是常见的动态script都会通过jsonp标准模式而非xhr的方式来请求下发,并顺利借道浏览器缓存,预期iOS加载时常可以降低到Android的水平
jQuery源码分析
上面我说针对动态script,jQuery会走xhr来进行请求,但细心的小伙伴在HTML里一搜,发现明明有对应<script>标签呀,但网络上也发了xhr的请求,这是怎么回事呢?这些就需要看看源码了
// 我们使用jQuery插入动态script
// 注意通过jQuery生成的这个script和原生是存在细微差异的
// 例如用原生方式插入本script标签也不会发jsonp请求,原因我还没找到
// 所以要走标准jsonp,就都需要用原生书写,不要偷懒
var $script = $('<script src="xxx.js">');
$('body').append($script);
// jQuery源码(截取并增加注释)
// append方法
append: function() {
// 发xhr的逻辑在这个domManip中,我们等下看
return domManip( this, arguments, function( elem ) {
// 这里是回调函数,会在针对script标签进行加工后、发xhr前执行
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
// 原生的插入动作,也正是有这句,才让我们可以在HTML中搜到script标签结构
// 但为什么标签明明在,却没发jsonp请求呢?往下看
target.appendChild( elem );
}
});
},
// domManip重要部分截取
function domManip (...) {
...
// 所有script标签都被disableScript方法加工了
scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
...
// 发送对应xhr请求
jQuery._evalUrl( node.src );
...
}
// 上面两个片段咱们分开说
// 先说disableScript做了什么
function disableScript( elem ) {
// 经过这行后,script标签的type会被加工,形式例如【trur/text/javascript】
// 很明显,前面加上boolean的type无论如何都是不合法的
// 所以这个奇怪type的script标签会被当作文本块,而不是发网络请求
// 这个type会在后面的逻辑中修正回来,但已插入的script标签修改type并不会让它重新请求
elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
return elem;
}
// 再看看_evalUrl
jQuery._evalUrl = function( url ) {
// 需要注意,因为是走xhr,所以全局的拦截器设置是生效的,就可能会对本次请求进行加工,例如加上时间戳之类
return jQuery.ajax( {
url: url,
type: "GET",
dataType: "script",
async: false, // 注意这里,是同步的,动态script标签也只有通过这种方式才能做到同步
global: false,
"throws": true
} );
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。