Why SeaJS

or What a wonderful world with module loader

Okay, You are a decent Frontend Engineer. After finished books like JavaScript: the Good Parts and JavaScript Web Applications, you probably write js like this:

// wrap my awesome code in an anonymous function call, get clousured!
// yeah, you heared me.
(function() {

    var brian_said = 'hello world',
        ritchie_said_also = 'konicuwa world?';

    // more code
})();

Then your project gets bigger, you found yourself should get your code modulized for better trans-page sharing.

// All I need is a tiny little spot to hold my precious data and methods.
// Gollum: My precious...
var Precious = {};

// module one
Precious.mod1 = (function() {
    // no more hello world plz.
    return {
        // exports methods
    }
})();

// module two
Precious.mod2 = (function() {
    // how about ohayo js?
    return {
        // exports methods
    }
})();

Those pages of your project differs. Some need module 1, 3, 5, some need module 2, 4, 6. So you split those codes into separate files and load them specifically in the corresponding page. Your page bottom might looks like this:

<!doctype html>
<html>
<head></head>
<body>
    <script>var Precious = {};</script>
    <script src="mod1.js"></script>
    <script src="mod2.js"></script>
    <script src="mod3.js"></script>
</body>
</html>

The requirements of that very project changes, a lot. Page A requires module 1, 3, 5 at first, suddenly in the next day it cries for module 2, 4, 6. Tedious work like changing script tags starting to take up a great portion of your time.

And it gets worse, modules have dependencies. There will be lots of bugs caused by circumstances like page foo requires module blah, so you add it, but blah rquires module tongue and you just forgot.

Starting to think that what the frontend egineering world would be if JavaScript had any module system? SeaJS come rescue.

Meet SeaJS

or How to get your code modulized

SeaJS implements its module system according to CMD, Common Module Definition. But we are not getting there yet. Let’s start from the basics.

First thing first, SeaJS isn’t magic. Like other libraries in the JavaScript world, SeaJS is written in pure JavaScript too.

So you need to load it in your page first.

<script src="sea.js"></script>

And like $ from jQuery, SeaJS is used via global object and function. Namely, seajs and define.

With SeaJS, we can define some module.

define('console', function(require, exports) {
    exports.log = function(msg) {
        if (window.console && console.log) {
            console.log(msg);
        }
        else {
            alert(msg);
        }
    };
});

And use it.

seajs.use('console', function(console) {
    // now you can stop worrying about whether or not `console` is provided.
    // use it freely!
    console.log('hello world!');

    // well, we need to enhance the `console` module a bit.
    // for methods like console.warn, and method calls like console.log(msg1, msg2, msg3);
});

What about module dependencies? Here comes require

define('jordan', function(require, exports) {
    // you can require inline
    exports.championship = function() {
        // Michael and Scottie should be together!
        return require('pippen').hasJoined();
    };

    // or, you can require module in the head.
    var rodman = require('rodman');

    exports.next3 = function() {
        // Dennis is the monster
        return require('rodman').hasJoined();
    };
});

Also, if your module provides nothing but pure Object or String, you can define it directly.

// Object
define({ foo: 1 });

// String
define('hello world');

But we need our code splitted into different files! Hang on. That is exactly what SeaJS is good at.

Asynchronous JavaScript

or Why is SeaJS implemented this way.

Now comes the cliche. The creator of JavaScript, Brendan Eich, implemented it within weeks. The initial goal was to be able to write snippets to make the web more interactive, a little. So, that results in some drawbacks of this language. Dauglas Crockford summarized some of those in the book JavaScript: the Good Parts.

Being lack of module system is one of those drawbacks. Some of you might heared or tried Node.js, that is module support of JavaScript done right.

But there are some interesting facts of JavaScript in the browser land.

  • You can insert a script tag via js with an @src to load js dynamically.
  • You can listen on that script tag’s onload, onerror or oncomplete events.

Well, browsers have there inconsistencies in the event stuff of script tag. But it’s solved in SeaJS! …almost!

With that two fact combined, we can do some thing like this:

  1. Load the bootstrap js
  2. Load different modules accordingly
  3. Perform tasks when those modules were loaded!

That’s what SeaJS is all about.

We load it to bootstrap:

<!-- the library and your app -->
<script src="sea.js"></script>
<script src="app.js"></script>

As a matter of fact, SeaJS also provide a shortcut for bootstraping your app. So instead of two script tags, you can load it via @data-main.

<!-- more compact way -->
<script src="sea.js" data-main="./app"></script>

./ means relative to current page url. Given page url http://foo.com/hello/world.html, SeaJS will load http://foo.com/hello/app.js.

The ./app is called module id in SeaJS. Instead of relative way, you can write module id in absolute path. Well, not so absolute. In this example, hello/app refers to base path + module id + .js.

