JS 加载失败的危害

我们都知道,现代的网页离不开 JS,它可以让页面变得更加动态和交互。

但是,JS 也有可能加载失败,导致页面样式错乱,甚至白屏无法使用。

这对用户体验是非常不利的,尤其是对于单页应用,如果 JS 加载不出来,用户就无法继续浏览页面了。

那么,JS 加载失败的原因有哪些呢?

可能是网络不稳定,可能是服务器出错,可能是跨域问题,也可能是其他未知的因素。

我们无法控制这些原因,但我们可以通过一个简单的方法来解决加载失败的问题:重试!

JS 加载失败的解决方案

重试就是当 JS 加载失败时,重新请求一次或多次,直到成功为止。

这样可以增加加载成功的概率,也可以避免用户看到错误的页面。

那么,怎么实现重试呢?其实只需要解决两个问题:

  • 什么时候重试?

  • 如何重试?

什么时候重试?

要知道什么时候重试,我们就要知道什么时候 JS 加载失败。

最简单的就是给 script 标签,加一个 onerror 事件。当出现错误的时候 script 会触发这个事件。
为了方便测试,我们本地有三个 JS,名字分别为 1、2、3,分别输出 1、2、3。

<script onerror="console.log(123)" src="http://127.0.0.1:5500/js/1.js"></script>

<script onerror="console.log(123)" src="http://other-domain-one.com/js/2.js"></script>

<script onerror="console.log(123)" src="http://127.0.0.1:5500/js/3.js"></script>

这样做虽然可以,但不是最好的,会比较麻烦,又特别是在工程化的环境里边,这些 script 都是自动生成的,要加上 onerror 事件的话就会很复杂。

那么有没有更好的方法呢?当然有!我们可以利用事件委托的原理,在 window 上监听 error 事件,然后判断是否是 script 标签引起的错误。

注意:这里我们要在第三个参数传入 true,表示在捕获阶段触发事件,因为 error 事件不会冒泡。

<script src="http://127.0.0.1:5500/js/1.js"></script>

<script src="http://other-domain-one.com/js/2.js"></script>

<script src="http://127.0.0.1:5500/js/3.js"></script>

<script>

window.addEventListener('error', (event) => {

console.log('有错误!');

}, true)

</script>

但是我们能这么写吗?同学们思考几秒钟。

其实是不行的,因为当前面的 JS 失败的时候,error 事件还没有注册,所以应该在最上方。

<script>

window.addEventListener('error', (event) => {

console.log('有错误!');

}, true)

</script>

<script src="http://127.0.0.1:5500/js/1.js"></script>

<script src="http://other-domain-one.com/js/2.js"></script>

<script src="http://127.0.0.1:5500/js/3.js"></script>

可以看到,我们已经触发 error 事件了。

但是这样还不够精确,因为 error 事件可能由其他原因引起,比如图片加载失败或者 JS 代码中抛出异常。

我们怎么区分呢?我们打印一下 error 的 event 值,看看它们有什么区别。

可以看到,图片和 script 引起的错误都是 Event 对象,而 JS 代码中抛出的错误是 ErrorEvent 对象。

并且 Event 对象中有一个 target 属性,指向触发错误的元素。

所以我们可以根据这两个特征来判断是否是 script 标签引起的错误。

<script>

window.addEventListener('error', (event) => {

// 拿到触发错误的标签

const tag = event.target;

// 便签的名称必须是 'SCRIPT' 与 event 错误的类型不能是 ErrorEvent

if (tag.tagName === 'SCRIPT' && !(event instanceof ErrorEvent)) {

console.log('script 加载错误');

}

}, true)

</script>

这样我们就可以准确地捕获到 script 加载失败的情况了。

如何重试?

实现重试,我们就要重新创建一个 script 元素,并且修改它的 src 属性为一个新的域名。

为什么要修改域名呢?因为之前加载失败的域名可能已经失效了,所以我们需要准备一些备用域名,在加载失败时依次尝试。

那么我们需要记录以下三个信息:

  • 备用域名列表

  • 要重试的 script 的路径

  • 已经重试过几次( 为了知道下一次要重试的备用域名是什么 )。

根据这些信息,我们可以写出以下代码:

<script>

// 备用域名列表

const domains = [

'other-domain-two.com',

'other-domain-three.com',

'other-domain-four.com',

'127.0.0.1:5500',

];

// 重试的信息

const retryInfo = {};

window.addEventListener('error', (event) => {

const tag = event.target;

if (tag.tagName === 'SCRIPT' && !(event instanceof ErrorEvent)) {

// 首先我们要知道是谁失败了,他请求的 js 是什么

// 可以通过 url.pathnam 得到请求的 js 的名字

const url = new URL(tag.src);

// 我们判断一下重发的信息里有没有重试过这个 js

if (!retryInfo[url.pathname]) {

// 没重试过就给它添加一个

retryInfo[url.pathname] = {

times: 0, // 第几次重试从 0 开始

nextIndex: 0, // 重试的域名也从 0 开始

};

}

// 取出要重试的信息

const info = retryInfo[url.pathname];

// 这里我们要判断一下,重试的次数是否小于域名的列表长度,防止所有域名都失败时一直重复重试

if (info.times < domains.length) {

// 重试就要生成一个新的元素

const script = document.createElement('script')

// 那我们要重试呢就是替换一下失败的域名,所以可以利用 url.host,把要重试的域名替换它,

url.host = domains[info.nextIndex]

// 然后将新的 url 添加到新的 script 的 src 里

script.src = url.toString()

// 将新的 script 呢加入到失败的 script 之前

document.body.insertBefore(script, tag)

// 最后不要忘记重试信息的索引都要加 1

info.times++

info.nextIndex++;

}

}

}, true)

</script>

可以看到 2 已经输出了,但是顺序不对,应该是 1、2、3 的顺序,JS 的执行顺序是很重要的,因为他们之间可能有依赖关系,比如说 3 里有依赖 2 的东西,那么先加载 3 就会出现问题了。

出现这个问题的原因就在于新加入的这个元素没有阻塞后续的加载,也就是说我们创建的这个元素必须要它阻塞页面后续的加载。

这里就用到了一个同学们一定接触过,但是早就不使用的东西,同学思考一下,看能不能想到。

其实它叫做 document.write(),这个就会阻塞页面的加载。

<script>

const domains = [

'other-domain-two.com',

'other-domain-three.com',

'127.0.0.1:5500',

];

const retryInfo = {};

window.addEventListener('error', (event) => {

const tag = event.target;

if (tag.tagName === 'SCRIPT' && !(event instanceof ErrorEvent)) {

const url = new URL(tag.src);

if (!retryInfo[url.pathname]) {

// 没重试过就给它添加一个

retryInfo[url.pathname] = {

times: 0, // 第几次重试从 0 开始

nextIndex: 0, // 重试的域名也从 0 开始

};

}

const info = retryInfo[url.pathname];

if (info.times < domains.length) {

const script = document.createElement('script')

url.host = domains[info.nextIndex]

// 阻塞页面后续的加载

// 因为我们是写在 script 标签里 所以要转译一下,否则会被认为是 script 标签的结束

document.write(`<script src="${url.toString()}"></script>`)

info.times++

info.nextIndex++;

}

}

}, true)

</script>

现在再看顺序就正常了,这里的警告是因为 document.write() 有阻塞,但是我们要的就是阻塞,所以就不用管他了。

来源:每天一道web前端面试题

举报/反馈

尚硅谷教育

3.9万获赞 2.1万粉丝
让天下没有难学的技术
北京晟程华科教育科技有限公司官方账号,优质教育领域创作者
关注
0
0
收藏
分享