As promised earlier, i’ll discuss in this post how we load the Javascript modules we have written over the last months. Please note that despite i am mainly talking about portlets in this post, the technique described is also usable in web applications that need to load additional Javascript after page load.
The setting
Let me explain the structure of our project a bit. The main constraint we are facing when developing rich user interfaces for the web platform is that we were up to do so using Java Portlets. Using the portlets standard (JSR-168), we are able to meet our customer’s needs in a way we never could have done when developing traditional, monolithic web applications. But this flexibility comes at a price: we do not know which apps are on a page when it gets loaded; further, we want to be able to add new portlets to each page at any time. From a Javascript perspective, this means that traditional “just put script tags into the header”-style Javascript coding will lead to a very bad user experience, since every page would be bloated with stuff the user will never use and – even more important- the user experience will degrade as we are adding new features to our product, even if our customer does not need or want the new features. As explained before, adding Javascript code to HTML/JSP Parts of the Portlets were no option and would have caused massive double-loading of script code. As our platform grew, we saw the need to put together a Javascript library to support code reuse up front. We clearly needed a mechanism to load our Javascript modules as they are needed. After some experiments (including the use of unholy synchronous XHR calls… shame on me!), we tried the YUI Loader.
Meet the YUI Loader Utility
Its no secret that we are using YUI in our product. As YUI comes with a special module for loading dependencies, the YUI Loader Utility, we explored if we could use YUI for our needs. The Loader’s feature list reads like our wish-list:
- Reliable, sorted loading of dependencies
- Safe, efficient mechanism for adding new components to a page on which YUI may already be present.
- Automatic use of rolled-up files.
So we’ve developed our own solution on top of the YUI loader for our Framework. It consists of two components: A Compile-time dependency analyzer and a configuration file generated by the dependency analyzer that tells YUI how to load our modules.
Analyzing JS dependencies
First of all, every JS module file starts with a set of comments telling the dependency analyzer how the current module is called and what modules are required by the declared module:
// @@module agimatec.topic
// @@requires event, agimatec.datatypes
// Module code goes here
// ...
// ...
// ...
// ...
// ...
// Tell the loader that this module has been loaded
YAHOO.register("agimatec.topic", agimatec.topic, {version:'1.0',build:'000'});
The analyzer (which is current implemented as stand-alone groovy script that is run by maven – we have plans to implement it as maven Mojo) parses the dependency information in the module files and generates a Javascript configuration file for the YUI loader from it:
// ******************************
// Generated Code - DO NOT EDIT
// ******************************
/*
This script adds all AJF module definitions to the YUI Loader utility as noted by
'// @@' - style comments inside the *.js files.
If the global variable 'agimatec_config' is not defined, it is defined by this script.
*/
(function(){
var arrModules =[
//...
{
name:'agimatec.scaffolding',
type:'js',
skinnable:false,
path: '../../agimatec/scaffolding-2.1.11-SNAPSHOT-min.js',
requires:["yahoo","event","calendar","tabview","agimatec.topic","agimatec.util","agimatec.util.table","agimatec.util.element","agimatec.util.minibrowser","agimatec.util.datePicker","agimatec.util.hintedText","agimatec.validator","agimatec.metadata"]},
{
name:'agimatec.topic',
type:'js',
skinnable:false,
path: '../../agimatec/topic-2.1.11-SNAPSHOT-min.js',
requires:["event","agimatec.datatypes"]},
{
name:'agimatec.util.browser',
type:'js',
skinnable:false,
path: '../../agimatec/util/browser-2.1.11-SNAPSHOT-min.js',
requires:["yahoo","dom","event","animation","datatable","agimatec.topic","agimatec.util","agimatec.util.yui","agimatec.util.lucene","agimatec.util.element","agimatec.preferences"]},
{
name:'agimatec.util.connector',
type:'js',
skinnable:false,
path: '../../agimatec/util/connector-2.1.11-SNAPSHOT-min.js',
requires:["yahoo","dom","event","container","datatable","agimatec.util","agimatec.metadata","agimatec.util.table","agimatec.portletservice","agimatec.util.minibrowser"]},
{
name:'agimatec.util.context',
type:'js',
skinnable:false,
path: '../../agimatec/util/context-2.1.11-SNAPSHOT-min.js',
requires:["agimatec.metadata","agimatec.util"]},
{
name:'agimatec.util.datePicker',
type:'js',
skinnable:false,
path: '../../agimatec/util/datePicker-2.1.11-SNAPSHOT-min.js',
requires:["yahoo","dom","event","agimatec.util","agimatec.util.hintedText"]},
// ...
];
agimatec_config = agimatec_config || {};
agimatec_config._modules = arrModules;
agimatec_config.getLoader = function(){
var modulesLength=agimatec_config._modules.length, i;
var configObject = {};
configObject.base = agimatec_config.yuiBasePath;
if(agimatec_config.moduleFilter){
configObject.filter=agimatec_config.moduleFilter;
}
if(undefined !== agimatec_config.allowRollup){
configObject.allowRollup = agimatec_config.allowRollup;
}
// configure the default skin
configObject.skin = {
'defaultSkin':agimatec_config.defaultSkinName,
'base': 'assets/skins/',
'path': 'skin.css',
'rollup': 3
};
var loader = new YAHOO.util.YUILoader(configObject);
for(i=0; i
This file is included inside the header of every portlet page. You see that thanks to the YUI Loader, we can mix YUI modules with our own ones without hassle, making development of a comprehensive Javascript Library much easier. (If your wondering why we add version numbers to the referenced module names, see this article)
Loading a module is as easy as:
// ...
var loader = agimatec_config.getLoader()
loader.require("agimatec.topic");
loader.insert(function(){
// use agimatec.topic and ALL of its dependencies here !!!!
});
You see that using modules is very easy. All you have to do is to tell the loader what you need and provide a function that should get executed as soon as the dependencies are resolved by adding the noted Javascript files.
This solution served us very well in the last months, in fact our complete webapp development depends on this loading mechanism. It made growing a stable and feature-rich javascript library possible for us without destroying the user experience.
This post just scratches the features of our Javascript module tooling. We have also added support for a module description file format that allows us to describe Javascript modules using XML and generate the loading and dependency management code, along with support for “rollup” JS modules that allow us to reduce the number of requests needed to load portal pages.
If you are interested in this component, just tell us so in the comments and we will see how fast we can get this utility out of the door as Open Source.
This is part two of an article series about Ajax Portlet development. In my next post, i will tell you about how we handle inter-portlet communication in our framework and how our approach makes our application extremely flexible without hardcoding dependencies between portlets.