31

NOTE: I have now created a jQuery plugin which is my attempt of a solution to this issue. I am sure that it could be improved and i've probably overlooked lots of use cases, so if anyone wants to give feedback feel free :-) https://github.com/WickyNilliams/ReadyBinder

I don't have a problem as such, but thought this would be an interesting point for discussion, and i hope people have some interesting ideas for this.

Basically, i work on a large-scale website and increasingly we are writing more and more JavaScript. This is fine, i enjoy JS' unique approach and i find the quirkiness in some of the darker annals of the language to be endearing ;-) However one thing that has always bugged me is how to manage document ready events as they get increasingly large over time (and as a result less focused/specific to the page being served)

The problem is that we have one JS file (merged & minified, though that's kind of inconsequential for my questions). Most of the JS is written using the revealing module pattern, and jQuery is our framework of choice. So all our JS funcitonality is logically grouped into methods, namespaced, and then right at bottom of the script file we have this

$(function(){
    //lots of code here, usually calling encapsulated methods 
    //on our namespaced revealing module
});

The problem is that not all of the code in this document ready handler pertains to every page. For instance, on one page only 10% of it might be relevant, on another perhaps 80% might be relevant. To me, this feels incredibly wrong, i feel i should only execute the code i need per page, mainly for efficiency, but also maintainability.

I've searched google for approaches to this issue, but cannot find anything, maybe i'm just searching for the wrong thing!

Anyway, so my questions are:

  • Has anybody ever thought about this issue?
  • Is it actually an issue in other people's opinion?
  • Do you have a large, all-encompassing document ready handler in your code or is it more focused for the type of page being served?
  • If the latter, how do you manage this? Multiple handlers which get switched in JS or dynamically spitting out the document ready handler server-side?

Look forward to people's thoughts on the matter.

Cheers

WickyNilliams
  • 5,218
  • 2
  • 31
  • 43
  • You should read this: http://www.nczonline.net/blog/2010/12/21/thoughts-on-script-loaders/ – Mohsen Sep 29 '11 at 20:41
  • that's not entirely relevant as I'm not concerned with script loading, but how to limit the amount of code that gets executed on document ready (particularly if all of your JS is merged into one file, though it would also be beneficial with separate files) – WickyNilliams Sep 30 '11 at 01:08
  • 1
    Frankly, you shouldn't even load code you aren't going to use. I would recommend checking out [require.js](http://requirejs.org/docs/jquery.html) or [head.js](http://headjs.com/). – namuol Oct 03 '11 at 20:06
  • pre-fetching is a perfectly valid approach. loading all (or at least the majority of all) code upfront saves future bandwidth use on subsequent requests. however, requireJS is solving a different issue - script *loading*. you'd still need a way of managing your document ready handlers. in fact requireJS would work with, rather than compete with what i'm suggesting. you could have the relevant document ready code (the problem i'm trying to solve) dynamically load scripts they require before utilising the scripts for whatever purpose (the problem requireJS is trying to solve) – WickyNilliams Oct 03 '11 at 22:01

9 Answers9

9

This is what i have done in my rails mvc project with heavy javascript, i have created a separate namespace for the controllers in js which resembles the rails controller

class BusinessController
   def new
   end  
   def index
   end
end

and

Var Business =  {
      init : function(action) {
         //code common to the Business module
         //even add the common jquery doc ready here, its clean
         //and call the page specific action
         this[action]();
      },
      new : function() {
             // check jquery document ready code here, thats relevant to this action
             //New rental page specific code here
      },
      index : function() {
             //  check jquery document ready code here, thats relevant to this action
             //index rental page specific code here 
      }
}

and on the view code(server side) just initiate the page specific js code by

<script type="text/javascript"> 
 <%= controller_name %>.init('<%= controller.action_name %>'); 
//which will render to
//  Business.init(index);
</script>

You can pretty much tweak this to make it work in any language. And this approach doesn't care whether you have a single file or multiple files.

RameshVel
  • 64,778
  • 30
  • 169
  • 213
  • Very neat solution! I like the use of plain ol' JS and how it marries up to your controller. this would lose some client side caching by spitting out the document ready stuff inline on the page, but it's negligible being only one line long! I'm not overly keen on spitting out JS server side, but it's a fair approach considering it's very generalised. I've almost completed a jQuery plugin to hook things up. I'll post it here when i'm done if you'd like to check it out - just need a snappy name now! Thanks for the food for thought :) – WickyNilliams Sep 30 '11 at 09:39
  • @mr.nicksta, i am glad you liked it... i have been following this approach for quite some time.. its clean and very easy to manage... – RameshVel Sep 30 '11 at 09:53
  • Good to hear it's been tried and tested too. Is there an error in your example? `<%= controller_name %>.init<%= controller.action_name %>` would output `Business.initindex`, right? Parentheses are missing :) – WickyNilliams Sep 30 '11 at 10:23
  • yep, it should have been like <%= controller_name %>.init('<%= controller.action_name %>'); – RameshVel Sep 30 '11 at 12:02
  • this was my favourite answer. In terms of cleanliness, maintainability and simplicity it did exactly what i asked. Even though i cannot actually use this approach because i'm stuck with a WebForms app at the moment, i will award you the bounty for the excellent, well thought-out answer. Enjoy your 100 points :-) – WickyNilliams Oct 10 '11 at 10:59
2

I think the must intuitive solution to this problem is, simply, to reduce the amount of work performed at load time.

If your code looks something like:

$(function () {
    // Binds a single click event (which will never be triggered if the hypothetical
    // widget is never used), regardless of how many widgets are used on the page.
    $(document).delegate('.rating-widget', 'click', namespace.rating.handler);
    // and so on for similar things which simply require event handler registration

    // Initializes each of the forms on the page, letting the initializer take care
    // of any details (datepicker widgets, validation, etc) based on the root element
    $('form.fancy').each(namespace.fancyform.initializer);
    // and so on for similar things that require initialization

    // ... (for each type of thing on the page requiring initial setup,
    //      find the most general/high level pattern that sufficient)
});

things should be relatively maintainable, even if there are a few dozen lines. There's no complicated/interesting logic at this level to update/remember when working with the component code and, because everything at this level is trivial, it's easy to read. At this point, there's no need to invent some complicated registration system; it's not going to be any simpler than .delegate and .each.

Note as well that all of the above gracefully handles the case where the component is not used. In those cases, little or no work is done and no conditional logic is necessary.

For interesting ideas about how you might implement the handlers/initializers, you might want to take a look at, for example, the "Contextual jQuery in practice" talk Doug Neiner gave at jQuery Boston last week.

zjs
  • 668
  • 4
  • 6
  • This is pretty much my current approach that I wish to get away from. It works fine if there are no such elements on the page, but on a large site your document ready handler gets big pretty quickly because all your logic is thrown in for every page. also `$('form.fancy').each(namespace.fancyform.initializer);`, despite gracefully handling the case where no such elements exist, is still wasting processing power (on what could be a seriously underpowered client) searching the DOM, even if it proves a fruitless search. Thanks for the link to the talk though, will watch that later :) – WickyNilliams Oct 05 '11 at 08:42
1

Great question. I actually handle this by using custom page load events. So in my core .js file I have a class like the following:

var Page = {
    init: function(pageName) {
        switch (pageName)
        {
            case: 'home': {
                // Run home page specific code
            }
            case: 'about': {
                // Run about page specific code
            }
            ...
        }
    }
}

You can call this a bunch of ways, either in a page-specific $(document).ready() or from the core script using some kind of URL parser (literally anything is possible with a URL parser):

$(document).ready(function() {
    // http://www.mydomain.com/about
    var currentPage = window.location.pathname;   // "about"
    Page.init(currentPage);    
});

window.location DOM reference: https://developer.mozilla.org/en/DOM/window.location

Terry
  • 14,099
  • 9
  • 56
  • 84
  • thanks for this, it's quite similar to how i thought to do it, except i was thinking of using CSS classes to identify the code to run, and you're using URL as the indicator. However, I have so many pages (thousands and thousands) it wouldn't be practical to categorize via URL. Much appreciated though, this is what i wanted to see, lots of ideas, a bit of discussion etc :-) – WickyNilliams Sep 30 '11 at 09:29
1

First I put specific classes on the body or on specific containers e.g. articles, form-validation, password-meter, … . If I have an MVC app, I prefer to put the controller name into a body class. This does not hurt a lot and can be useful for styling, too.

Then on each document.ready block I check for the existence of such a class, and if it does not exist I return from this function. This approach is simpler as it does not have the main code inside an if clause. This approach resembles to assertions that are used to check prerequisites of a function.

Example:

$(function(){
    if($('body').hasClass('articles') === false){ return; }

    //body of the articles code
});

$(function(){
    if($('body').hasClass('password-meter') === false){ return; }

    //include password meter in page
});

The bonus of this approach is that no unwanted code can sneak into a ready callback, which makes it easier to reuse. The downside is that you get many callback blocks and if you do not pay attention duplicate code can easily sneak into your app.

topek
  • 18,609
  • 3
  • 35
  • 43
  • did you check out my github at the bottom of the question? it would solve your situation in a neater manner :) you'd only need one document ready handler and you wouldn't need if statements! if you check it out let me know what you think. also, your above code could be simplified, the following if statement is equivalent to what you have above: `if(!$('body').hasClass('password-meter'))`. hope that's helpful – WickyNilliams Oct 03 '11 at 21:48
  • Overlooked this link and it would simplify my layout - so I have to come up with a better answer 8^). As to the simplification of the if statement: I like this way because an exclamation mark can be overlooked so easily and the triple comparison just burns into your eyes. – topek Oct 03 '11 at 21:58
  • good point, code readability should always be paramount. another inefficiency (which i know is a valid observation this time ;-)) with your current approach is also having to re-select the body all the time. feel free to try out my code and see how you find it, if you've got any suggestions etc. i may give the bounty to someone who is helpful in refining my approach! – WickyNilliams Oct 03 '11 at 22:33
