AMD 模式介绍(en)

AMD: The Definitive Source

So what is AMD?

As web applications continue to grow more advanced and more heavily rely on JavaScript, therehas been a growing movement towards using modules to organize code and dependencies.Modules give us a way to make clearly distinguished components and interfaces that can easilybe loaded and connected to dependencies. The AMD module system gives us the perfect pathfor using JavaScript modules to build web applications, with a simple format, asynchronousloading, and broad adoption.

The Asynchronous Module Definition (AMD) format is an API for defining reusable modules that can be used across different frameworks. AMD was developed to provide a way to define modules such that they could be loaded asynchronously using the native browser script element-based mechanism. The AMD API grew out of discussions in 2009 in the Dojo community which then moved to discussions with CommonJS on how to better adapt the CommonJS module format (used by NodeJS) for the browser. It has since grown into its own standard with its own community. AMD has taken off in popularity, with numerous module loaders and widespread usage. At SitePen we have worked extensively with AMD in Dojo, adding support and now actively building applications with this format.

Glossary

  • Module – An encapsulated JavaScript filethat follows a module format, indicatingdependencies and providing exports.
  • Module ID – This is the unique string that identifies a module. There are relative module ids that resolve to absolute module ids relative to the current module’s id.
  • Module Path – This is the URL that is used to retrieve a module. A module id is mapped to the module path based on the loader’s configuration rules (by default, modules are assumed to be relative to the base path, typically the parent of the module loader package).
  • Module Loader – This is the JavaScript code that resolves and loads modules and their associated dependencies, interacts with plugins, and handles configuration.
  • Package – A collection of modules grouped together. For example, dojo, dijit, and dgrid are packages.
  • Builder – This is a tool that will generate a concatenated JavaScript file composed of a module (or modules) and its dependencies, thus making it possible to take an application composed of numerous modules and create a number of built layers that can be loaded in a minimal number of HTTP requests.
  • Layer – A file that contains modules that have been optimized into a single file by a Builder.
  • Dependency – This is a module that must be loaded for another module to function properly.
  • AMD – Asynchronous Module Definition, a module format optimized for browser usage.
  • Factory – The function provided to the module loader via define that is to be executed once all the dependencies are ready

Why AMD Modules?

The basic premise of a module system is to:

  • allow the creation of encapsulated segments of code, called modules
  • define dependencies on other modules
  • define exported functionality that can in turn be used by other modules
  • discreetly access the functionality provided by these modules

AMD satisfies this need, and uses a callback function with dependencies as arguments so that dependencies can be asynchronously loaded before the module code is executed. AMD also provides a plugin system for loading non-AMD resources.

While alternate methods can be used to load JavaScript (XHR + eval), using script elements to load JavaScript has an edge in performance, eases debugging (particularly on older browsers), and has cross-domain support. Thus AMD aims to provide an optimal development experience in the browser.

The AMD format provides several key benefits. First, it provides a compact declaration of dependencies. Dependencies are defined in a simple array of strings, making it easy to list numerous dependencies with little overhead.

AMD helps eliminate the need for globals. Each module defines dependencies and exports by referencing them with local variables or return objects. Consequently, modules can define functionality and interact with other modules without having to introduce any global variables. AMD is also “anonymous”, meaning that the module does not have to hard-code any references to its own path, the module name relies solely on its file name and directory path, greatly easing any refactoring efforts.

By coupling dependencies with local variables, AMD encourages high-performance coding practices. Without an AMD module loader, JavaScript code has traditionally relied on the nested objects to “namespace” functionality of a given script or module. With this approach, functions are typically accessed through a set of properties, resulting in a global variable lookup and numerous property lookups, adding extra overhead and slowing down the application. With dependencies matched to local variables, functions are typically accessed from a single local variable, which is extremely fast and can be highly optimized by JavaScript engines.

Using AMD

The foundational AMD API is the define() method which allows us to define a module and its dependencies. The API for writing modules consists of:

