A Dive into SystemJS – Part 1

The ECMA2015 module syntax for JavaScript was a much needed addition to the language. For years now the JavaScript community has tried to make up for the lack of a standard module format with several competing formats: AMD, CommonJS, and then UMD which tried to wrap both of the others. The introduction of an official module syntax, details of which can be found at the MDN imports documentation page, means that there is going to be a new module loader required to load the new format. Unfortunately the ECMA2015 specification ended up not including a module loader, but there is a proposal with the WhatWG team to add a module loader to the browser. SystemJS is a module loader built on top of the original ES6 module loader polyfill and soon to be the polyfill for the WhatWG module loader. This series of articles is going to take a deep dive into SystemJS and see what all it has to offer. This article assumes at least a basic understanding of the module syntax so it would be a good idea to read the MDN documentation first.

This is the first of three articles related to SystemJS. The second article covers the loading and translating of modules, and the third article talks about production considerations.

An Overview of the Loading Process
SystemJS loads files in order from top to bottom and then instantiates from bottom to top. For instance, consider the example of File1 which has a dependency on File2 which itself has a dependency on File3. When you ask SystemJS to load File1 it will first load File1 and check for dependencies. It will see that it has a dependency on File 2 so it will load that next, and then because of the dependency on File3 it will pull down File3. Once all of those files have been loaded it will actually execute the contents of File3 to get an instance of the module and then execute File2 passing that instance of File3. It will take the returned instance of File2 and pass that to File1 when instantiating that, and then pass the instance of File1 to the consumer that initiated the original import. SystemJS also caches the loaded modules so if at any point in that process one of the modules has been loaded previously then SystemJS will use the cached version instead of performing additional load and instantiation steps.

A Simple Example
Let us start with a simplistic view of what happens when you attempt to load a script with SystemJS.

System.import('./src/codeFile.js').then(function(m) {
    // do something with 'm'
});

In this example I am attempting to import the module located at the file path of “src/codeFile.js” relative to the current location.

  1. The first thing that SystemJS does is to normalize the file path that you are attempting to import so that absolute, relative, and aliased paths all resolve to the same file. There are several different configuration options that will affect the normalization process, but we will cover those later.
  2. Once it has the normalized name then SystemJS will check its internal registry to see if that module has already been loaded, and it simply returns the loaded version if it exists. If it has not been loaded previously then it attempts to fetch the module by making an XHR request to the absolute URL.
  3. Once the contents of the file have been retrieved, it will pass them to a translate method which can perform additional modification of the file’s contents if desired.
  4. Finally, it will pass the translated contents to an instantiate function which will execute the module, add it to the registry, and then resolve the promise with the module.

These actions are asynchronous so the caller is initially returned a promise that will be resolved with the module once it has been loaded.

Path Normalization
There are several different things that can affect the normalization process. The JSON config settings for SystemJS contains “map”, “paths”, and “package” sections that can be used to modify the normalized name.

The “paths” configuration section can be used to define aliases for path parts. For instance, the following code will define an alias that is used in the subsequent import.

System.paths["subfolder/*"] = "./src/app/subfolder/*";
System.import('subfolder/codeFile.js');

In this example the normalized path for the import would be http://localhost:8080/src/app/subfolder/codeFile.js. SystemJS supports wildcards but also uses specificity to determine the normalized value where the most specific version will win out.

System.paths["subfolder/*"] = "./src/app/subfolder/*";
System.paths["subfolder/codeFile2.js"] = "./src/app/secondFolder/codeFile2.js";
System.import('subfolder/codeFile2.js');

This example would evaluate to http://localhost:8080/src/app/secondFolder/codeFile2.js since that path is more specific than the wildcard path.

The “map” section can be used to define aliases for modules and it can also be used in a similar manner as “paths” to modify parts of a path, although that particular use has been deprecated. Maps are applied prior to the paths configurations so you can make use of any path aliases in your maps. The map section can be especially useful when you are dealing with dependencies of a file since you can define aliases that are scoped to a specific module.

map: {
    "codeFile1.js": {
        "jquery": "vendor/jquery2.0.12/jquery.min.js"
    }
}

//codeFile1.js
import $ from 'jquery';

