Request a topic or
contact an Arke consultant
404-812-3123
jQuery

Arke Systems Blog

Useful technical and business information straight from Arke.

About the author

Author Name is someone.
E-mail me Send mail

Recent comments

Archive

Authors

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2024

Gotchas when loading jquery from a CDN

A client wants to take advantage of browser cache and load jquery from a CDN.

There is always a lot of discussion about whether one should load jquery from a CDN.  Pros include:

  • Some users will have the file cached, saving you 20k of bandwidth and them a bit of load time
  • It’s a very cheap (free) way to serve files from geographically distributed close-to-your-user servers
  • Browsers only simultaneously load a few files per domain name, so using an extra domain name can lead to more parallel file loads
  • You can count on Google to get gzip and expires headers right (provided you request the right specific file)

Drawbacks include:

  • There may be an extra DNS resolution to load from a new domain name, and DNS resolution takes time
  • Google might be down or blocked (particularly on intranet sites)
  • You’re serializing loads of your js anyway, so will you see gains from other parallel loads?

Particularly because some of our users are in controlled office environments where they don’t necessarily have access to the entire web, the concern about ajax.googleapis.com being blocked is a real concern.  So, the script should fallback to load jquery locally if it exists.

There are plenty of blog and forum discussions about this, so this is yet another, but I thought I’d like to focus on some of the gotchas that can be encountered with even such an apparently straightforward task.

This is the most obvious potential solution:

<head>
...
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>  
<script type="text/javascript">   
if (typeof jQuery === 'undefined') {   
   var e = document.createElement('script');   
   e.src = '/js/jquery-1.4.2.min.js';   
   e.type='text/javascript';   
   document.getElementsByTagName("head")[0].appendChild(e);   
}   
</script>  
    <script type="text/javascript" src="/script-that-needs-jquery-loaded.js"></script>
...
</head>

However, don’t use that ‘solution’! 

Do this instead, even though document.write is on the ‘generally avoid this’ list of things not to do:

<head>
...
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
if (typeof jQuery === 'undefined')
{
    document.write(unescape("%3Cscript src='/js/jquery-1.4.2.min.js' type='text/javascript'%3E%3C/script%3E"));
}
</script>
    <script type="text/javascript" src="/script-that-needs-jquery-loaded.js"></script>
...
</head>

 

The first ‘obvious’ solution has a few non-obvious problems:

  1. The first problem is a race condition.  When a browser encounters a script tag in a document, it pauses while the script loads.  This is so that you can chain together a bunch of scripts that rely on each other – the jquery library makes some functions available, the next library you load can use those functions.  It’s a pretty essential feature, and we routinely rely on it all the time without even thinking about it.

    However, look at what our failover case is doing – if jquery doesn’t load, it tacks it in as the last element in the document head in the DOM.

    Elements loaded via adding dynamically to the DOM *don’t* block like regular script elements, they load asynchronously. 
    This means that script-that-needs-jquery-loaded probably won’t have jquery loaded in time.  But maybe it will, due to caching.  We now have a heisenbug – if google is accessible, everything works great.  If google is not accessible, jquery still gets loaded, but without blocking, so now sometimes things later in the page that need jquery will work but other times it won’t have loaded in time.
    The easiest fix is to switch to document.write to include a script tag in the page.  A script tag written with document.write will run and block once your current script block completes.

  2. Next up, we have a caching problem.  When you request http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js , look at this little header:

    Cache-Control: public, must-revalidate, proxy-revalidate, max-age=3600

    The odds of getting a useful document from a visitor’s cache with a max-age of one hour are not that great.
    However, when you request http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js , the cache time is now:

    Cache-Control: public, max-age=31536000

    Google serves up 1.4 with a 1 hour cache expires time, because a new version might be released and they don’t want the old version stuck in your cache forever.  The solution is to instead request a specific version, which is served up with a year long expires time.

  3. Now, on our document.write change: There’s a gotcha here too. You’re technically not allowed to have the string </whatever> in your javascript, even if it’s inside a string.  Some browsers may run it wrong.  Some AV/firewall software will incorrectly dynamically rewrite this.  So

    document.write("<script src='/codescripts/js/jquery-1-3-2-min.js' type='text/javascript'></script>"));

    isn’t legit.  Two solutions here – either split up the /script into two strings added together, or else encode the brackets and use the unescape function to decode.  I think the unescape is more clear:

    document.write(unescape("%3Cscript src='/codescripts/js/jquery-1-3-2-min.js' type='text/javascript'%3E%3C/script%3E"));

  4. Final problem: If using jquery 1.3: Firefox 3.5 doesn’t support document.readyState, so if you put this randomly somewhere in the body of your page and it runs after the page is ready, your onready events may not fire.  There are fixes for this behavior in jquery 1.4 and in Firefox 3.6, but in general to be safest, include your jquery in the document head so it runs before onready fires.