1
2
3
define(dependencyIds, function (dependency1, dependency2,...){
    // module code
});

The dependencyIds argument is an array of strings that indicates the dependencies to be loaded. These dependencies will be loaded and executed. Once they have all been executed, their export will be provided as the arguments to the callback function (the second argument todefine()).

To demonstrate basic usage of AMD, here we could define a module that utilizes the dojo/query(CSS selector query) and dojo/on (event handling) modules:

1
2
3
4
5
6
7
8
9
10
define([ "dojo/query" , "dojo/on" ],
         function (query, on){
     return {
         flashHeaderOnClick: function (button){
             on(button, "click" , function (){
                 query( ".header" ).style( "color" , "red" );
             });
         }
     };
});

Once dojo/query and dojo/on are loaded (which doesn’t happen until their dependencies are loaded, and so on), the callback function is called, with the query argument given the export ofdojo/query (a function that does CSS selector querying), and the on argument given the export ofdojo/on (a function that adds an event listener). The callback function (also known as the module factory function), is guaranteed to be called only once.

Each of the module ids listed in the set of dependencies is an abstract module path. It is abstract because it is translated to a real URL by the module loader. As you can see the module path does not need to include the “.js” suffix, this is automatically appended. When the module id starts with a name, this name is considered to be an absolute module id. In contrast, we can specify relative ids by starting with a “./” or a “../” to indicate a sibling path or parent path, respectively. These are resolved to their absolute module ids by standard path resolution rules. You can then define a module path rule to determine how these module paths are converted to URLs. By default, the module root path is defined relative to the parent of the module loader package. For example, if we loaded Dojo like (note that we set async to true to enable true async loading of AMD):

Then the root path to modules would be assumed to be “/path/to/”. If we specified a dependency of “my/module”, this would be resolve to “/path/to/my/module.js”.

Initial Module Loading

We have described how to create a simple module. However, we need an entry point to trigger the chain of dependencies. We can do this by using the require() API. The signature of this function is basically the same as define(), but is used to load dependencies without defining a module (when a module is defined, it is not executed until it is required by something else). We could load our application code like:

1
2

Dojo provides a shortcut for loading an initial module. The initial module can be loaded by specifying the module in the deps configuration option:

1

This is an excellent way of loading your application because JavaScript code can be completely eliminated in HTML, and only a single script tag is needed to bootstrap the rest of the application. This also makes it very easy to create aggressive builds that combine your application code and dojo.js code in a single file without having to alter the HTML script tags after the build. RequireJS and other module loaders have similar options for loading the top level modules.

The progression of dependency loading from the require() call to the modules is illustrated in the diagram above. The require() call kicks off the loading of the first module, and dependencies are loaded as needed. Modules that are not needed (“module-d” in the diagram) are never loaded or executed.

The require() function can also be used to configure the module path look-ups, and other options, but these are generally specific to the module loader, and more information on configuration details are available in each loader’s documentation.

Plugins and Dojo Optimizations

AMD also supports plugins for loading alternate resources. This is extremely valuable for loading non-AMD dependencies like HTML snippets and templates, CSS, and internationalized locale-specific resources. Plugins allow us to reference these non-AMD resources in the dependency list. The syntax for this is:

1
"plugin!resource-name"

A commonly used plugin is the dojo/text plugin which allows you to directly load a file as text. With this plugin, we list the target file as the resource name. This is frequently used by widgets to load their HTML template. For example, with Dojo we can create our own widget like this:

