What is AMD?
Defining a module is pretty simple. Let's say there exists a plain non-AMD script file defining a Utils class. It might look something like this:
Utils will exist in the global (window) scope when this file is loaded on the webpage. The AMD version of this would be written as:
The syntax for loading this module is pretty straight-forward as well. In this example we need to have Utils loaded before making a call to it. You'll note that we can use the Utils object because it's returned from the define and passed in as the argument for the require callback. This sort of encapsulation is highly encouraged by the AMD pattern.
Dependencies between modules are expressed by passing in an array of module names as the second argument to a define call:
In practice we omit the module name from our script files as the system will infer the module name based on how the script is loaded.
Tying together AMD and Platform Plugins
Module Path Mappings
By default RequireJS will try to load modules relative to the current URL. So if the current page is /pentaho/foo/index.html, a call to load the "Utils" module will have the system try to load it from /pentaho/foo/Utils.js. This is not very valuable to us so we've adopted a namespaced approach. Each plugin is able to define root namespaces and provide URL path mappings for them. For instance the CDF plugin defines modules with the "cdf" namespace like "cdf/CoreComponents.js". It also provides a configuration for the RequireJS system mapping the "cdf" namespace like so:
Note that CONTEXT_PATH is provided by the webcontext.js file and is supplies the webapp name ("/pentaho/" by default). So when a user makes a call to require "cdf/CoreComponents" RequireJS will load it from "/pentaho/content/pentaho-cdf/js". This provides from a greater degree of abstraction in where scripts are loaded.
Providing RequireJS Configurations
To understand how plugins supply their configuration, you have to understand the webcontext.js Filter and External Resource definitions.
WebContext writes out the following in order:
- The global CONTEXT_PATH variable holding the webapp name if any.
- The global FULLY_QUALIFIED_URL variable which holds the full URL of the server.
- The base requireCfg configuration Object which is extended by plugins.
- All External Resource scripts defined with the "requirejs" context. This is where plugins configure the RequireJS paths!
- The SESSION_LOCALE variable containing the computed locale for the request
- The require.js and require-cfg.js files to initialize the RequireJS ystem.
- Finally it loads the remaining External Resources entries with the "global" context and those matching the current context for the request ("dashboards", "analyzer", etc.)
Below is the actual webcontext.js contexts for 4.8.0-GA:
You'll notice several scripts are injected before the inclusion of require.js and require-cfg.js. These scripts are provided by the plugins and contain code extending the RequireJS configuration object. As mentioned, they are provided through the External Resource system.
Plugins can provide scripts and CSS to be loaded in other areas of the platform by including entries in their plugin.xml. For instance, Analyzer provides it's Dashboard Widget and it's configuration for RequireJS by defining the following in it's plugin.xml:
You'll notice the name of Analyzer RequireJS config script and all of the others included in the sample webcontext.js end with "require-js-cfg.js". This is important as the Spring Security white-list is allowing all requests ending with this to return un-authenticated. Analyzer's RequireJS configuration file is pretty typical:
It adds an "analyzer" root namespace to the "paths" entry. This is what routes requests for modules beginning with "analyzer/" to the appropriate URL location. The check for "debug=true" is changing the location where the system loads the scripts so that the uncompressed files are available for debugging.