【问题标题】:Continue to load HTML when <script src='...' is taking too long to load?当 <script src='...' 加载时间过长时继续加载 HTML?
【发布时间】:2019-06-09 13:14:41
【问题描述】:

我在一次工作面试中被问到:

如果脚本加载时间超过 X 秒,则整个 页面应该被加载(除了这个同步脚本)。请注意,我们 不应将脚本更改为异步运行(例如,通过 附加孩子)。没有服务器。

嗯,我有几种方法:

-remove the dom
-window.abort
-mess up the document by document.write("'</s'+'cript>'")
-moving it to an iframe
-adding headers of CSP

没有任何效果。

这是删除脚本dom标签的代码(例如):

请注意,脚本后面有 TEXT。所以预计会在 1 秒后看到文字。

<body>
  <script>
    setTimeout( function (){
 document.querySelector('#s').parentNode.removeChild(document.querySelector('#s'))

    },1000); //change here
  </script>

<script id ='s'  src="https://www.mocky.io/v2/5c3493592e00007200378f58?mocky-delay=40000ms" ></script>
    <span>!!TEXT!!</span>
</body>

问题

我似乎找不到如何让页面在特定超时后继续加载的技巧。我该怎么做?

Fiddle

顺便说一句,我见过有趣的方法 here

【问题讨论】:

  • 这篇文章可能会回答你的问题:flaviocopes.com/javascript-async-defer
  • @EdOverflow 会使其异步。这个问题明确说我们不应该。
  • 啊,哎呀。我的错,@RoyiNamir。
  • 不管解决方案如何,这都是我听过的最糟糕的面试问题之一。它测试了一个大多数人都不知道的非常具体的 hack,因为这个用例是由可怕的代码气味组成的。答案很可能被新手和专家知道,因为它是纯粹的案例知识。“你将如何解决这个问题?”的真正答案是“我不会。我会弄清楚如何解决这种情况的根本原因,而不是使用一些在我之后处理此文件的人都不会理解的疯狂黑客行为。”见鬼,这甚至可能是他们想要的答案。叹。我跑题了。
  • @Vulcan 这解决了这里的 cmets 存在的一些相同问题,虽然这是一个聪明的解决方案,但它仍然没有解决 OP 的非常具体的请求,即不能加载文件",也不允许加载脚本标签后面的 HTML。

标签: javascript html


【解决方案1】:

如果页面没有到达设置window.isDone = true 的块,页面将重新加载一个查询字符串,告诉它不要首先将慢速脚本标签写入页面。

我正在使用setTimeout(..., 0) 将脚本标签写入当前脚本块之外的页面,这不会使加载异步。

请注意,慢速脚本正确阻止了 HTML,然后在 3 秒后页面重新加载,HTML 立即出现。

这可能在 jsBin 内部不起作用,但如果您在独立的 HTML 页面中本地测试它,它会起作用。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  
  
</head>
<body>
  <script>
        
    setTimeout( function (){
       
     
      //    window.stop();
    
      window.location.href = window.location.href = "?prevent"
         
 
    }, 3000); //change here
  </script>
  
  <script>
    function getSearchParams(){
      return window.location.search.slice(1).split('&').reduce((obj, t) => {
        const pair = t.split('=')
        
        obj[pair[0]] = pair[1]
        
        return obj
      }, {})
    }

    console.log(getSearchParams())
    
    if(!getSearchParams().hasOwnProperty('prevent')) setTimeout(e => document.write('<script id ="s"  src="https://www.mocky.io/v2/5c3493592e00007200378f58?mocky-delay=4000ms"><\/script>'), 0)
  </script>
    <span>!!TEXT!!</span>
</body>
</html>

【讨论】:

  • 脚本所在的页面没有其他页面。
  • 它仍然是一页,只是在服务器端设置了一个标志。无论如何,这不起作用,我只是对其进行了测试,并且当发生非常长的同步操作时,超时永远不会触发。
  • @RoyiNamir 我已经更新了这个答案,只使用一页,本质上是纯粹的前端 javascript
  • 我已经试过了(也在问题中写过)。他们说:(除了这个同步脚本)。其他脚本不可动
  • @RoyiNamir 好吧,这是我最后一次尝试了 :) 这是相同的方法,但相反 - 默认是将脚本标签写入页面,但如果 prevent 标志是目前,它不会被写入,也不会请求脚本。
【解决方案2】:

由于我注意到你说这次采访发生在“很久以前”,所以下面的解决方案可能不是他们当时所期望的。
我承认我不知道他们当时的期望是什么,但是使用今天的 API,这是可行的:

