Interviewer: What are the highlights of your project? Me: Solved the problem of JS script loading failure!

Interviewer: What are the highlights of your project? What problem did it solve?
You: Well...
Interviewer: Go back and wait for the notification.

The above dialogue is really the most heart-wrenching scene in the interview session. If you want to calmly face the project questioning session during the interview, then you must read today's article.

I am Teacher Du Yizichen, and today I will take you to solve the problem of js script loading failure.

The danger of JS loading failure

We all know that modern web pages are inseparable from JS, which can make the page more dynamic and interactive.

However, JS may also fail to load, resulting in disordered page styles, or even a white screen that cannot be used.

This is very detrimental to user experience, especially for single-page applications. If JS cannot be loaded, users cannot continue to browse the page.

So, what are the reasons for JS loading failure?

It may be that the network is unstable, it may be a server error, it may be a cross-domain problem, or it may be other unknown factors.

We have no control over these causes, but we can fix load failures with a simple solution: try again!

Solution to JS loading failure

Retry means that when JS loading fails, re-request one or more times until it succeeds.

This can increase the probability of successful loading and prevent users from seeing the wrong page.

So, how to achieve retry? In fact, only two problems need to be solved:

  1. When to retry?
  2. How to retry?

When to retry?

To know when to retry, we need to know when JS fails to load.

The simplest is to add an onerror event to the script tag. The script fires this event when an error occurs.
For the convenience of testing, we have three JS locally, named 1, 2, and 3, and output 1, 2, and 3 respectively.

<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>

image.png

Although it is possible to do this, it is not the best, and it will be more troublesome. Especially in an engineering environment, these scripts are automatically generated, and it will be very complicated to add onerror events.

So is there a better way? Of course there is! We can use the principle of event delegation to listen to the error event on the window, and then determine whether the error is caused by the script tag.

注意:这里我们要在第三个参数传入 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>

image.png

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

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

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

image.png

可以看到,图片和 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>

image.png

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

如何重试?

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

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

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

  1. 备用域名列表
  2. 要重试的 script 的路径
  3. 已经重试过几次( 为了知道下一次要重试的备用域名是什么 )。

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

<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>

image.png

image.png

可以看到 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>

image.png

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

总结

现在我们的问题已经解决了,但其实仍然可以再深入的去挖掘,比如 script 元素有 defer 怎么办?有 async 怎么办?这里就不展开叙述了。

还有精力的话可以再学一下工程化,在笔面试的时候直接就会惊呆面试官,当面吊打!

如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!

Guess you like

Origin juejin.im/post/7246948333277184061