0

I like the RameshVel approach. I also found the Paloma gem to create page-specific JavaScript. For instance:

Paloma.controller('Admin/Users', {
  new: function(){
    // Handle new admin user
  }
});
MegaTux
  • 1,591
  • 21
  • 26
0

I know where you're coming from. Right webapps for mobile use it can feel silly making the user download a massive JS file full of irrelevant code (the JQuery API for one is a bad start!).

What I do, which may be right or wrong but certainly seems to work is include required functions on a page by page basis before the tag. (This position for the most part, gets around the document.ready issue). Yet can't help but feel this approach is too simple.

I am interested in hearing other answers though so it's a good question, +1.

amcc
  • 2,608
  • 1
  • 20
  • 28
  • my problem with this would be that you don't get the benefit of client-side caching if you only include the script inline per-page. I want all my scripts downloaded up front but only the necessary code be executed on each page. i keep thinking about some kind of namespaced document ready handler where it switches the code executed depending on some flag (say a class added to the body tag). This seems workable but i can't get my head around how to implement with some JS magic. – WickyNilliams Sep 28 '11 at 12:34
0

"The problem is that we have one JS file"

I'm not sure how you can avoid it as long as you are including the same (merged) JS file for all pages. You could use some server-side logic to merge different pieces to go with different pages, but then you presumably lose the benefits of client-side caching of the JS, so...

