The code I have produced for my Zazl JavaScript Optimizer works by generating URL's for HTML script tags. Because of this the developer must use some form of server-side support to generate the HTML resource so the script tags can be inserted. As the code is written in Java the obvious choice for the server-side technology is JSP's. If you are comfortable with writing JSP's and perhaps also plan to use them to insert other dynamic content into the returning HTML resource then they are a good solution, but if you are only interested in using the Optimizer then writing HTML is a simpler choice to pick.
I decided to write an HTML Filter to make adoption of the Optimizer easier. It can be used to insert the required javascript script tag into the HTML resource before it is returned to the requester. All that the developer is required to do is add the javascript that references the Optimizer's AMD loader entry point. This can be via an embedded script within the HTML or via a "main" javascript resource referenced by the HTML via a script tag with a "src" attribute.
Writing the HTML Filter was made fairly straight forward because of great third party open source libraries that are available, NekoHTML and UglifyJS. The HTML Filter itself is written as a JEE Filter. Filters allows the HTTP requests and responses to be modified before and after the HTTP servlet serving the HTML is executed. In this particular case the HTTP response is obtained by the Filter and analyzed before being returned back to the requester.
NekoHTML is an HTML parser written in Java. The Zazl Optimizer HTML Filter uses it to parse the HTML response returned from the WebContainer. The parser allows the filter to find and scan embedded javascript within the HTML. It also allows the filter to identify "main" javascript resources attached to the HTML that might contain the Optimizers AMD loader entry point.
Once these javascript snippets have been obtained the filter uses the javascript parser provided by UglifyJS to parse the code and locate the Optimizer AMD loader entry point. The entry point should provide the ids of the AMD modules that can be considered the top level modules for the page. The Optimizer's analyzer is then used to analyze and generate a javascript tag URL that can be inserted into the HTML response.
In a nutshell that's about all there is to it. I should also mention that the Filter attempts to ensure the HTML resource does not get cached because if any javascript resources required for the page are modified a new javascript tag URL would have to be generated. If the HTML resource is cached the javascript changes would never be picked up by the requester. It attempts to do this by stripping out any request headers that the WebContainer might use to indicate caching is possible.
You can see this in action in the Zazl AMD Optimizer sample WAR, also a wiki page is found here that provides some more details. Alternatively, I have a Music Server Application found here that also demonstrates the Filter and Optimizer in action.
Sunday, January 15, 2012
Saturday, January 14, 2012
Optimizing with AMD JavaScript loaders - Part 2
This is the second part, an update might be a better description, for my earlier blog post Optimizing with AMD JavaScript Loaders. Since then Dojo 1.7 is now in GA and I have updated the Zazl Dynamic Optimizer to support optimizing Dojo 1.7 based applications.
I had originally wanted to have the optimizer work generically with AMD compliant loaders and also be able to optimize plugin references using the referenced plugins themselves, however it became obvious that this is not a reasonable goal to achieve with the state that AMD spec is currently in. The result of this is that the optimizer provides its own AMD loader (similar to Almond in that it expects all the modules to be part of the javascript stream) and also supports its own server-side plugin API that allows for custom optimizations to be applied where possible.
The Zazl Optimizer's AMD Loader
As the optimizer ensures all the required AMD modules are part of the javascript stream delivered to the client there is no need for the loader to fully support asynchronous loading. I also found that the loader had to support modules that contained references to the local "require" function. The loader will ensure that modules referenced from "define" calls will be present in the initial javascript stream. If modules contain calls to the local "require" the loader will make additional XHR calls back to the optimizer to load the "required" module along with its dependencies, again in a single response.
Another value-add feature that the loader provides is the ability to preload a cache that is made available to modules via a "cache" property on the require object. The cache property is not part of the AMD spec but the Dojo AMD loader provides this support. This enables optimizations to be made for plugins such as the dojo/text plugin. Using a server-side extension the cache is preloaded with the text value that the plugin will provide, thus avoid and additional XHR call to load the resource.
Configuring the zazl loader
The entry point into Optimizers AMD loader is called "zazl". If follows a similar pattern to how my lsjs AMD loader had defined its entry point. For example :
zazl({
packages: [
{
name: 'dojo',
location: 'dojo',
main:'main'
},
{
name: 'dijit',
location: 'dijit',
main:'main'
},
{
name: 'dojox',
location: 'dojox',
main:'main'
}
]
},
["amdtest/Calendar"],
function(calendar) {
console.log("done");
});
Additionally, an zazl.json file has to be provided on the server-side.
Here is an example of the zazl.json file :
{
"bootstrapModules" : ["loader/amd/zazl.js"],
"debugBootstrapModules" : ["loader/amd/zazl.js"],
"amdconfig" : {
"plugins" : {
"dojo/has" : {
"proxy": "optimizer/amd/plugins/dojo/has",
"has" : {
"host-browser" : 1,
"dom" : 1,
"dojo-dom-ready-api" : 1,
"dojo-sniff" : 1
}
},
"dojo/text" : {
"proxy": "optimizer/amd/plugins/dojo/text"
},
"dojo/selector/_loader" : {
"proxy": "optimizer/amd/plugins/dojo/selector/_loader",
"defval": "dojo/selector/lite"
},
"dojox/gfx/renderer" : {
"proxy": "optimizer/amd/plugins/dojox/gfx/renderer",
"defval": "dojox/gfx/svg"
}
},
"i18nPluginId" : "dojo/i18n"
},
"type" : "amd"
}
Optimizing plugin references
Having given up on loading and running the plugins themselves on the server-side I faced the reality that to support frameworks like Dojo I would have to provide server-side equivalents for some of the plugins the Dojo provides. As the server-side was already providing a commonjs loader environment I decided to write these plugin proxies just as commonjs modules.
A server-side plugin proxy is configured via the zazl.json configuration file.
For example :
1) write(pluginName, normalizedName, callback, moduleUrl) - the return value from the callback is written into the javascript stream.
2) normalize(id, config, expand) - the returned value is used to determine if the plugin has additional dependencies that need to be included in the javascript response stream.
The "config" param for both calls is value specified for the "amdconfig" property in zazl.json.
dojo/has
The dojo/has plugin is used to determine whether to dynamically include other modules based on a "has" configuration. This provides a challenge for how to deal with this in an optimized environment. I chose to provide my own server-side version of the has plugin that used its own configuration. It provides a "normalize" function that is used to direct the optimizer to include these additional dependencies. The "has" configuration is provided in the zazl.json configuration file.
dojo/text
The dojo/text plugin is used to load in text resource dependencies. Ideally, in a optimized environment you would want the optimizer to ensure these text resources are included in the javascript response stream in a form that avoids additional downloads. The dojo/text plugin makes use of a "require.cache" property to populate a cache. The zazl AMD loader supports populating the cache by providing an "addToCache" function. The server-side version of the dojo/text plugin writes calls to this function that are included into the javascript response stream.
This ensures that when the client-side version it will find the cache value and avoid an XHR call to obtain the resource.
dojo/selector/_loader
The dojo/selector/_loader plugin determines what selector engine to use. The client side version expects a true browser environment to be available to determine the required engine type. This not something that can be determined easily in an optimized environment so for now the server side version I have written provides a "normalize" method that simply returns the default "lite" module id.
exports.normalize = function(id, config, expand) {
return "dojo/selector/lite";
};
The optimizer ensures that this module, along with its dependencies, is included in the javascript response stream. I plan to make the value returned configurable.
dojox/gfx/renderer
The dojox/gfx/renderer plugin works in a similar fashion to the dojo/selector/_loader plugin. It currently returns a default value of "dojox/gfx/svg" via a provided "normalize" function.
i18n plugin support
Optimizing i18n plugin support is more complicated that other types of plugins due to the need to provide i18n message bundles based on the locale of the calling client. The other optimized plugins produce output that can be cached and included for ever request for the same AMD module and its dependencies. The i18n output must be generated for each request made.
Because of this I have made the i18n plugin support a special case and there is specialized code that produces the message bundles. The "dojo/i18n" plugin supports the same format of messages as the requirejs i18n plugin does. The zazl configuration files specifies a property that indicates the module id of the i18n plugin. When the optimizer encounters an i18n plugin reference the details are used by the javascript response renderer to include the specified message bundles based on the locale of calling client. When rendered into the client the client i18n plugin finds that the messages bundles have been loaded in and avoid XHR requests to load them.
What's next ?
Update:
I have improved the configuration such that it is not duplicated in zazl.json. I have also written a blog post about the HTML filtering approach to inserting the required script tags
I had originally wanted to have the optimizer work generically with AMD compliant loaders and also be able to optimize plugin references using the referenced plugins themselves, however it became obvious that this is not a reasonable goal to achieve with the state that AMD spec is currently in. The result of this is that the optimizer provides its own AMD loader (similar to Almond in that it expects all the modules to be part of the javascript stream) and also supports its own server-side plugin API that allows for custom optimizations to be applied where possible.
The Zazl Optimizer's AMD Loader
As the optimizer ensures all the required AMD modules are part of the javascript stream delivered to the client there is no need for the loader to fully support asynchronous loading. I also found that the loader had to support modules that contained references to the local "require" function. The loader will ensure that modules referenced from "define" calls will be present in the initial javascript stream. If modules contain calls to the local "require" the loader will make additional XHR calls back to the optimizer to load the "required" module along with its dependencies, again in a single response.
Another value-add feature that the loader provides is the ability to preload a cache that is made available to modules via a "cache" property on the require object. The cache property is not part of the AMD spec but the Dojo AMD loader provides this support. This enables optimizations to be made for plugins such as the dojo/text plugin. Using a server-side extension the cache is preloaded with the text value that the plugin will provide, thus avoid and additional XHR call to load the resource.
Configuring the zazl loader
The entry point into Optimizers AMD loader is called "zazl". If follows a similar pattern to how my lsjs AMD loader had defined its entry point. For example :
zazl({
packages: [
{
name: 'dojo',
location: 'dojo',
main:'main'
},
{
name: 'dijit',
location: 'dijit',
main:'main'
},
{
name: 'dojox',
location: 'dojox',
main:'main'
}
]
},
["amdtest/Calendar"],
function(calendar) {
console.log("done");
});
Additionally, an zazl.json file has to be provided on the server-side.
Here is an example of the zazl.json file :
{
"bootstrapModules" : ["loader/amd/zazl.js"],
"debugBootstrapModules" : ["loader/amd/zazl.js"],
"amdconfig" : {
"plugins" : {
"dojo/has" : {
"proxy": "optimizer/amd/plugins/dojo/has",
"has" : {
"host-browser" : 1,
"dom" : 1,
"dojo-dom-ready-api" : 1,
"dojo-sniff" : 1
}
},
"dojo/text" : {
"proxy": "optimizer/amd/plugins/dojo/text"
},
"dojo/selector/_loader" : {
"proxy": "optimizer/amd/plugins/dojo/selector/_loader",
"defval": "dojo/selector/lite"
},
"dojox/gfx/renderer" : {
"proxy": "optimizer/amd/plugins/dojox/gfx/renderer",
"defval": "dojox/gfx/svg"
}
},
"i18nPluginId" : "dojo/i18n"
},
"type" : "amd"
}
Optimizing plugin references
Having given up on loading and running the plugins themselves on the server-side I faced the reality that to support frameworks like Dojo I would have to provide server-side equivalents for some of the plugins the Dojo provides. As the server-side was already providing a commonjs loader environment I decided to write these plugin proxies just as commonjs modules.
A server-side plugin proxy is configured via the zazl.json configuration file.
For example :
"plugins" : {
"dojo/has" : "optimizer/amd/plugins/dojo/has",
"dojo/text" : "optimizer/amd/plugins/dojo/text",
"dojo/selector/_loader" : "optimizer/amd/plugins/dojo/selector/_loader",
"dojox/gfx/renderer" : "optimizer/amd/plugins/dojox/gfx/renderer"
}
A plugin proxy can provide two exports :
1) write(pluginName, normalizedName, callback, moduleUrl) - the return value from the callback is written into the javascript stream.
2) normalize(id, config, expand) - the returned value is used to determine if the plugin has additional dependencies that need to be included in the javascript response stream.
The "config" param for both calls is value specified for the "amdconfig" property in zazl.json.
dojo/has
The dojo/has plugin is used to determine whether to dynamically include other modules based on a "has" configuration. This provides a challenge for how to deal with this in an optimized environment. I chose to provide my own server-side version of the has plugin that used its own configuration. It provides a "normalize" function that is used to direct the optimizer to include these additional dependencies. The "has" configuration is provided in the zazl.json configuration file.
dojo/text
The dojo/text plugin is used to load in text resource dependencies. Ideally, in a optimized environment you would want the optimizer to ensure these text resources are included in the javascript response stream in a form that avoids additional downloads. The dojo/text plugin makes use of a "require.cache" property to populate a cache. The zazl AMD loader supports populating the cache by providing an "addToCache" function. The server-side version of the dojo/text plugin writes calls to this function that are included into the javascript response stream.
function jsEscape(content) {
return content.replace(/(['\\])/g, '\\$1')
.replace(/[\f]/g, "\\f")
.replace(/[\b]/g, "\\b")
.replace(/[\n]/g, "\\n")
.replace(/[\t]/g, "\\t")
.replace(/[\r]/g, "\\r");
};
exports.write = function(pluginName, moduleName, write, moduleUrl) {
var textContent = require('zazlutil').resourceloader.readText(moduleUrl);
if (textContent) {
write("zazl.addToCache('"+moduleName+"', '"+jsEscape(textContent)+"');\n");
}
};
return content.replace(/(['\\])/g, '\\$1')
.replace(/[\f]/g, "\\f")
.replace(/[\b]/g, "\\b")
.replace(/[\n]/g, "\\n")
.replace(/[\t]/g, "\\t")
.replace(/[\r]/g, "\\r");
};
exports.write = function(pluginName, moduleName, write, moduleUrl) {
var textContent = require('zazlutil').resourceloader.readText(moduleUrl);
if (textContent) {
write("zazl.addToCache('"+moduleName+"', '"+jsEscape(textContent)+"');\n");
}
};
This ensures that when the client-side version it will find the cache value and avoid an XHR call to obtain the resource.
dojo/selector/_loader
The dojo/selector/_loader plugin determines what selector engine to use. The client side version expects a true browser environment to be available to determine the required engine type. This not something that can be determined easily in an optimized environment so for now the server side version I have written provides a "normalize" method that simply returns the default "lite" module id.
exports.normalize = function(id, config, expand) {
return "dojo/selector/lite";
};
The optimizer ensures that this module, along with its dependencies, is included in the javascript response stream. I plan to make the value returned configurable.
dojox/gfx/renderer
The dojox/gfx/renderer plugin works in a similar fashion to the dojo/selector/_loader plugin. It currently returns a default value of "dojox/gfx/svg" via a provided "normalize" function.
i18n plugin support
Optimizing i18n plugin support is more complicated that other types of plugins due to the need to provide i18n message bundles based on the locale of the calling client. The other optimized plugins produce output that can be cached and included for ever request for the same AMD module and its dependencies. The i18n output must be generated for each request made.
Because of this I have made the i18n plugin support a special case and there is specialized code that produces the message bundles. The "dojo/i18n" plugin supports the same format of messages as the requirejs i18n plugin does. The zazl configuration files specifies a property that indicates the module id of the i18n plugin. When the optimizer encounters an i18n plugin reference the details are used by the javascript response renderer to include the specified message bundles based on the locale of calling client. When rendered into the client the client i18n plugin finds that the messages bundles have been loaded in and avoid XHR requests to load them.
What's next ?
- Currently the optimizer is only usable in a Java JEE WebContainer environment that requires you to write your frontend HTML resources as JSP's. I now have a working HTML filter that enables developers to write HTML resources instead. The HTML is parsed for AMD module references. The HTML filter inserts the required javascript tag to load the module and all its dependencies based on the parser results. My next blog post will provide more details on how the HTML filter works.
- I plan to also provide a version of both the AMD optimizer and HTML filter for node.js.
- So far I have focused on making the optimizer support Dojo based applications. As jquery and other javascript frameworks are now supporting AMD I plan to ensure the optimizer supports applications using these frameworks too.
Update:
I have improved the configuration such that it is not duplicated in zazl.json. I have also written a blog post about the HTML filtering approach to inserting the required script tags
Subscribe to:
Posts (Atom)