1
2
3
4
5
6
define([ "dojo/_base/declare" , "dijit/_WidgetBase" , "dijit/_TemplatedMixin" , "dojo/text!./templates/foo.html" ],
         function (declare, _WidgetBase, _TemplatedMixin, template){
     return 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. The dojo/i18n plugin is used to load internationalized locale-specific bundles (often used for translated text or regional formatting information). Another important plugin is dojo/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 use dojo/domReady as a plugin, but no resource name is needed:

1
2
3
4
5
6
7
define([ "dojo/query" , "dojo/domReady!" ],
         function (query){
    // DOM is ready, so we can query away
    query( ".some-class" ).forEach( function (node){
        // do something with these nodes
    });
});

Another valuable plugin is dojo/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 a has() 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 the dojo/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:

1
2
3
4
5
6
define([ "dojo/has!touch?ui/touch:ui/desktop" ],
   function (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 using dojo/has is more than just a run-time API for feature detection. By usingdojo/has, both in has() 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 the staticHasFeatures 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 argument define() call, where the argument is the object. This is very simple and straightforward:

1
2
3
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 subsequent require()‘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 site provides a central place to see a list of some of the packages that are available. The CPM installer can 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, James Burke, the author of RequireJS has created Volo, 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 a define() 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:

1
2
3
4
5
6
7
my-script.js:
// add this to top of the script
defined([], function (){
// existing script
...
// add this to the end of script
});

And we could build application components that pull modules from various sources:

1
2
3
require([ "dgrid/Grid" , "dojo/query" , "my-script" ], function (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 a define() 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 the var or function 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:

1
2
3
4
require([ "dojo" , "jquery.js" ], function (dojo){ // jquery will be loaded as plain script
     dojo.query(...); // the dojo exports will be available in the "dojo" local variable
     $(...);   // the other script will need to create globals
});

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 (like dgrid) 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 calling define() with a single callback argument where the factory function contains require() 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:

1
2
3
4
5
define( function (require){
     var query = require( "dojo/query" );
     var on = require( "dojo/on" );
     ...
});

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 the require() API: When require() 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// declare modules that we need up front
define([ "dojo/dom-create" , "require" ],
         function (domCreate, require){
     return function (node){
        // 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
        var slider = domCreate( "div" , {className: "slider" }, node);
        var progress = domCreate( "div" , {className: "progress" }, node);
        // now load the widgets, we load them independently
        // so each one can be rendered as it downloads
        require([ "dijit/form/HorizontalSlider" ], function (Slider){
           new Slider({}, slider);
        });
        require([ "dijit/Progress" ], function (Progress){
           new 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-local require() 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
main.js:
define([ "component" , "exports" ],
         function (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 = function (){
         new component.Widget();
     };
});
component.js:
define([ "main" , "exports" , "dojo/_base/declare" ],
         function (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: function (){
             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”, and the require() calls will be scanned, so this example could be written:

1
2
3
4
5
6
define( function (require, exports){
     var query = require( "dojo/query" );
     exports.myFunction = function (){
        ....
     };
});

Looking Forward

The EcmaScript committee has been working on adding native module support in JavaScript. The proposed addition is based on new syntax in the JavaScript language for defining and referencing modules. The new syntax includes a module keyword for defining modules in scripts, an export keyword defining exports, and an import 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.

1
2
3
4
5
6
7
import {query} from "dojo/query.js" ;
import {on} from "dojo/on.js" ;
export function flashHeaderOnClick(button){
     on(button, "click" , function (){
        query( ".header" ).style( "color" , "red" );
     });
}

The proposed new module system includes support for custom module loaders that can interact with the new module system, which may also still be used to retain certain existing AMD features like plugins for non-JavaScript resources.

Conclusion

AMD  provides  a  powerful  module  system  for  browser-based  web  applications leveraging  native browser  loading  for  fast  asynchronous  loading supporting  plugins  for  flexible  usage  of heterogeneous  resources and  utilizing  a  simple straightforward  format With  great  AMD  projects like  Dojo RequireJS and  others the  world  of  AMD  is  an  exciting  and  growing  opportunity  for  fast ,interoperable  JavaScript  modules .

from http://www.sitepen.com/blog/2012/06/25/amd-the-definitive-source/

你可能感兴趣的:(html/javascript,dojo)