No matter what, test!  On that first script load, temporarily set an invalid filename, temporarily set an invalid domain name, and make sure the fallback works like you’d expect!


Posted by David Eison on Thursday, August 12, 2010 5:38 PM
Permalink | Comments (0) | Post RSSRSS comment feed

jQuery Validation. My first use and modification

I meant to update this post earlier but forgot.  The same can be accomplished using the built-in valid: option with CSS classes.

I’m relatively new to jQuery.  But I needed a client-side validation framework.  Enter jQuery.validate.  Great little framework and easy to implement.  But I needed one more thing.  When a field WAS valid, I wanted it to display a success message.  Below is the (ugly, hardcoded) additions I made.  It gives me a nice little green checkmark when the field’s all nice and valid.

 

successes: function() {

      return $(this.settings.errorElement + "." + this.settings.successClass, this.errorContext);

},

 

successesFor: function(element) {

      return this.successes().filter("[for='" + this.idOrName(element) + "']");

},

 

showSuccess: function(me) {

      for (var i = 0; this.successList[i]; i++) {

            var success = this.successList[i];

            if (success) {

                  var message = "<img src=\"../../Content/Images/greencheckmark.png\" class=\"checkmark\" />"

                  if (this.settings.successClass) { } else { this.settings.successClass = "field-validation-success"; }

                  var label = this.successesFor(success);

                  if (label.length) {

                        // refresh error/success class

                        label.removeClass().addClass(this.settings.successClass);

 

                        // check if we have a generated label, replace the message then

                        label.attr("generated") && label.html(message);

                  } else {

                        // create label

                        label = $("<" + this.settings.errorElement + "/>")

                        .attr({ "for": this.idOrName(success), generated: true })

                        .addClass(this.settings.successClass)

                        .html(message || "");

                        if (this.settings.wrapper) {

                              // make sure the element is visible, even in IE

                              // actually showing the wrapped element is handled elsewhere

                              label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent();

                        }

                        if (!this.labelContainer.append(label).length)

                              this.settings.errorPlacement

                  ? this.settings.errorPlacement(label, $(success))

                  : label.insertAfter(success);

                  }

 

                  if (this.settings.success) {

                        label.text("");

                        typeof this.settings.success == "string"

            ? label.addClass(this.settings.success)

            : this.settings.success(label);

                  }

            }

      }

      if (this.successList.length) {

            this.toShow = this.toShow.add(this.containers);

      }

      if (this.settings.success) {

            for (var i = 0; this.successList[i]; i++) {

                  this.showLabel(this.successList[i]);

            }

      }

      if (this.settings.unhighlight) {

            for (var i = 0, elements = this.invalidElements(); elements[i]; i++) {

                  var label = this.successesFor(elements[i]);

                  if (label.length) {

                        label.attr("generated") && label.html("");

                  }

            }

      }

      this.toHide = this.toHide.not(this.toShow);

      this.hideErrors();

      this.addWrapper(this.toShow).show();

},

 

 

 

And modified the following function:

// http://docs.jquery.com/Plugins/Validation/Validator/element

                  element: function(element) {

                        element = this.clean(element);

                        this.lastElement = element;

                        this.prepareElement(element);

                        this.currentElements = $(element);

                        var result = this.check(element);

                        if (result) {

                              delete this.invalid[element.name];

                        } else {

                              this.invalid[element.name] = true;

                        }

                        if (!this.numberOfInvalids()) {

                              // Hide error containers on last error

                              this.toHide = this.toHide.add(this.containers);

                        }

                        this.showErrors();

                   this.showSuccess();

                        return result;

                  },


Posted by Trenton Adams on Tuesday, June 16, 2009 2:16 PM
Permalink | Comments (0) | Post RSSRSS comment feed