Projects I've worked on have (typically) used multiple JS files, a couple being common to every page, and the others being included or not as appropriate - some JS files included by only one page. This means that pretty much every page includes the common library functions whether they use them or not, but the common JS file gets cached so it doesn't really matter. Page-specific code really is page-specific.

I can't tell from your question whether you are aware of it, but you can have multiple document ready handlers (I believe jQuery executes them in the order they were bound?). So if you do split the JS up each JS file can include its own document ready. Of course that approach has its own overhead, but I think it's better than putting everything in the same file.

nnnnnn
  • 147,572
  • 30
  • 200
  • 241
  • from a page load perspective it definitely makes sense to take the hit of downloading all of the JS up front and cache client-side, so i think a merged file is fine. But if you are doing lots of stuff in the document ready handler, it doesn't make sense to have lots of code *execute* (downloading is a one-time hit, but executing needless code is incurring a cost for every page load). I am aware of multiple ready handlers, but i don't think it really solves the issue and may incur it's own performance problems (presumably firing many events is more costly than firing one larger event) – WickyNilliams Sep 28 '11 at 12:32
  • Yeah, I only suggest multiple ready handlers in the scenario of multiple JS files. That way the browser is still caching the various JS files but any given page only loads the ones it needs and only executes the code it needs. I'm assuming firing many events that do only what you need is better than firing one larger event that (according to your question) may be 90% irrelevant. – nnnnnn Sep 28 '11 at 12:45
  • you make a valid point, but the main factor in page load is number of HTTP requests, and i wish to abide by this rule. I've started thinking of an approach where i can declaratively add classes to the body (server side). These would describe the type of page (to keep things semantic), using JS i could then look these up, and use them as keys to access specific pieces of functionality on some object. This extra logic and lookups will incur some cost, but should provide gains by focusing the amount of code executed per page. i'd need to benchmark. A rough prototype: http://jsfiddle.net/v5QZn/ – WickyNilliams Sep 28 '11 at 13:27
  • sorry small mistake in original fiddle, although it still worked. This one is syntactically correct: http://jsfiddle.net/v5QZn/1/ – WickyNilliams Sep 28 '11 at 13:42
  • Well it's an interesting question, hence the up-vote I gave you earlier. I took a quick look at your prototype and (though I'm sure you'll tweak it and pretty it up as you go) it's a much better way to meet your one-JS-file requirement than, say, a chain of if/else if. Don't forget that even if not executed it still has to be parsed. (As for me, I think I'll stick to the one-common-plus-several-page-specific-JS-files approach.) – nnnnnn Sep 28 '11 at 13:47
  • thanks for the up-vote BTW, expected a bit more debate from a few more people though. Do you happen to know if i can add a bounty at this point, to stir up some interest? As for your approach, that is probably how i would do it given my own freedom, but for my job page load times are of paramount importance so i'm stuck with one file. Could hybrid the approach and asynchronously load scripts into the page instead of looking up a function on an object? So now server-side code isn't managing which scripts go on which page (a front-end concern). I'll write this up as a jQuery plugin tonight :) – WickyNilliams Sep 28 '11 at 14:09
