Modules/Transport/A

From CommonJS Spec Wiki
Jump to: navigation, search

STATUS: PROPOSED

Alternate Module Format: run()

This is a proposal for an alternate module format that is optimized for browser loading. The current difficulties with [Modules/1.1] as it relates to loading code in a browser:

In order to work in the browser, CommonJS 1.1 modules normally need to take one of two paths:

  1. Requires a synchronous XHR request to load the module and eval it, so that modules are loaded in the right order.
  2. Requires an intermediate process (like a server process or build step) to wrap the module in a function wrapper so the modules are not executed out of order.

Path #1 is a slower loading process that blocks the browser. Using eval() on the modules makes it hard to debug them in all browsers, and eval() is generally discouraged as a coding practice.

Path #2 requires an intermediary, extra machinery to develop modules. Depending on your environment choice, it means slower development. It also means a developer cannot just write some static files in an IDE and click refresh to load static files.

Proposed Format

To fix these problems, an alternate format is suggested. This format is implemented by RunJS, but it is open for discussion on the particulars. RunJS in some form is a likely candidate for a Dojo 2.0 code loader.

Choices made in the module format are optimized for/restricted by the browser environment, but they will work on in a server environment.

All modules are registered by calling a run() function. The name of the function is not set in stone, but a function entry point is needed.

run() takes a variable amount of arguments, based on the scenario:

Running code that does not define a module

run(
    ["a", "b", "c"], //the module names
    function(a, b, c) { //modules are passed in as arguments matching the array of module names.
        //do things with the a, b and c modules.
    }
);

Defining a module

Defining a module is the same as running code that depends on a module, but the first argument is the module name, and the function should return an object that defines the module:

run(
    "a",
    ["b", "c"],
    function(b, c) {
        //Return an object or function to define the "a" module.
        return {
            color: "blue"
        };
    }
);

An explicit module name is needed in order to properly identify the run call in the browser. Mike Wilson on the dojo-contributors list suggested that maybe the script tag's onload handler would be fired right after loading of a script, which would make it possible to not specify the module name. Unfortunately, Internet Explorer does not always fire onload events that match up to the script onload events. See this test page.

So an explicit module name is needed. This is also beneficial when combining a few modules together into one file, for the purposes of optimized file loading.

If a module does not have any dependencies, and it is just a set of properties, then the run() call can be simplified:

run(
    "a",
    {
        color: "blue"
    }
);

A module can also define a function as its definition, but run() needs to know that it will return a Function, so Function is passed after the module name.

run(
    "node",
    Function,
    ["b", "c"],
    function(b, c) {
        return function(id) {
           if (typeof id == "string") {
               return document.getElementById(id);
            } else {
               //already a DOM node, return it.
               return id;
            }
        }
    }
);

Currently runjs also supports a format for loading i18n bundles. I will not document it here, since it is not strictly necessary for basic module syntax, but you can read more about it here, in the "Define an I18N Bundle" section.

Configuration Options

At the top level of code execution, a configuration object can be passed as the first option. This example shows it running in a browser:

<script type="text/javascript" src="scripts/run.js"></script>
<script type="text/javascript">
  run({
          baseUrl: "/another/path/",
          paths: {
              "some": "some/v1.0"
          },
          waitSeconds: 15,
          context: "foo"
      },
      ["a.js", "b.js", "some.module", "my.module"],
      function() {
          //This function will be called when all the dependencies
          //listed above are loaded. Note that this function could
          //be called before the page is loaded. This callback is optional.
      }
  );
</script>

That example shows all the supported configuration options:

baseUrl: the root path to use for all module lookups. So in the above example, "my.module"'s script tag will have a src="/another/path/my/module.js". If no baseUrl is passed in, the path to run.js is used as the baseUrl path.

paths: allows configuration of some modules paths. Assumed to be relative to baseUrl. So for "some.module"'s script tag will have a src="/another/path/some/v1.0/module.js"

waitSeconds: The number of seconds to wait before giving up on loading a script. The default is 7 seconds. This is needed for the browser context, where loading of individual modules is done through async script tags.

context: A name to give to a loading context. This allows run.js to load multiple versions of modules in a page, as long as each top-level run call specifies a unique context string. See Advanced Features below.

Advanced Features

Multiversion support

As mentioned in Configuration Options, multiple versions of a module can be loaded in a page by using different "context" configuration options. Here is an example that loads two different versions of the alpha and beta modules (this example is taken from one of the test files):

<script type="text/javascript" src="../run.js"></script>
<script type="text/javascript">
run({
          context: "version1",
          baseUrl: "version1/"
    },
    ["run", "alpha", "beta",],
        function(run, alpha, beta) {
          log("alpha version is: " + alpha.version); //prints 1
          log("beta version is: " + beta.version); //prints 1
 
          setTimeout(function() {
                run(["omega"],
                  function(omega) {
                        log("version1 omega loaded with version: " + omega.version); //prints 1
                  }
                );
          }, 100);
        }
);
 
run({
          context: "version2",
          baseUrl: "version2/"
    },
    ["run", "alpha", "beta"],
    function(run, alpha, beta) {
      log("alpha version is: " + alpha.version); //prints 2
      log("beta version is: " + beta.version); //prints 2
 
      setTimeout(function() {
        run(["omega"],
          function(omega) {
            log("version2 omega loaded with version: " + omega.version); //prints 2
          }
        );
      }, 100);
      }
);
</script>

Module Modifiers

The RunJS documentation describes a module modifier capability, that works by lazy loading modules that modify other modules. However, that feature is not required for this proposal.

Tests

There are some tests that show a simple use case, a circular reference and multiversion support:

RunJS 0.0.3 tests

Contrasts with CommonJS modules

This format differs from current CommonJS files in the following ways:

  1. Requires modules to specify their name, because of browser constraints. It also makes bundling multiple modules in one file easier.
  2. Relative module names that start with "." and ".." are not supported in the module names, given the first point.
  3. All dependencies are declared up top, and passed as arguments to a function that define the module.
  4. "." is used in module names, however, it could be changed to "/" if that worked better for the community.