24 ways

to impress your friends

Keeping JavaScript Dependencies At Bay by Christian Heilmann

This article available in German at webkrauts.de

As we are writing more and more complex JavaScript applications we run into issues that have hitherto (god I love that word) not been an issue. The first decision we have to make is what to do when planning our app: one big massive JS file or a lot of smaller, specialised files separated by task.

Personally, I tend to favour the latter, mainly because it allows you to work on components in parallel with other developers without lots of clashes in your version control. It also means that your application will be more lightweight as you only include components on demand.

Starting with a global object

This is why it is a good plan to start your app with one single object that also becomes the namespace for the whole application, say for example myAwesomeApp:

  1. var myAwesomeApp = {};

You can nest any necessary components into this one and also make sure that you check for dependencies like DOM support right up front.

Adding the components

The other thing to add to this main object is a components object, which defines all the components that are there and their file names.

  1. var myAwesomeApp = {
  2. components :{
  3. formcheck:{
  4. url:'formcheck.js',
  5. loaded:false
  6. },
  7. dynamicnav:{
  8. url:'dynamicnav.js',
  9. loaded:false
  10. },
  11. gallery:{
  12. url:'gallery.js',
  13. loaded:false
  14. },
  15. lightbox:{
  16. url:'lightbox.js',
  17. loaded:false
  18. }
  19. }
  20. };
  21. Source: /code/keeping-javascript-dependencies-at-bay/2.txt

Technically you can also omit the loaded properties, but it is cleaner this way. The next thing to add is an addComponent function that can load your components on demand by adding new SCRIPT elements to the head of the documents when they are needed.

  1. var myAwesomeApp = {
  2. components :{
  3. formcheck:{
  4. url:'formcheck.js',
  5. loaded:false
  6. },
  7. dynamicnav:{
  8. url:'dynamicnav.js',
  9. loaded:false
  10. },
  11. gallery:{
  12. url:'gallery.js',
  13. loaded:false
  14. },
  15. lightbox:{
  16. url:'lightbox.js',
  17. loaded:false
  18. }
  19. },
  20. addComponent:function(component){
  21. var c = this.components[component];
  22. if(c && c.loaded === false){
  23. var s = document.createElement('script');
  24. s.setAttribute('type', 'text/javascript');
  25. s.setAttribute('src',c.url);
  26. document.getElementsByTagName('head')[0].appendChild(s);
  27. }
  28. }
  29. };
  30. Source: /code/keeping-javascript-dependencies-at-bay/3.txt

This allows you to add new components on the fly when they are not defined:

  1. if(!myAwesomeApp.components.gallery.loaded){
  2. myAwesomeApp.addComponent('gallery');
  3. };
  4. Source: /code/keeping-javascript-dependencies-at-bay/4.txt

Verifying that components have been loaded

However, this is not safe as the file might not be available. To make the dynamic adding of components safer each of the components should have a callback at the end of them that notifies the main object that they indeed have been loaded:

  1. var myAwesomeApp = {
  2. components :{
  3. formcheck:{
  4. url:'formcheck.js',
  5. loaded:false
  6. },
  7. dynamicnav:{
  8. url:'dynamicnav.js',
  9. loaded:false
  10. },
  11. gallery:{
  12. url:'gallery.js',
  13. loaded:false
  14. },
  15. lightbox:{
  16. url:'lightbox.js',
  17. loaded:false
  18. }
  19. },
  20. addComponent:function(component){
  21. var c = this.components[component];
  22. if(c && c.loaded === false){
  23. var s = document.createElement('script');
  24. s.setAttribute('type', 'text/javascript');
  25. s.setAttribute('src',c.url);
  26. document.getElementsByTagName('head')[0].appendChild(s);
  27. }
  28. },
  29. componentAvailable:function(component){
  30. this.components[component].loaded = true;
  31. }
  32. }
  33. Source: /code/keeping-javascript-dependencies-at-bay/5.txt

For example the gallery.js file should call this notification as a last line:

  1. myAwesomeApp.gallery = function(){
  2. // [... other code ...]
  3. }();
  4.  
  5. myAwesomeApp.componentAvailable('gallery');
  6. Source: /code/keeping-javascript-dependencies-at-bay/6.txt

Telling the implementers when components are available

The last thing to add (actually as a courtesy measure for debugging and implementers) is to offer a listener function that gets notified when the component has been loaded:

  1. var myAwesomeApp = {
  2. components :{
  3. formcheck:{
  4. url:'formcheck.js',
  5. loaded:false
  6. },
  7. dynamicnav:{
  8. url:'dynamicnav.js',
  9. loaded:false
  10. },
  11. gallery:{
  12. url:'gallery.js',
  13. loaded:false
  14. },
  15. lightbox:{
  16. url:'lightbox.js',
  17. loaded:false
  18. }
  19. },
  20. addComponent:function(component){
  21. var c = this.components[component];
  22. if(c && c.loaded === false){
  23. var s = document.createElement('script');
  24. s.setAttribute('type', 'text/javascript');
  25. s.setAttribute('src',c.url);
  26. document.getElementsByTagName('head')[0].appendChild(s);
  27. }
  28. },
  29. componentAvailable:function(component){
  30. this.components[component].loaded = true;
  31. if(this.listener){
  32. this.listener(component);
  33. };
  34. }
  35. };
  36. Source: /code/keeping-javascript-dependencies-at-bay/7.txt