In this example we defined an alias “jquery” that actually maps to the file located at “vendor/jquery/jquery-2.0.2.js”, so if we upgrade jQuery to a different version in the future then we just need to only update this config file. You can also use the map section to scope dependencies to specific modules; this helps to aggregate all of your dependency versions in one place instead of needing to scatter those throughout your code base. It also allows you to override the version for specific modules if they need a different version than the rest of your application.

// codeFile.js that needs jQuery 2
import $ from 'jquery';

// codeFile2.js that needs jQuery 1.10
import $ from 'jquery';

// config section
map: {
    "src/app/codeFile.js": {
        "jquery": "vendor/jquery/jquery-2.0.2.js"
    },
    "src/app/codeFile2.js": {
        "jquery": "vendor/jquery/jquery-1.10.0.js"
    }
}

In this example we scoped the “jquery” alias to be specific to each module so one module will get jQuery 2 and the other module will get jQuery 1.10, which can be really useful if you have code in your application that requires different versions of dependencies than the rest of the application.

The map section also works with the paths section to give even more flexibility.

// codeFile.js that needs jQuery 2
import $ from 'jquery';

// config section
paths: {
    "vendor/*": "src/vendors/*"
},
map: {
    "src/app/codeFile.js": {
        "jquery": "vendor/jquery/jquery-2.0.2.js"
    }
}

The final path for jQuery in this example would be http://localhost:8080/src/vendors/jquery/jquery-2.0.2.js since the path modification happens after the map modification. because of this our vendors path alias is picked up and used as expected.

The last configuration section that can really modify the normalized name is the “packages” section. The packages section allows you to use a different configuration that is scoped to a package, which can be a module or a partial path. For instance, you can define a custom map section underneath a package.

// config section
packages: {
    "src": {
        map: {
            "jquery": "vendor/jquery/jquery-2.0.2.js"
        }
    }
}

// This file will have a module alias "jquery" available to it since it is nested underneath the "src" package
System.import('src/components/codeFile.js');

Any module resolved inside of the “src” path will have this custom map section merged into the global map configuration section. The packages section allows you to configure other things that we will get to later but this is the part that lets you modify the normalized module path.

SystemJS Module Registry
SystemJS exposes a public API to allow you to interact directly with the module registry. With this API you can check to see if a module exists, retrieve the loaded module, or even register new modules. You need to make sure to use the normalized name when interacting with the registry though since it contains the fully normalized names.

System.has('src/codeFile1.js');  // false since that is an unnormalized path
System.has(System.normalize('src/codeFile1.js'));  // true, assuming that module has been loaded

// Now let's register a module if it is not there already
var normalizedName = System.normalize("src/newModule.js");
if (System.has(normalizedName)) {
    var codeModule = System.get(normalizedName);
    // Now you can get either the default module or named modules off of this object.
    var defaultModule = codeModule.default;
    var namedModule = codeModule.someNamedExport;
} else {
    var newModule = System.newModule({
        default: function() {},
        prop1: 'value'
    });
    System.set(normalizedName, newModule);
}

If you call System.set on a new module without normalizing the name first then when you call System.import you will probably run into issues since System.import normalizes the name before checking the registry.

You can also use System.register or System.registerDynamic instead of System.set. System.register is intended to be used only with modules that follow the official format for ES6/2015 modules. System.registerDynamic is more closely aligned with legacy module formats to make it easy to convert older modules into the System format.

// Legacy code codeFile1.js in CommonJS format
module.exports = require('codeFile1Dependency.js');

// New version as a System module wrapper around the CommonJS code
System.registerDynamic(['./codeFile1Dependency.js'], true, function(require, exports, module) {
    module.exports = require('./codeFile1Dependency.js');
});

This example follows the CommonJS syntax except that it is wrapped with the System.registerDynamic call. The reason that this wrapper is needed is because the CommonJS require statement is synchronous, which in the web environment will only work if the module you are requiring has already been loaded. The System module wrapper defines the dependency to ensure that the dependency will have been loaded by the time that the CommonJS require statement executes.

Conclusion
I hope that this article has provided you with at least a basic understanding of the SystemJS configuration and API. In my next article I will outline what happens in the time between when a module is fetched and when it is instantiated, and then in my last article I will talk about production workflows.

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: