Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to know if a link/script is still pending or has it failed

I'd like to know from the html below, if link[rel=import], link[rel=stylesheet], img and script are pending/loaded/failed/aborted without the need to add listeners beforehand and at any point after the event has happened

<!DOCTYPE html>
<html>
<head>
    <title>App</title>
    <meta charset="utf-8">
    <link rel="import" href="template-bundle.html">
    <link rel="stylesheet" href="bundle.css">
</head>
<body>
    <header><img src="logo.png" alt="App logo"></header>
    <!-- Boilerplate... -->
    <script src="./app-bundle.js"></script>
</body>
</html>

In other words: Is there an interface which provides something similar to a Bluebird's isPending(), isResolved(), isRejected() methods or a regular ES6 Promise?


Bonus question: Is this something can be achieved using a Service Worker?

Since SW can intercept requests and know their status, I was wondering if I can implement an API which returns a Promise that

  • is pending if request is still pending
  • is resolved if load event fired
  • is rejected if error or aborted was fired.

Thanks for the help


Update & Solution:

Thanks to the answers of @pritishvaidya and @guest271314, I was able to come up with a viable solution using MutationObserver that involves watching DOM for additions of resource nodes (link,img,script) and adding a promise to them that will resolve as described above

This works great, with the only caviat that the script tag needs to be inlined in <head> before any other resource. Here's an example

var resourceNodeSelector = 'link[href],script[src],img[src]';
function watchResource (n) {
    var url = n.href || n.src;

    if (!n.matches || !n.matches(resourceNodeSelector)) {
        return;
    }

    if (n.status) {
        return;
    }

    n.status = resourceObserver.promises[url] = new Promise(function (resolve, reject) {
        n.addEventListener('load', resolve);
        n.addEventListener('error', reject);
        n.addEventListener('abort', reject);
        n.addEventListener('unload', function (l) { delete resourceObserver.promises[url]} );
    });
    n.status.catch(function noop () {}); //catch reject so that it doesn't cause an exception
}

var resourceObserver = new MutationObserver(function (mutations) {
    document.querySelectorAll(resourceNodeSelector).forEach(watchResource);
});
resourceObserver.promises = {};
resourceObserver.observe(window.document, {childList: true, subtree: true});

Once the observer is in place, any valid resource element, should have a status promise property that you can check at any point in time

document.querySelector('link').status.then(linkLoaded).catch(linkFailed)

A more elegant solution, that does not involve using the expensive querySelectorAll, should be possible with ServiceWorker, since it can be programmed to intercept and keep track of all resource requests and their status

like image 300
Dogoku Avatar asked Oct 03 '16 05:10

Dogoku


People also ask

Do script tags go after?

The best practice is to put JavaScript <script> tags just before the closing </body> tag rather than in the <head> section of your HTML. The reason for this is that HTML loads from top to bottom. The head loads first, then the body, and then everything inside the body.

Does script tag location matter?

The position in the source is irrelevant - only the position in time matters. You should avoid putting scripts in the <head> if possible as it slows down page display (see the link Alan posted).

Are script tags loaded in order?

Script tags are executed in the order they appear It also means scripts which appear later on the page can depend on things scripts which appear earlier have done. Elements on the page won't render until all the script tags preceding them have loaded and executed.

How do you link a script tag in HTML?

We can link JavaScript to HTML by adding all the JavaScript code inside the HTML file. We achieve this using the script tag which was explained earlier. We can put the <script></script> tag either inside the head of the HTML or at the end of the body.


1 Answers

You can utilize onload, onerror events of <link> element; see Browser CSS/JS loading capabilities at right column.

Create an object to store status of all <link> requests and resolved or rejected Promise corresponding to the <link> element.

Reject Promise at onerror event; use .catch() chained to Promise.reject() to handle error so that Promise.all() will not stop processing resolved promises within array passed as parameter. You can also throw error from .catch() at onerror handler to Promise.all() if any rejected Promise should stop processing of resolved promise within array of promises.

At window.onload event handler, use Promise.all() to process all resolved links, using same function called before window.onload event. To wait for results of Promise.all() to be available, set src of last <script> element to bundle.js at .then() chained to Promise.all()

<!DOCTYPE html>
<html>

<head>
  <title>App</title>
  <meta charset="utf-8">
  <script>
    var handleLinks = {
      links: [],
      isPending: true
    };

    function handleBeforeLoad() {
      if (document.querySelectorAll("link").length === 0) {   
        console.log("links loading state is pending..", handleLinks.isPending);
      } else {
        handleLinks.isPending = false;
        Promise.all(handleLinks.links)
          .then(function(linksContent) {
            console.log("links resolved:", linksContent
                       , "links loading state is pending.."
                       , handleLinks.isPending);
            linksContent.filter(Boolean).forEach(function(link) {
              // `content` property : html `document`,  `CSSStyleSheet` 
              // requested at `<link>` element
              console.log(link); 
            });
            // load `bundle.js`
            document.getElementById("bundle")
            .src = "bundle.js"

          })
          .catch(function(err) {
            console.log("link error:", err.message)
          })
      }
    }
    handleBeforeLoad();
    window.onload = handleBeforeLoad;

    function handleLink(el) {
      handleLinks.links.push(Promise.resolve({
        content: el.import || el.sheet,
        type: el.type,
        rel: el.rel,
        href: el.href,
        integrity: el.integrity,
        isResolved: true
      }));

    }

    function handleLinkError(el) {
      handleLinks.links.push(Promise.reject(new Error(JSON.stringify({
        error: "error loading link",
        type: el.type,
        rel: el.rel,
        href: el.href,
        integrity: el.integrity,
        isRejected: true
      }))).catch(function(err) {
        // handle error
        console.log(err);
        // this will return a resolved Promise
        return "error requesting link " + el.href;
        // `throw err` here if any rejected Promise should
        // stop `Promise.all()` from handling resolved Promise
      }));

    }
  </script>
  <link onload="handleLink(this)" 
        onerror="handleLinkError(this)" 
        rel="import" 
        href="template-bundle.html" 
        type="text/html">
  <link onload="handleLink(this)" 
        onerror="handleLinkError(this)" 
        rel="stylesheet" 
        href="bundle.css" 
        type="text/css">
  <!-- this should throw error, file does not exist -->
  <link onload="handleLink(this)" 
        onerror="handleLinkError(this)" 
        rel="stylesheet" 
        href="bundles.css" 
        type="text/css">

  <body>
    <header><img src="" alt="App logo"></header>
    <!-- Boilerplate... -->
    <script id="bundle"></script>
  </body>

</html>

plnkr http://plnkr.co/edit/DQj9yTDcoQJj3h7rGp95?p=preview

like image 119
guest271314 Avatar answered Sep 18 '22 13:09

guest271314