您可以设置一个ServiceWorker,它将像代理一样处理您页面的所有请求(但托管在浏览器中),并且如果请求时间过长,它将能够中止请求。
但要做到这一点,我们需要AbortController API,它仍然被认为是一项实验性技术,所以再一次,这可能不是他们在这次采访中所期望的答案......

不管怎样,下面是我们的 ServiceWorker 完成请求任务时的样子:

self.addEventListener('fetch', async function(event) {
  const controller = new AbortController();
  const signal = controller.signal;

  const fetchPromise = fetch(event.request.url, {signal})
    .catch(err => new Response('console.log("timedout")')); // in case you want some default content

  // 5 seconds timeout:
  const timeoutId = setTimeout(() => controller.abort(), 5000);
  event.respondWith(fetchPromise);
});

And here it is as a plnkr. (有一种作弊方式,它使用两个页面来避免等待整整50s,一个用于注册ServiceWorker,另一个用于出现慢速网络。但是由于要求说慢速网络“偶尔”发生,我认为假设我们能够至少注册一次仍然有效。)
但如果这真的是他们想要的,那么你最好只缓存这个文件。

正如 cmets 中所说,如果您曾经遇到过这个问题 IRL,那么一定要尝试解决根本原因而不是这个 hack。

【讨论】:

  • 不使用fetch算作异步加载脚本? OP 明确指出脚本应该是同步加载的。
  • @Weft 不,这里的 fetch 是从 Service Worker 进行的,但是 html 页面仍然是同步加载的。只需尝试 plunker,您会看到您将等待 5 秒,然后文件才准备好,而不是 50 秒。
  • 问题说脚本标签。网络工作者如何在这里提供帮助?
  • @RoyiNamir 谁谈到了 WebWorkers?我谈到了 ServiceWorker。这些是完全不同的,后者确实允许我们控制如何获取页面上的资源 => 它与问题有关,甚至提供了实现请求的唯一方法。
【解决方案3】:

这是使用window.stop()、查询参数和php 的解决方案。

<html>
<body>
<?php if(!$_GET["loadType"] == "nomocky"):?>
<script>
var waitSeconds = 3; //the number of seconds you are willing to wait on the hanging script
var timerInSeconds = 0;
var timer = setInterval(()=>{
timerInSeconds++;
console.log(timerInSeconds);
  if(timerInSeconds >= waitSeconds){
    console.log("wait time exceeded. stop loading the script")
    window.stop();
    window.location.href = window.location.href + "?loadType=nomocky"
  }
},1000)
setTimeout(function(){
   document.getElementById("myscript").addEventListener("load", function(e){
     if(document.getElementById("myscript")){
        console.log("loaded after " + timerInSeconds + " seconds");
        clearInterval(timer);
     }
  })
},0)
</script>
<?php endif; ?>

<?php if(!$_GET["loadType"] == "nomocky"):?>
<script id='myscript' src="https://www.mocky.io/v2/5c3493592e00007200378f58?mocky-delay=20000ms"></script>
<?php endif; ?>

<span>!!TEXT!!</span>
</body>
</html>

【讨论】:

  • 问题说,没有服务器。
【解决方案4】:

我的猜测是,这是他们试图解决但没有解决的问题,所以他们向候选人询问解决方案,任何有解决方案的人都会给他们留下深刻印象。我希望这是一个诡计问题,他们知道并想看看你是否知道。

鉴于他们的定义,使用 2017 年得到广泛支持的任务是不可能的。使用 ServiceWorkers 在 2019 年是有可能的。

如您所知,在浏览器中,窗口在运行事件循环的单个线程中运行。异步的一切都是某种延迟任务对象,它被放入队列以供稍后执行1。窗口线程和工作线程之间的通信是异步的,Promise 是异步的,通常 XHR 是异步完成的。

如果要调用同步脚本,则需要进行阻塞事件循环的调用。但是,JavaScript 没有中断或抢占式调度程序,因此当事件循环被阻塞时,没有其他任何东西可以运行来导致它中止。 (也就是说,即使您可以启动操作系统与主线程并行运行的工作线程,工作线程也无法使主线程中止脚本的读取。)唯一的如果有一种方法可以在 TCP 请求上设置操作系统级别的超时,但没有,希望您可以对脚本的获取有一个超时。除了通过 HTML script 标记(无法指定超时)之外,获取脚本的唯一方法是 XHRXMLHttpRequest 的缩写)或 fetch,它支持。 Fetch 只有一个异步接口,所以没有任何帮助。虽然可以发出同步的XHR 请求,但根据 MDN(Mozilla 开发者网络),许多浏览器完全拥有deprecated synchronous XHR support on the main thread。更糟糕的是,XHR.timeout()“不应该用于文档环境中使用的同步 XMLHttpRequests 请求,否则它会throw an InvalidAccessError exception。”

