link element onload

2019-01-08 16:38发布

问题:

Is there anyway to listen to the onload event for a <link> element?

F.ex:

var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';

link.onload = link.onreadystatechange = function(e) {
    console.log(e);
};

This works for <script> elements, but not <link>. Is there another way? I just need to know when the styles in the external stylesheet has applied to the DOM.

Update:

Would it be an idea to inject a hidden <iframe>, add the <link> to the head and listen for the window.onload event in the iframe? It should trigger when the css is loaded, but it might not guarantee that it's loaded in the top window...

回答1:

This is kind of a hack, but if you can edit the CSS, you could add a special style (with no visible effect) that you can listen for using the technique in this post: http://www.west-wind.com/weblog/posts/478985.aspx

You would need an element in the page that has a class or an id that the CSS will affect. When your code detects that its style has changed, the CSS has been loaded.

A hack, as I said :)



回答2:

Today, all modern browsers support the onload event on link tags. So I would guard hacks, such as creating an img element and setting the onerror:

if !('onload' in document.createElement('link')) {
  imgTag = document.createElement(img);
  imgTag.onerror = function() {};
  imgTag.src = ...;
} 

This should provide a workaround for FF-8 and earlier and old Safari & Chrome versions.

minor update:

As Michael pointed out, there are some browser exceptions for which we always want to apply the hack. In Coffeescript:

isSafari5: ->
  !!navigator.userAgent.match(' Safari/') &&
      !navigator.userAgent.match(' Chrom') &&
      !!navigator.userAgent.match(' Version/5.')

# Webkit: 535.23 and above supports onload on link tags.
isWebkitNoOnloadSupport: ->
  [supportedMajor, supportedMinor] = [535, 23]
  if (match = navigator.userAgent.match(/\ AppleWebKit\/(\d+)\.(\d+)/))
    match.shift()
    [major, minor] = [+match[0], +match[1]]
    major < supportedMajor || major == supportedMajor && minor < supportedMinor


回答3:

The way I did it on Chrome (not tested on other browsers) is to load the CSS using an Image object and catching its onerror event. The thing is that browser does not know is this resource an image or not, so it will try fetching it anyway. However, since it is not an actual image it will trigger onerror handlers.

var css = new Image();
css.onerror = function() {
    // method body
}
// Set the url of the CSS. In link case, link.href
// This will make the browser try to fetch the resource.
css.src = url_of_the_css;

Note that if the resource has already been fetched, this fetch request will hit the cache.



回答4:

E.g. Android browser doesn't support "onload" / "onreadystatechange" events for element: http://pieisgood.org/test/script-link-events/
But it returns:

"onload" in link === true

So, my solution is to detect Android browser from userAgent and then wait for some special css rule in your stylesheet (e.g., reset for "body" margins).
If it's not Android browser and it supports "onload" event- we will use it:

var userAgent = navigator.userAgent,
    iChromeBrowser = /CriOS|Chrome/.test(userAgent),
    isAndroidBrowser = /Mozilla\/5.0/.test(userAgent) && /Android/.test(userAgent) && /AppleWebKit/.test(userAgent) && !iChromeBrowser; 

addCssLink('PATH/NAME.css', function(){
    console.log('css is loaded');
});

function addCssLink(href, onload) {
    var css = document.createElement("link");
    css.setAttribute("rel", "stylesheet");
    css.setAttribute("type", "text/css");
    css.setAttribute("href", href);
    document.head.appendChild(css);
    if (onload) {
        if (isAndroidBrowser || !("onload" in css)) {
            waitForCss({
                success: onload
            });
        } else {
            css.onload = onload;
        }
    }
}

// We will check for css reset for "body" element- if success-> than css is loaded
function waitForCss(params) {
    var maxWaitTime = 1000,
        stepTime = 50,
        alreadyWaitedTime = 0;

    function nextStep() {
        var startTime = +new Date(),
            endTime;

        setTimeout(function () {
            endTime = +new Date();
            alreadyWaitedTime += (endTime - startTime);
            if (alreadyWaitedTime >= maxWaitTime) {
                params.fail && params.fail();
            } else {
                // check for style- if no- revoke timer
                if (window.getComputedStyle(document.body).marginTop === '0px') {
                    params.success();
                } else {
                    nextStep();
                }
            }
        }, stepTime);
    }

    nextStep();
}

