6
"dojo/_base/declare"
"dijit/_WidgetBase"
"dijit/_TemplatedMixin"
"dojo/text!./templates/foo.html"
(declare,_WidgetBase,_TemplatedMixin,template){
declare([_WidgetBase,_TemplatedMixin],{
templateString: template
});
This example is instructive on multiple levels for creating Dojo widgets. First,this represents the basic boilerplate for creating a widget using the Dijit infrastructure. You might also note how we created a widget class and returned it. The declare() (class constructor) was used without any namespace or class name. As AMD eliminates the need for namespaces,we no longer need to create global class names with declare() . This aligns with a general strategy in AMD modules of writing anonymous modules. Again,an anonymous module is one that does not have any hardcoded references to its own path or name within the module itself and we could easily rename this module or move it to a different path without having to alter any code inside the module. Using this approach is generally recommended,however if you will be using this widget with declarative markup,you will still need to include namespace/class names in order to create a namespaced global for Dojo’s parser to reference in Dojo 1.7. Improvements coming in Dojo 1.8 allow you to use module ids.
There are several other plugins that are included with Dojo that are useful. Thedojo/i18n plugin is used to load internationalized locale-specific bundles (often used for translated text or regional formatting information). Another important plugin isdojo/domReady ,which is recommended to be used as a replacement for dojo.ready. This plugin makes it very simple to write a module that also waits for the DOM to be ready,in addition to all other dependent modules,without having to include an extra callback level. We usedojo/domReady as a plugin,but no resource name is needed:
|
"dojo/domReady!"
(query){
// DOM is ready,so we can query away
".some-class"
).forEach(
(node){
// do something with these nodes
});
Another valuable plugin isdojo/has . This module is used to assist with feature detection,allowing you to choose different code paths based on the presence of certain browser features. While this module is often used as a standard module,providing ahas() function to the module,it can also be used as a plugin. Using it as a plugin allows us to conditionally load dependencies based on a feature presence. The Syntax for thedojo/has plugin is to use a ternary operator with conditions as feature names and module ids as values. For example,we could load a separate touch UI module if the browser supports touch (events) like:
|
"dojo/has!touch?ui/touch:ui/desktop"
(ui){
// ui will be ui/touch if touch is enabled,
//and ui/desktop otherwise
ui.start();
The ternary operators can be nested,and empty strings can be used to indicate no module should be loaded.
The benefit of usingdojo/has is more than just a run-time API for feature detection. By usingdojo/has ,both inhas() form in your code,as well as a dependency plugin,the build system can detect these feature branches. This means that we can easily create device or browser specific builds that are highly optimized for specific feature sets,simply by defining the expected features with thestaticHasFeatures option in the build,and the code branches will automatically be handled correctly.
Data Modules
For modules that do not have any dependencies,and are simply defined as an object (like just data),one can use a single argumentdefine() call,where the argument is the object. This is very simple and straightforward:
define({
foo:
"bar"
This is actually similar to JSONP,enabling script-based transmission of JSON data. But,AMD actually has an advantage over JSONP in that it does not require any URL parameters; the target can be a static file without any need for active code on the server to prefix the data with a parameterized callback function. However,this technique must be used with caution as well. Module loaders always cache the modules,so subsequentrequire() ‘s for the same module id will yield the same cached data. This may or may not be an issue for your data retrieval needs.
Builds
AMD is designed to be easily parsed by build tools to create concatenated or combined sets of modules in a single file. Module systems provide a tremendous advantage in this area because build tools can automatically generate a single file based on the dependencies listed in the modules,without requiring manually written and updated lists of script files to be built. Builds dramatically reduce load time by reducing requests,and this is an easy step with AMD because the dependencies are specifically listed in the code.
Without build
With build
Performance
As noted before,using script element injection is faster than alternate methods because it relies more closely on native browser script loading mechanisms. We setup some tests on dojo.js in different modes,and script element loading was about 60-90% faster than using XHR with eval. In Chrome,with numerous small modules,each module was loaded in about 5-6ms,whereas XHR + eval was closer to 9-10ms per module. In Firefox,synchronous XHR was faster than asynchronous,and in IE asynchronous XHR was faster than synchronous,but script element loading is definitely the fastest. It is also surprising that IE9 was the fastest,but this is probably at least partly due to Firefox and Chrome’s debugger/inspector adding more overhead. Module Loaders
The AMD API is open,and there are multiple AMD module loader and builder implementations that exist. Here are some key AMD loaders that are available:
- Dojo– This is a full AMD loader with plugins and a builder,and this is what we typically use since we utilize the rest of the Dojo toolkit.
- RequireJS– This is the original AMD loader and is the quintessential AMD loader. The author,James Burke,was the main author and advocate of AMD. This is full-featured and includes a builder.
- curl.js– This is a fast AMD loader with excellent plugin support (and its own library of plugins) and its own builder.
- lsjs– An AMD module loader specifically designed to cache modules in local storage. The author has also built an independent optimizer.
- NeedJS– A light AMD module loader.
- brequire– Another light AMD module loader.
- inject– This was created and is used by LinkedIn. This is a fast and light loader without plugin support.
- Almond– This is a lightweight version of RequireJS.
Getting AMD Modules
There is an increasing number of packages and modules that are available in AMD format. TheDojo Foundation packages siteprovides a central place to see a list of some of the packages that are available. TheCPM installercan be used to install any of the packages (along with automatically installing the dependencies) that have been registered through the Dojo Foundation packages site.
Alternately,the author of RequireJS has createdVolo,a package installer that can be used to easily install packages directly from github. Naturally,you can also simply download modules directly from their project site (on github or otherwise) and organize your directory structure yourself.
With AMD,we can easily build applications with any package,not just Dojo modules. It is also generally fairly simple to convert plain scripts to AMD. You simply add adefine() with an empty array,and enclose the script in the callback function. You can also add dependencies,if the script must be executed after other scripts. For example:
my-script.js:
defined([],monospace; font-size:1em; border:0px; bottom:auto; float:none; height:auto; left:auto; line-height:1.1em; margin:0px; outline:0px; overflow:visible; padding:0px; position:static; right:auto; top:auto; vertical-align:baseline; width:auto; min-height:inherit">(){
// existing script
...
// add this to the end of script
And we could build application components that pull modules from varIoUs sources:
require([
"dgrid/Grid"
"my-script"
(Grid,query){
new
Grid(config,query(
"#grid"
)[0]);
One caveat to be aware of when converting scripts to modules is that if the script has top level functions or variables,these would ordinarily result in globals,but inside of adefine() callback they would be local to the callback function,and no globals will be created. You can either alter the code to explicitly create a global (remove thevar orfunction prefix (you would probably want to do this if you need the script to continue working with other scripts that rely on the globals that it produces),or alter the module to return the functions or values as exports and arrange the dependent modules to use those exports (rather than the globals,this allows you to pursue the global-free paradigm of AMD).
Directly Loading Non-AMD Scripts
Most module loaders also support direct loading of non-AMD scripts. We can include a plain script in our dependencies,and denote that they are not AMD by suffixing them with “.js” or providing an absolute URL or a URL that starts with a slash. The loaded script will not be able to provide any direct AMD exports,but must provide its functionality through the standard means of creating global variables or functions. For example,we could load Dojo and jQuery:
4
"dojo"
"jquery.js"
(dojo){
dojo.query(...);
$(...);
});
|
Keep It Small
AMD makes it easy to coordinate and combine multiple libraries. However,while it may be convenient to do this,you should exercise some caution. Combining libraries like Dojo and jQuery may function properly,but it adds a lot of extra superfluous bytes to download since Dojo and jQuery have mostly overlapping functionality. In fact,a key part of Dojo’s new module strategy is to avoid any downloading of unnecessary code. Along with converting to AMD,the Dojo base functionality has been split into varIoUs modules that can be individually used,making it possible to use the minimal subset of Dojo that is needed for a given application. In fact,modern Dojo application and component development (likedgrid) often can lead to an entire application that is smaller than earlier versions of Dojo base by itself.
AMD Objections
There have been a few objections raised to AMD. One objection is that using the original CommonJS format,from which AMD is somewhat derived,is simpler,more concise,and less error prone. The CommonJS format does indeed have less ceremony. However,there are some challenges with this format. We can choose to leave the source files unaltered and directly delivered to the browser. This requires the module loader to wrap the code with a header that injects the necessary CommonJS variables,and thus relies on XHR and eval. The disadvantages of this approach have already been discussed,and include slower performance,difficult with debugging on older browsers,and cross-domain restrictions. Alternately,one can have a real-time build process,or on-request wrapping mechanism on the server,that essentially wraps the CommonJS module with the necessary wrapper,which actually can conform to AMD. These approaches are not necessarily showstoppers in many situations,and can be very legitimate development approaches. But to satisfy the broadest range of users,where users may be working on a very simple web server,or dealing with cross-browser,or older browsers,AMD decreases the chance of any of these issues becoming an obstacle for the widest range of users,a key goal of Dojo.
The dependency listing mechanism in AMD specifically has been criticized as being error prone because there are two separate lists (the list of dependencies and the callback arguments that define the variables assigned the dependencies) that must be maintained and kept in sync. If these lists become out of sync the module references are completely wrong. In practice,we haven’t experienced much difficulty with this issue,but there is an alternate way of using AMD that addresses this issue. AMD supports callingdefine() with a single callback argument where the factory function containsrequire() calls rather than a dependency list. This actually can not only help mitigate dependency list synchronization issues,but also can make it extremely easy to add CommonJS wrappers,since the factory function’s body essentially conforms to the CommonJS module format. Here is an example of how to define a module with this approach:
|
define(
(require){
var
query = require(
);
on = require(
);
...
When a single argument is provided,require,exports and module are automatically provided to the factory. The AMD module loader will scan the factory function for require calls,and automatically load them prior to running the factory function. Because the require calls are directly inline with the variable assignment,I could easily delete one of the dependency declarations without any further need to synchronize lists.
A quick note about therequire() API: Whenrequire() is called with a single string it is executed synchronously,but when called with an array it is executed asynchronously. The dependent modules in this example are still loaded asynchronously,prior to executing the callback function,at which time dependencies are in memory,and the single string require calls in the code can be executed synchronously without issue or blocking.
Limitations
AMD gives us an important level of interoperability of module loading. However,AMD is just a module definition,it does not make any prescriptions on the API’s that the module create. For example,one can’t simply ask the module loader for a query engine and expect to return the functionality from interchangeable query modules with a single universal API. There may be benefit to defining such APIs for better module interchange,but that is beyond the scope of AMD. And most module loaders do support mapping module ids to different paths,so it would be very feasible to map a generic module id to different target paths if you had interchangeable modules.
Progressive Loading
The biggest issue that we have seen with AMD is not so much a problem with the API,but in practice there seems to be an overwhelming tendency to declare all dependencies up front (and that is all we have described so far in this post,so we are just as guilty!). However,many modules can operate correctly while deferring the loading of certain dependencies until they are actually needed. Using a deferred loading strategy can be very valuable for providing a progressively loaded page. With a progressive loading page,components can be displayed as each one is downloaded,rather than forcing the download of every byte of JavaScript before the page is rendered and usable. We can code our modules in a way to defer loading of certain modules by using the asynchronous require([]) API in the code. In this example,we only load the necessary code for this function to create children container nodes for immediate visual interaction,but then defer the loading of the widgets that go inside the containers:
10
11
12
13
14
15
16
17
18
19
|
"dojo/dom-create"
"require"
(domCreate,require){
return
function
// create container elements for our widget right away,
// these could be styled for the right width and height,
// and even contain a spinner to indicate the widgets are loading
slider = domCreate(
"div"
"slider"
},node);
progress = domCreate(
"progress"
// now load the widgets,we load them independently
// so each one can be rendered as it downloads
"dijit/form/HorizontalSlider"
(Slider){
Slider({},slider);
});
"dijit/Progress"
(Progress){
Progress({},progress);
});
}
This provides an excellent user experience because they interact with components as they become available,rather than having to wait for the entire application to load. Users are also more likely to feel like an application is fast and responsive if they can see the page progressively rendering.
require,exports
In the example above,we use a special dependency “require”,which give us a reference to a module-localrequire() function,allowing us to use a module reference relative to the current module (if you use the global “require”,relative module ids won’t be relative to the current module).
Another special dependency is “exports”. With exports,rather than returning the exported functionality,the export object is provided in the arguments,and the module can add properties to the exports object. This is particularly useful with modules that have circular references because the module factory function can start running,and add exports,and another function utilize the factory’s export before it is finished. A simple example of using “exports” in a circular reference:
|
main.js:
"component"
"exports"
(component,exports){
// we define our exported values on the exports
// which may be used before this factory is called
exports.config = {
title:
"test"
};
exports.start =
(){
component.Widget();
};
});
component.js:
"main"
(main,exports,declare){
// again,we define our exported values on the exports
// which may be used before this factory is called
exports.Widget = declare({
showTitle:
(){
alert(main.config.title);
}
});
This example would not function properly if we simply relied on the return value,because one factory function in the circular loop needs to execute first,and wouldn’t be able to access the return value from the other module.
As shown in one of the earlier examples,if the dependency list is omitted,the dependencies are assumed to be “require” and “exports”,serif; font-size:13.600000381469727px">require() calls will be scanned,so this example could be written:
(require,exports){
exports.myFunction =
....
};
Looking Forward
The EcmaScript committee has been working on adding native module support in JavaScript. Theproposed additionis based on new Syntax in the JavaScript language for defining and referencing modules. The new Syntax includes amodule keyword for defining modules in scripts,anexport keyword defining exports,and animport keyword for defining the module properties to be imported. These operators have fairly straightforward mappings to AMD,making it likely that conversion will be relatively simple. Here is an example of how this might look based upon the current proposed examples in EcmaScript Harmony,if we were to adapt the first example in this post to Harmony’s module system.
import {query} from
"dojo/query.js"
;
import {on} from
"dojo/on.js"
;
export
flashHeaderOnClick(button){
(){
);
}
|