Keeping JavaScript Dependencies At Bay

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:

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.

var myAwesomeApp = {
	components :{
		formcheck:{
			url:'formcheck.js',
			loaded:false
		},
		dynamicnav:{
			url:'dynamicnav.js',
			loaded:false
		},
		gallery:{
			url:'gallery.js',
			loaded:false
		},
		lightbox:{
			url:'lightbox.js',
			loaded:false
		}
	}
};

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.

var myAwesomeApp = {
	components :{
		formcheck:{
			url:'formcheck.js',
			loaded:false
		},
		dynamicnav:{
			url:'dynamicnav.js',
			loaded:false
		},
		gallery:{
			url:'gallery.js',
			loaded:false
		},
		lightbox:{
			url:'lightbox.js',
			loaded:false
		}
	},
	addComponent:function(component){
		var c = this.components[component];
		if(c && c.loaded === false){
			var s = document.createElement('script');
			s.setAttribute('type', 'text/javascript');
			s.setAttribute('src',c.url);
			document.getElementsByTagName('head')[0].appendChild(s);
		}
	}
};

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

if(!myAwesomeApp.components.gallery.loaded){
	myAwesomeApp.addComponent('gallery');
};

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:

var myAwesomeApp = {
	components :{
		formcheck:{
			url:'formcheck.js',
			loaded:false
		},
		dynamicnav:{
			url:'dynamicnav.js',
			loaded:false
		},
		gallery:{
			url:'gallery.js',
			loaded:false
		},
		lightbox:{
			url:'lightbox.js',
			loaded:false
		}
	},
	addComponent:function(component){
		var c = this.components[component];
		if(c && c.loaded === false){
			var s = document.createElement('script');
			s.setAttribute('type', 'text/javascript');
			s.setAttribute('src',c.url);
			document.getElementsByTagName('head')[0].appendChild(s);
		}
	},
	componentAvailable:function(component){
		this.components[component].loaded = true;
	}
}

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

myAwesomeApp.gallery = function(){
	// [... other code ...]
}();
myAwesomeApp.componentAvailable('gallery');

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:

var myAwesomeApp = {
	components :{
		formcheck:{
			url:'formcheck.js',
			loaded:false
		},
		dynamicnav:{
			url:'dynamicnav.js',
			loaded:false
		},
		gallery:{
			url:'gallery.js',
			loaded:false
		},
		lightbox:{
			url:'lightbox.js',
			loaded:false
		}
	},
	addComponent:function(component){
		var c = this.components[component];
		if(c && c.loaded === false){
			var s = document.createElement('script');
			s.setAttribute('type', 'text/javascript');
			s.setAttribute('src',c.url);
			document.getElementsByTagName('head')[0].appendChild(s);
		}
	},
	componentAvailable:function(component){
		this.components[component].loaded = true;
		if(this.listener){
			this.listener(component);
		};
	}
};

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

myAwesomeApp.listener = function(component){
	if(component === 'gallery'){
	   showGallery();
	}
};

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:

myAwesomeApp.listener = function(component){
	if(component === 'bespokecomponent'){
		myAwesomeApp.addComponent('bespokelabels');
	};
	if(component === 'bespokelabels'){
		myAwesomeApp.addComponent('bespokedata');
	};
	if(component === 'bespokedata'){
		myAwesomeApp,bespokecomponent.init();
	};
};
myAwesomeApp.components.bespokecomponent = {
	url:'bespoke.js',
	loaded:false
};
myAwesomeApp.components.bespokelabels = {
	url:'bespokelabels.js',
	loaded:false
};
myAwesomeApp.components.bespokedata = {
	url:'bespokedata.js',
	loaded:false
};
myAwesomeApp.addComponent('bespokecomponent');

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.

This article available in German at webkrauts.de

About the author

Christian Heilmann grew up in Germany and, after a year working 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 and moved on to be the Lead Developer Evangelist for the Yahoo Developer Network. In December 2010 he moved on to Mozilla as Principal Developer Evangelist for HTML5 and the Open Web. He publishes an almost daily blog at http://wait-till-i.com and runs an article repository at http://icant.co.uk. He also authored Beginning JavaScript with DOM Scripting and Ajax: From Novice to Professional.

More articles by Christian

Comments