Demo: http://codepen.io/malyw/pen/AuCtH



回答5:

Since you didn't like my hack :) I looked around for some other way and found one by brothercake.

Basically, what is suggested is to get the CSS using AJAX to make the browser cache it and then treat the link load as instantaneous, since the CSS is cached. This will probably not work every single time (since some browsers may have cache turned off, for example), but almost always.



回答6:

Another way to do this is to check how many style sheets are loaded. For instance:

With "css_filename" the url or filename of the css file, and "callback" a callback function when the css is loaded:

var style_sheets_count=document.styleSheets.length;
var css = document.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('type', 'text/css');
css.setAttribute('href', css_filename);
document.getElementsByTagName('head').item(0).appendChild(css);
include_javascript_wait_for_css(style_sheets_count, callback, new Date().getTime());

function include_javascript_wait_for_css(style_sheets_count, callback, starttime)
/* Wait some time for a style sheet to load.  If the time expires or we succeed
 *  in loading it, call a callback function.
 * Enter: style_sheet_count: the original number of style sheets in the
 *                           document.  If this changes, we think we finished
 *                           loading the style sheet.
 *        callback: a function to call when we finish loading.
 *        starttime: epoch when we started.  Used for a timeout. 12/7/11-DWM */
{  
   var timeout = 10000; // 10 seconds
   if (document.styleSheets.length!=style_sheets_count || (new Date().getTime())-starttime>timeout)
      callback();
   else
      window.setTimeout(function(){include_javascript_wait_for_css(style_sheets_count, callback, starttime)}, 50);
}


回答7:

This trick is borrowed from the xLazyLoader jQuery plugin:

var count = 0;

(function(){
  try {
    link.sheet.cssRules;
  } catch (e) {
    if(count++ < 100)
      cssTimeout = setTimeout(arguments.callee, 20);
    else
      console.log('load failed (FF)');
    return;
  };
  if(link.sheet.cssRules && link.sheet.cssRules.length == 0) // fail in chrome?
    console.log('load failed (Webkit)');
  else
    console.log('loaded');
})();

Tested and working locally in FF (3.6.3) and Chrome (linux - 6.0.408.1 dev)

Demo here (note that this won't work for cross-site css loading, as is done in the demo, under FF)



回答8:

You either need a specific element which style you know, or if you control the CSS file, you can insert a dummy element for this purpose. This code will exactly make your callback run when the css file's content is applied to the DOM.

// dummy element in the html
<div id="cssloaded"></div>

// dummy element in the css
#cssloaded { height:1px; }

// event handler function
function cssOnload(id, callback) {
  setTimeout(function listener(){
    var el = document.getElementById(id),
        comp = el.currentStyle || getComputedStyle(el, null);
    if ( comp.height === "1px" )
      callback();
    else
      setTimeout(listener, 50);
  }, 50)
}

// attach an onload handler
cssOnload("cssloaded", function(){ 
  alert("ok"); 
});

If you use this code in the bottom of the document, you can move the el and comp variables outside of the timer in order to get the element once. But if you want to attach the handler somewhere up in the document (like the head), you should leave the code as is.

Note: tested on FF 3+, IE 5.5+, Chrome



回答9:

The xLazyLoader plugin fails since the cssRules properties are hidden for stylesheets that belong to other domains (breaks the same origin policy). So what you have to do is compare the ownerNode and owningElements.

Here is a thorough explanation of what todo: http://yearofmoo.com/2011/03/cross-browser-stylesheet-preloading/



回答10:

// this work in IE 10, 11 and Safari/Chrome/Firefox/Edge
// if you want to use Promise in an non-es6 browser, add an ES6 poly-fill (or rewrite to use a callback)

let fetchStyle = function(url) {
  return new Promise((resolve, reject) => {
    let link = document.createElement('link');
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.onload = resolve;
    link.href = url;

    let headScript = document.querySelector('script');
    headScript.parentNode.insertBefore(link, headScript);
  });
};