0

Firstly, if you're using jQuery, document.getElementsByTagName isn't really necessary, you can just use $("tagname"). Have a look at the jQuery Selectors documentation for even more magic!

Your work-in-progress solution is the way I would go about it, but you don't need to go to the trouble of defining extra functions, getting bodyClasses, running a loop, or any of that jazz... why not just use jQuery's .hasClass() function?

$(function() {
    if($("body").hasClass("article")) {
        alert("some article specific code");
    }
    if($("body").hasClass("landingPage")) {
        alert("some landing specific code");
    }
});

Bam. All you need. If you want to be even more efficient, you can reuse one body query:

$(function() {
    var body = $("body");
    if(body.hasClass("article")) {
        alert("some article specific code");
    }
    if(body.hasClass("landingPage")) {
        alert("some landing specific code");
    }
});

Here it is on jsfiddle: http://jsfiddle.net/W8nx8/6/

Mike Turley
  • 1,172
  • 1
  • 9
  • 26
  • i fully understand selectors, it was a conscious decision not to use them, don't worry about that :-) also, i wanted to loop through the classes so that i wouldn't have to add a new `if` statement whenever i wanted to attach new functionality on document ready. your approach is too aware of what classes may appear on the body element, checking for specific conditions. mine is a more general approach in that it doesn't care what classes are attached to the body, it's only concerned with whether there exists a matching method on my object literal. – WickyNilliams Sep 30 '11 at 01:05
  • Making "conscious decisions" to avoid jQuery isn't necessarily a good idea. You're denying yourself the advantages of all the optimizations (many of them platform-specific) that the jQuery team engineers into the library. You may be doing more harm than good. – Pointy Oct 09 '11 at 13:49
0

Having less javascript files minimizes the number of requests sent to the server and caching the files makes the second and third pages load faster. Usually, I think about the likelihood of some parts of a js file will be needed in the following pages (and not just the current page). This makes me categorize my js code into two types: code that goes in my "main.js", and even if not used, it is going to be downloaded by the client (but not necessarily executed, I will explain), and the second category of code goes in separate files. You could actually create multiple files grouped by probability. This depends on the details of your project. If it's a large scale project, running some statistical analysis on user behavior might reveal interesting ways of grouping js code. Now that the code has been downloaded to the client, we do not want to execute all of it. What you should do is have different layouts or pages. Place a partial in the layout or page. Inside the partial set a javascript variable. Retrieve the javascript variable in the javascript code and determine whether you want to execute some code or not.

i.e:

Create partial "_stats.html.erb", place this code in it:

<script type="text/javascript">
    var collect_statistics = <%= collect_statistics %>;
</script>

Place the partial in the pages you would like to collect statistics: show.html.erb

<%= render "_stats", :collect_statistics => false %>

Now in your js file, do the following:

$(document).load( function() {
 if (typeof collect_statistics != "undefined" && collect_statistics) {
  // place code here
 }
});

I hope this helps!

Abdo
  • 13,549
  • 10
  • 79
  • 98
  • thanks for the answer. whilst this approach does allow for code to be executed depending on certain circumstances, i'm trying to avoid having to write lots of if statements which your approach mandates – WickyNilliams Oct 03 '11 at 22:42
  • How about creating a map that does the wiring for you, and a code generator that does the js code for you. The reason I do it in js and not in an erb file that renders js is because I need to minify my files and serve them statically. The JS code generator solves this problem while making it easier to manage your code. – Abdo Oct 04 '11 at 06:39
  • I think it's cumbersome to auto-generate Javascript on the server-side as your back end suddenly needs to be aware of (and be capable of generating) javascript - this breaks separation of concerns. Did you see my github? I wrote a jQuery plugin that auto-wires up the code needed to be executed, similar to what you have done but it keeps the docuemtn ready even simpler. – WickyNilliams Oct 04 '11 at 10:34