- Version: 0.4.0
- Author: Chris Ackerman
- Email: bluejeansandrain@gmail.com
This is a pure CommonJS Synchronous Modules implementation for browsers. No frills, no optimization, synchronous, no cross domain support.
Given that this is a synchronous system, it has the potential for numerous and blocking XMLHttpRequests. But wait! This problem is addressed in exactly the same way most asynchronous solutions address the request overhead problem: by adding a compile step that collects all module source into a single file and therefore a single request.
In ModularJS terminology, a compiled collection of modules is called a pack.
Goals
- Cross engine modules that can be "write once, use anywhere". Especially modules that work in the browser and on the server.
- Easy debugging and coding that behaves the same in development and production.
- Light weight. No built-in support that favors a specific engine on the server. That's what modules are for.
CommonJS Synchronous Module Specification
http://wiki.commonjs.org/wiki/Modules/1.1.1
- The optional
require.main,require.paths, andmodule.uriproperties are supported. - A .js extension is always added when resolving module names to absolute URIs, so unless a module ends in .js.js, don't add .js to a module name.
- Absolute IDs which begin with / are also supported.
module.idwill always be an absolute ID. This is not part of the specification, but it is a necessary adaptation due to the specification's slightly silly stipulation thatmodule.idbe a top-level ID, while making no requirement that relative IDs be resolvable to top-level IDs. Using absolute IDs is not recommended because they make your source less portable.
Why I Prefer Synchronous Module Systems
This project is actually a direct reaction to the apparent drift towards AMD. Asynchronous module definition is an attempt to make an "efficient enough" browser module system. The problem is that it isn't efficient enough for production out of the box, so a compilation step is recommended anyway. If a compilation step is going to be used, why not use what I believe is the cleaner and more direct synchronous CommonJS modules. After several attempts to use AMD, I just don't see it as the future of JavaScript modules.
I prefer the syntax of the synchronous require...
var myModule = require( 'myModule' );
// Dependant code here.
... to that of the asynchronous require or define .
define( ['myModule'], function( myModule )
{
// Dependant code here.
});
A lot of people will tell you that synchronous I/O in JavaScript is evil. For production they are generally right. But as I mentioned before, in both async and sync module systems you will most likely be using a compiler to put all of your modules into a single file in order to reduce remote I/O overhead, as well as to minify and lint your source. Once your production code is compiled, async vs sync becomes moot as far as efficiency is concerned.
Debugging is easier in synchronous code. When stepping through execution, you can step over a require call, where as you would need to step into a define and through its internals to get to where the define initializer function is called.
Synchronous calls can be wrapped in try/catch.
When using define an entire module must be considered to depend on all the dependencies passed to define, instead of allowing individual code paths to have dependencies. This might seem like a quibble, but if module initialization can be delayed until the first time a module is required, then a module may never need to be initialized if no code path which uses it is ever excercised.
The CommonJS synchronous module specification is widely used in server side JavaScript engines. Notably NodeJS, CouchDB, and TeaJS. This makes it easier to develop "write once, use anywhere" modules with little or no extra boilerplate.
Will ModularJS Ever Support Any Asynchronicity?
Probably. In fact, pack loading is already asynchronous.
Asynchronous things are awesome, but that doesn't make them perfect for every case. I've heard good arguments for allowing at least partial asynchronicity by having multiple packs and only downloading them if a user activates that part of your web application. A totally valid use case. See the Future Plans section for info what it will probably look like.
Client Side Usage
The following is an example of including modular.js in an HTML file.
Hypothetical Page: http://example.com/foo/index.html
<script src="js/modular.js" data-root="/foo/" data-main="main" data-pack="js/main.pack" data-paths="modules bar/node_modules" data-sandbox data-delay="interactive" data-mode="production"></script>
src should end with "modular.js" or "modular.min.js" so that ModularJS can find its own script tag. If you need to rename it to something else, you can set the script id attribute to "ModularJS" and it will be found that way.
data-root is optional and sets the virtual root. The virtual root defaults to the path of the page with the modular.js script tag. If your web page is at http://example.com/foo/index.html, then your default virtual root would be /foo/. You could force it back to / by specifying / or ../ as the virtual root, but this would mean that your index.html file would always have to be in a directory named "foo" to avoid having to recompile packs.
Modules cannot be loaded from outside the virtual root because module absolute IDs are calculated relative to the virtual root. This is a portability feature that allows changes to the directory structure above the virtual root to have no effect on module IDs in your app.
data-main is required and requires the first and main module. The name of the main module follows the same rules as all CommonJS module names. It can be relative or top-level, is / separated, and technically should be camel case (but nobody follows that last part). If the main module is relatively or absolutely named, it will be resolved relative to the virtual root.
A main module is required so that ModularJS doesn't have to polute the global scope and so that all code runs in a similar environment. It also provides an entry point for the compiler to determine what modules should be built into a pack.
When using a pack, main module will usually be the same one that you passed to the compiler. That's not strictly necessary if you have multiple valid entry points.
data-pack is optional and references a set of modules compiled into a pack by the compiler. This is NOT a module name. It should be a URI like the src attribute. Note in the above example that the pack does not have a .js extension. This means that the pack file does not have a .js extension.
data-paths is optional and sets the top-level module directories. Multiple paths can be space separated. If no paths are set then top-level module ids will be resolved relative to the virtual root. In the above example, the main module may be resolved to http://example.com/foo/modules/main.js or http://example.com/foo/bar/node_modules/main.js based on the two paths given. If no paths were given then the main module would have to resolve to http://example.com/foo/main.js.
Note that paths both relative and absolute are relative to the virtual root!
data-sandbox turns on sandboxed module mode when present. The uri property will not be present on module objects and the paths property will not be present on require when sandbox mode is enabled, as per the CommonJS modules spec.
data-delay prevents main module execution until the DOM is ready, and optionally until all resources (images, etc.) have also loaded. The default delay is "none". If this attribute is present but has no value, then the value defaults to "interactive".
- "none" means that the main module will be executed as soon as possible.
- "interactive" means that the main module will wait until the DOM is ready, specifically until document.readyState === "interactive". This is identical to the jQuery.ready callback.
- "complete" waits for the DOM to be ready and all resources to have been downloaded (AKA document.readyState === "complete").
data-mode specifies a mode in which to run. The default mode is "mixed", but "development" and "production" are also options.
- "mixed" means that if a required module is available in a pack, it will be used instead of requesting the module from the server. If the module isn't in an available pack, it will be requested from the server.
- "development" means that all packs will be ignored and all modules will be loaded as required from the server.
- "production" means that required modules will never be loaded from the server. If a module is not found in a pack, an exception will be thrown.
NOTE: Scripts in other script tags will not be affected by ModularJS. ModularJS does not add to the global scope at all, though modules themselves are free to do so.
Here is the minimum you need to use ModularJS on a page:
<script src="modular.js" data-main="main"></script>
As an alternative to using data-* attributes on the script tag, you can define a ModularJS object variable before the modular.js script tag and its properties will be used as options. Properties on the ModularJS object will override matching data-* attributes on the script tag. The following example is equivalent to the first example in this section.
<script>
var ModularJS = {
pack: 'js/main.pack.js',
main: 'main',
paths: ['modules', '/bar/node_modules'],
sandbox: true,
delay: 'interactive',
mode: 'production'
};
</script>
<script src="js/modular.js"></script>
Server Side Compiling
The ModulerJS Compiler is a NodeJS utility which will compile a main module and all required modules into a module "pack". Please see the compiler's home page for more information.
http://bluejeansandrain.github.com/modularjs-compiler/
Installation:
npm install modularjs-compiler -g
Usage:
modularjs [options] main-module-id [extra-modules ...]
FAQ
Q: Why am I seeing 404 errors?
A: If you have two or more paths and a module is not in the first one, then you will see a 404 error for each path the module was not found in. In development this is expected. Compiling should stop it from happening. This is one reason why uncompiled is not a recommended mode for production.
Q: I compiled my code, but I still see the browser making requests for the server for each module?
A: Make sure you used the correct virtual root when you compiled. All module IDs are resolved to client side absolute IDs, which means that the compiler on the server must understand where the virtual root should be so that it can resolve them the same way. For example, if your page is at at http://foo.com/bar/index.html, then make sure the compiler virtual root is equivalent to http://foo.com/bar/.
Future Plans
Child Packs
Web apps are growing larger and larger, so it could definitely be desirable not to load an entire app all at once.
The future solution will probably be a built-in, top-level module called "modularjs-pack" that can be required and used to asynchronously load a child pack very much like the script tag can be used to require an initial pack. It will probably look something like this:
var pack = require( "modularjs-pack" );
pack( '/path/to/a/child.pack', 'entry-module', function( entry-module-exports )
{
Do something with your entry module exports, or not.
});
It should behave almost like a seperate web-app, with just this point of contact with the parent app. The uri and entry module would be required. The callback would be optional, but useful to connect the two apps. It will not be meant as an asynchronous alternative to require.
If your child pack is just a collection of late loaded modules and doesn't really need an entry point, then your entry module might look something like this:
if ( false )
{
require( 'a' );
require( 'b' );
...
}
This way, the compiler would recognize that the module depends on the 'a' and 'b' modules, but the entry point would never actually require them.
The modularjs-pack module may also have something like an isLoaded method which would allow code to conditionally execute based on whether the entry point module has been required yet. It would look something like this:
if ( pack.isLoaded( 'entry-module' ) )
{
// Do pack dependant stuff.
}
The reasoning behind making this a "built-in" module that can be required rather than say attaching a pack method to require, is that it becomes much easier to backfill the functionality in a server side JavaScript engine that already has require. You just need an actual module named "modularjs-pack".