因此,如果您阻止主线程等待 JavaScript 的同步加载,那么当您确定加载时间过长时,您将无法中止加载。如果您不阻塞主线程,则脚本将在页面加载后才会执行。

Q.E.D

使用 Service Worker 的部分解决方案

@Kaiido 认为这可以是solved with ServiceWorkers。虽然我同意 ServiceWorkers 旨在解决此类问题,但我不同意他们出于几个原因回答这个问题。在我进入它们之前,让我说,我认为 Kaiido 的解决方案在更普遍的情况下很好,即让完全托管在 HTTPS 上的单页应用程序实现某种资源超时以防止整个应用程序锁定,我对该解决方案的批评更多是因为它是对面试问题的合理回答,而不是整体解决方案的任何失败。

  • OP 说这个问题来自“很久以前”,Edge 和 Safari 的生产版本中的服务工作者支持不到一年。 ServiceWorker 仍不被视为“标准”,AbortController 今天仍未得到完全支持。
  • 为了使其工作,必须在加载相关页面之前安装 Service Worker 并配置超时。 Kaiido 的解决方案加载一个页面来加载服务工作者,然后重定向到具有慢 JavaScript 源的页面。尽管您可以使用clients.claim() 在加载它们的页面上启动服务工作者,但它们仍然不会在页面加载后启动。 (另一方面,它们只需要加载一次,并且可以在浏览器关闭后持续存在,因此在实践中,假设已经安装了 service worker 并不是完全不合理的。)
  • Kaiido 的实现对所有获取的资源施加相同的超时。为了仅适用于有问题的脚本 URL,服务工作者需要在获取页面本身之前预加载脚本 URL。如果没有某种 URL 的白名单或黑名单,超时将适用于加载目标页面本身以及加载脚本源和页面上的所有其他资产,无论是否同步。虽然在这个问答环境中提交一个未准备好生产的示例当然是合理的,但将其限制为一个有问题的 URL 意味着该 URL 需要单独硬编码到服务工作者中,这让我感到不舒服这是一个解决方案。
  • ServiceWorkers 仅适用于 HTTPS 内容。我错了。虽然 ServiceWorker 本身必须通过 HTTPS 加载,但它们并不限于代理 HTTPS 内容。

也就是说,我感谢 Kaiido 提供了一个很好的例子来说明 ServiceWorker 可以做什么。

1来自 Jake Archibald 的 excellent article 解释了异步任务如何排队并由窗口的事件循环执行。

【讨论】:

  • It is possible 他们可能很想检查受访者是否了解最新技术。请注意,ServiceWorkers 是当今行业必须的,而 AbortController 则少一些。
  • @Kaiido 我更新了我的答案,解释了为什么我认为 ServiceWorkers 没有完全回答这个问题,但我同意你的看法,这可能是面试官想要的。 SMH
  • 对于第 1 点,我承认我完全错过了时间信息,你可能是对的,ServiceWorker 的解决方案可能不是他们当时想要的(虽然我们真的不知道是多久以前的曾是)。对于第二点,您只需要注册一次 SW 就可以在每次访问时工作,因此由于要求指出此脚本“有时”太慢,我认为我们可以假设第二次访问的解决方案是可以接受,但我接受对这方面的批评,我什至在我的回答中制定了自己。不过,对于第三点,您似乎已经了解自己,
  • ...这实际上只是一个 MCVE,当然,在设置这样的系统时,您可以(甚至应该)更加精细。如果需要,您可以像读取请求的 POST 数据的内容一样精细,因此将 GET 请求列入黑名单也是可行的。对于第四个,没有。 ServiceWorkers 处理所有网络请求(不幸的是,不在 blob:// 请求上......),即使托管页面确实需要由 https 提供服务。但无论如何,自从 LetsEncrypt 以来,从 https 运行您的页面不再是我所说的“不可接受的约束”。
  • 但总的来说,鉴于这些新信息,我只能同意你的回答;-)
【解决方案5】:

您可以在脚本标签中使用 async 属性,它将异步加载您的 js 文件。 脚本 src="file.js" 异步>

【讨论】:

    猜你喜欢
    • 2020-07-22
    • 1970-01-01
    • 2012-08-23
    • 2016-06-20
    • 2016-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多