This allows you to write a main listener function that acts when certain components have been loaded, for example:

  1. myAwesomeApp.listener = function(component){
  2. if(component === 'gallery'){
  3. showGallery();
  4. }
  5. };
  6. Source: /code/keeping-javascript-dependencies-at-bay/8.txt

Extending with other components

As the main object is public, other developers can extend the components object with own components and use the listener function to load dependent components. Say you have a bespoke component with data and labels in extra files:

  1. myAwesomeApp.listener = function(component){
  2. if(component === 'bespokecomponent'){
  3. myAwesomeApp.addComponent('bespokelabels');
  4. };
  5. if(component === 'bespokelabels'){
  6. myAwesomeApp.addComponent('bespokedata');
  7. };
  8. if(component === 'bespokedata'){
  9. myAwesomeApp,bespokecomponent.init();
  10. };
  11. };
  12. myAwesomeApp.components.bespokecomponent = {
  13. url:'bespoke.js',
  14. loaded:false
  15. };
  16. myAwesomeApp.components.bespokelabels = {
  17. url:'bespokelabels.js',
  18. loaded:false
  19. };
  20. myAwesomeApp.components.bespokedata = {
  21. url:'bespokedata.js',
  22. loaded:false
  23. };
  24. myAwesomeApp.addComponent('bespokecomponent');
  25. Source: /code/keeping-javascript-dependencies-at-bay/9.txt

Following this practice you can write pretty complex apps and still have full control over what is available when. You can also extend this to allow for CSS files to be added on demand.

Influences

If you like this idea and wondered if someone already uses it, take a look at the Yahoo! User Interface library, and especially at the YAHOO_config option of the global YAHOO.js object.

About the author

Chris Heilmann Christian Heilmann grew up in Germany and, after a year working with people with disabilities for the red cross, spent a year as a radio producer. From 1997 onwards he worked for several agencies in Munich as a web developer. In 2000 he moved to the States to work for Etoys and, after the .com crash, he moved to the UK where he lead the web development department at Agilisys. In April 2006 he joined Yahoo! UK as a web developer. He publishes an almost daily blog at http://wait-till-i.com and runs an article repository at http://icant.co.uk. He is a member of the Web Standards Project's DOM Scripting Task Force and the author of Beginning JavaScript with DOM Scripting and Ajax: From Novice to Professional.

Responses

Got something to add? Post to your own site and tag it 24ways07 and christianheilmann to be included here.

Your comments

  1. § Ryan van der Wal:

    Good article. I would like to add for people who want to use this to also take a look at: unobtrusive javascript which will help you to write good code for the dynamically included javascript that will ‘play nice’ with other js code on the page.

    It’s written by someone named: Christian Heilmann. The name sound kinda familiar, don’t know why though :o).

  2. § Wade Harrell:

    on the subject of lazy loading js, you might find this interesting: <a href=“http://tech.groups.yahoo.com/group/ydn-javascript/message/10686” target=”_blank”>Using yui_2.2.0a with JSAN</a> or this <a href=“http://tech.groups.yahoo.com/group/ydn-javascript/message/15052” target=”_blank”>Crockford thoughts on JavaScript package systems?</a>

  3. § Danilo Celic:

    Christian,

    You say that this technique can be applied to CSS files as well, but I’m curious about how you would know if the CSS file actually loaded? That is, how would the myAwesomeApp.componentAvailable() get called for CSS file(s)?

  4. § Jakob Kruse:

    This is a nice solution, but it still has disadvantages. For one, components loaded through addComponent will not be generally available until the current script block has exited and the next has begun. What this means is that if we want to write some component ‘A’ which depends on some other component ‘B’, then all of ‘A’ must be placed inside a listener function that checks for the availability of ‘B’ (and what’s with this implementation that doesn’t allow more than one single listener registered for the entire component hierarchy? Something like the ObserverPattern should have been used).

    Worse, if ‘A’ depends on both ‘B’, ‘C’ and ‘D’, then the listener function must painfully remember which of these components are ready, and only execute ‘A’ when all of them have been loaded.

    Result: very nasty and voluminous dependency management code.

    Another problem would be when some of these components are not specific to myAwesomeApp, but of a more general nature. Then it just doesn’t work at all.

    Personally I prefer a more general approach. Traces of it can be seen in the ObserverPattern.js linked above. It contains a line reading “Script.require(‘Prototype’);”. Obviously, what this means, is that this “package” (or component if you will) does not work without first loading something referenced as “Prototype”. “Script” (a generic dependency management package) resolves this dependency by looking up the “Prototype” key in an application specific alias table (because the same components are available from different URI’s in different applications), and then immediately makes this dependency available (an option is included for loading in the same manner as above, but rarely used). The result is one line of code per dependency – that doesn’t change even when components are moved around.

    Full source code is available.

  5. § Einar:

    I am surprised that you recommend many smaller js files as opposed to one larger. This goes against the principles of YSlow, which I thought voiced the opinion of all YAHOO front-end engineers. I guess you don’t all agree, nothing wrong with that….
    Personally I prefer one larger file, minified, gzipped, and cached, this will be a better experience for the user.

Commenting is closed for this article.

24 ways: day 18