AMD Javascript Modules (RequireJS)

The BA-Server's client-side Javascript is making the transition from plain Javascript files to AMD-compatible script modules. This began with the 4.5 release and has continued in 4.8 where we've also taken the opportunity to update to RequireJS 2.0. The following describes the system as it exists in 4.8.

What is AMD?

First-off, you have to understand AMD modules. Asynchronous Module Definitions are JavaScript files that declare a module name and optionally list their dependencies. These script files are not included directly in the HTML source, rather they're loaded using the RequireJS script. Once a call has be made to load a module, it's retrieved from the server based on the URL mappings, dependencies are computed and retrieved, and baring any unresolved dependencies it is finally loaded and returned to the requestor.

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:

 
var Utils = {
  somethingUseful : function(param){
    return param;
  }
}

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:

 
define("Utils", function(){
  return {
    somethingUseful : function(param){
      return param;
    }
  };
});

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.

 
require(["Utils"], function(Utils){

  alert(Utils.somethingUseful("hello world"));

});

Dependencies between modules are expressed by passing in an array of module names as the second argument to a define call:

 
define("ModuleA", ["Utils", "ModuleB"], function(Utils, ModuleB){
 ...
});

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

As plugins become more and more capable we've started sharing Javascript code between them. This is most commonly done by providing Javascript from one plugin to extend the functionality of another. In other cases a plugin may have a hard dependency upon script provided by another. This cross-plugin loading is accomplished through RequireJS and setup by module path mappings.

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:

 

requireCfg['paths']['cdf'] = CONTEXT_PATH+'content/pentaho-cdf/js';

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.js

Almost every page supplied by our server will include a webcontext.js script tag. You won't find a webcontext.js file anywhere in the system, the content is actually supplied by an HTTP Servlet Filter. It's writes out several things which are critical for the execution of most of our client-side Javascript. Most calls to webcontext.js also include an additional "context" parameter telling it what area of the Platform it's intended for. This context is tied into the External Resources system described later.

WebContext writes out the following in order:

  1. The global CONTEXT_PATH variable holding the webapp name if any.
  2. The global FULLY_QUALIFIED_URL variable which holds the full URL of the server.
  3. The base requireCfg configuration Object which is extended by plugins.
  4. All External Resource scripts defined with the "requirejs" context. This is where plugins configure the RequireJS paths!
  5. The SESSION_LOCALE variable containing the computed locale for the request
  6. The require.js and require-cfg.js files to initialize the RequireJS ystem.
  7. 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:

var CONTEXT_PATH = '/pentaho/';

var FULL_QUALIFIED_URL = 'http://localhost:8080/pentaho/';

var requireCfg = {waitSeconds: 30, paths: {}, shim: {}};
<!-- Injecting web resources defined in by plugins as external-resources for: requirejs-->
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/analyzer/scripts/analyzer-require-js-cfg.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "js/require-js-cfg.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/pentaho-cdf/js/cdf-require-js-cfg.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/pentaho-geo/resources/web/geo-require-js-cfg.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/common-ui/resources/web/common-ui-require-js-cfg.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/reporting/reportviewer/reporting-require-js-cfg.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/dashboards/script/dashboards-require-js-cfg.js'></scr"+"ipt>");
document.write("<script type='text/javascript' src='/pentaho/js/require.js'></scr"+"ipt>");
document.write("<script type='text/javascript' src='/pentaho/js/require-cfg.js'></scr"+"ipt>");
<!-- Providing computed Locale for session -->
var SESSION_LOCALE = 'en_US';
if(typeof(pen) != 'undefined' && pen.define){pen.define('Locale', {locale:'en_US'})};<!-- Injecting web resources defined in by plugins as external-resources for: global-->
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "js/themes.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/common-ui/resources/themes/jquery.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/common-ui/resources/web/dojo/djConfig.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/common-ui/resources/web/cache/cache-service.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/common-ui/resources/themes/jquery.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/common-ui/resources/themes/themeUtils.js'></scr"+"ipt>");
document.write("<script language='javascript' type='text/javascript' src='"+CONTEXT_PATH + "content/pentaho-mobile/resources/mobile-utils.js'></scr"+"ipt>");

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.

External Resources

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:

  <external-resources>
    <file context="dashboards">content/analyzer/scripts/widget/AnalyzerDashboardWidget.js</file>
    <file context="requirejs">content/analyzer/scripts/analyzer-require-js-cfg.js</file>
  </external-resources>

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:

if(document.location.href.indexOf("debug=true") > 0){
	requireCfg['paths']['analyzer'] = CONTEXT_PATH+'content/analyzer/scripts';
} else {
	requireCfg['paths']['analyzer'] = CONTEXT_PATH+'content/analyzer/scripts/compressed';
}

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.

Helpful links!

https://github.com/amdjs/amdjs-api/wiki/AMD
http://requirejs.org/