What is base path? SeaJS uses base to resolve module uri if the module id does not start with an .. It can be configured:

seajs.config({
    base: '/'
});

With base configured, now hello/app will be resolved to http://foo.com/hello/app.js.

When you require module within module, the rules are the same. Relative paths will be solved relative to current module uri.

// http://foo.com/worker/carpenter.js
define(function(require, exports) {
    var hammer = require('../util/hammer');

    exports.nail = function() {
        hammer.smash();
    };
});

That hammer is http://foo.com/util/hammer.js.

The Only difference with Node.js, is that your module code (and your app code) needs to be wrapped in a typical define callback.

define(id, dependencies, function(require, exports, module) {
    // module code.
});

The id and dependencies parameters can be omitted.

id is used mostly when your project went online. All of your modules used in that page gets combined into one file. SeaJS can not guess which modules is which by that script’s @src. Normally you will not need to use that.

dependencies is the other parameter used only in combined js files in normal circumstances. It list out what modules current module is dependent on. But you don’t have to provide that list when you are writing a module. SeaJS will inspect your module callback and find out all of the require statements, and therefore, get all of your module’s dependencies.

That relies on three important rules.

  • You should not rename require
  • You should not rewrite require
  • You can only require('string');. require(<js_statement>) is prohibited offically.

About the first two rules, think requrie as a reserved word in JavaScript programming language. You’re good to go.

On the third however, there’s something interesting to be discussed. SeaJS prohibit usages of require like these:

define(function(require, exports) {
    var mod = require(condition ? 'a' : 'b');
    var mod_name = prefer_singer() ? 'jackson' : 'jordan';
    var celebrity = require(mod_name);
});

SeaJS can not parse those kind of modules from funciton.toString(), because the values are determined only when that function is being executed. If the dependencies is not correctly parsed, require(mod_name) will yield nothing but an error. Hence that module will not be successfully initialized.

But, what if I really want to do it this way? Well, as a matter of fact, there’re two ways of reflecting modules dynamically.

  • declare dependencies manually
  • use require.async

Here’s the demo corrected in the former way

define(['a', 'b', 'jordan', 'jackson'], function(require, exports) {
    var mod = require(condition ? 'a' : 'b');
    var mod_name = prefer_singer() ? 'jackson' : 'jordan';
    var celebrity = require(mod_name);
});

require(mod_name) will yield the correct module now because both jordan and jackson are loaded already.

Here’s the latter way of correction

define(function(require, exports) {
    require.async(condition ? 'a' : 'b', function(mod) {
        // use mod a or b
    });
});

There are pros and cons in both solutions. The differences are:

  1. Modules loaded by require.async will not be combined by any automatic combo tools, because no one knows what that module actually is by static parsing.
  2. If your code needs to be executed serially, require.async won’t be an option.

But the former one isn’t the one true answer. Here’s my advice. If your module is big, it’s data rather module, use require.async. If all of the possible modules will be used eventually, just load them all in your dependencies parameter.

Dive Into SeaJS

or How to handle complex code architecture.

We will be writing a hello world generator, which says hello in different programming languages. Let’s define those modules first.

lang/ruby.js

define('' +
    '#!ruby' +
    'puts "Hello, #{' + msg + '}"'
);

lang/js.js

define('' +
    '(function(console) {' +
    '    console.log("Hello", msg);' +
    '})(window.console || {' +
    '    log: function() {}' +
    '});'
);

lang/lisp.js

define('' +
    '(print "Hello, ' + string.escape(msg, ['"', '\\']) + '")'
);
<!doctype html>
<html>
<head></head>
<body>
    <pre id="output" data-lang="ruby"></pre>
    <script src="sea.js" data-main="./generator"></script>
</body>
</html>

the generator should be able to perform these tasks, in specific order:

  1. read @data-lang from pre tag.
  2. determine the corresponding language module, and get the result.
  3. shove the result back into the pre tag.
seajs.use('./util/html', function(HTMLUtil) {
    var pre = document.getElementById('output'),
        lang = pre.getAttribute('data-lang');

    seajs.use('./lang/' + lang, function(lang) {
        pre.innerHTML = HTMLUtil.escape(lang.hello());
    });
});

Now back to the problem raised in the begginning. We can solve it with the knowledge of SeaJS so far. Every page should have one and only one script tag, which just differs in the @data-main attribute.

<!-- page a -->
<script src="sea.js" data-main="./page-a"></script>

<!-- page b -->
<script src="sea.js" data-main="./page-b"></script>

Those (function(){})(); modules can be organized via define in separated files. use require for inner module inclusion. Then page a, b, c .js files can be written like this.

seajs.use(['mod1', 'mod2', 'mod3'], function() {
    // ah my awesome code.
});