Blog-Archiv

Samstag, 21. Februar 2015

JS Requires Dependency Management

When you go shopping, you need money. When you go there by car, you also need your driving license. Taking both with you is called dependency management :-)

Every programming language has some kind of dependency management, C has include statements, Java has import statements. In JavaScript such does not exist. There are JS libraries that fill the gap and provide means to state the dependencies of a function call. One of them is requireJs.

require.js has the reputation to be a "real" dependency manager for JS. It features AMD (asynchronous module definition). A "module" is some JS object or function. I did not find out what exactly "Asynchronous definition" means and what's its target, but in fact, with requireJS, it does not mean that the browser loads every module on demand only, this would be ways too slow, they feature an optimizer.
There are only two functions that you need to know when working with requireJs: define() and require().
With about 2000 lines of code it is a decent JS library. It can work together with most major libraries like jQuery, although these were not designed as AMD modules.
Here is a link to the tutorial, and here is a link to their design considerations (in case you plan to write a better script loader you should read this first :-).

Declaring Dependencies

When you write JS code in context of require.js, life changes drastically:

  • you define only one function or object in one file.js
  • you enclose any such function or object into a define() call ("module definition")
  • you actively call a dependent function through a require() wrapper where you pass dependencies explicitly
  • you name dependencies as relative paths (without .js extension) in a string array
  • you name any of those dependencies then once more as a function parameter.

Let me show you how this feels in a very simple example.

Example (Specification)


The example will use (and thus depend on) ...

  • a module that provides a function that can output text in a dialog, and
  • a module that provides an object holding several text messages,

... and it will show one message in a dialog and another one as page title, both done on load.


Directory Structure

  • example
    • demo.html
    • js
      • main.js
      • app
        • dialog.js
        • messages.js
      • lib
        • require.js   // the downloaded library

requireJs does not force a certain directory structure, but it recommends to keep JS files in according directories. The require.js file is in lib directory because it is an external library. The app directory holds the example application files.

HTML

This is demo.html:

<!DOCTYPE html>
<html>
<head>
  <title>RequireJs Example</title>
</head>

<body>

  <script type="text/javascript" data-main="js/main" src="js/lib/require.js"></script> 

</body>
</html>

The data-main attribute in the <script> tag is the requireJs entry point. It will append .js to the value of that attribute when necessary, and then load that file, interpreting the path to be relative to the HTML page.

So we will need to implement the logic specified above in js/main.js.

Modules

requireJs expects at least a function or an object to be passed to any define() call. If it is a function, it will be called and its return considered to be the module, else the given object is the module. The module is then stored under the name of the file where it was defined. A module will be loaded only once. Thus a module implicitly is a singleton.

dialog.js

define(
  function() {
    return function(message) {
      alert(message);
    };
  }
);

This uses the built-in JS alert() dialog, supported by all browsers. A function is passed to requireJs, returning another function that will be the module.

messages.js

define(
  {
    infoHead: 'INFO',
    hello:  'Hello World',
    goodBye: 'Good Bye World'
  }
);

This passes an object to requireJs, providing some text messages to the user of the module.

Script

main.js

require(
  [
    "app/messages",
    "app/dialog"
  ],
  function(
      msgs,
      print)
  {
    print(msgs.hello);
    
    var paragraph = document.createElement("DIV");
    paragraph.innerHTML = "<b>"+msgs.goodBye+"</b>";
    paragraph.style.cssText += "text-align: center; font-size: 200%;";
    document.body.appendChild(paragraph);
  }
);

This is a call to the library function require(). First parameter is an array holding relative paths of used module files (names without .js extension, these are module names, it would not work with .js):

  • "app/messages"
  • "app/dialog"

Paths are relative to main.js.

Second parameter is the function to execute, declaring one parameter for each "imported" module:

  • msgs
  • print

The number and order of parameters must match that of dependency array members. Any module object or function can then be used by the name of the according parameter.

Generally I don't like parameter names like "msgs", but in this case I used it to demonstrate that the name of the parameter is not related to the name of the imported module (in world full of naming conventions we need to know this). Nevertheless you find this parallelism in most examples on the internet, and it is a good style. Always use long names, never something like "msgs", such code smells.

Result

Loading demo.html in a browser you should see an alert-dialog saying "Hello World" first, and then this:

Good Bye World


Of course requireJs has much more capabilities. A very important one I must add here. You can also declare dependencies in a module definition, so that every module is bound to the modules it depends on. For example, when dialog.js needs to use messages.js, it would look like the following:

dialog.js

define(
  [
    "app/messages"
  ],
  function(appMessages) {
    return function(message) {
      alert(appMessages.infoHead+": "+message);
    };
  }
);

This binds the app/dialog module to app/messages module. Now the dialog() function would display an infoHead() in front of any message. Mind that I used the full relative path, although messages.js and dialog.js are in the same directory. Alternatively I could have written "./messages", this works, but "messages" would not work, as specified by AMD.

Consequences

Path Duplications

There is no way for requireJs to figure out a server directory structure when running in a client browser. Thus relative module paths will exist hard-coded in JS source code, meaning that you have to edit code when you change the directory structure. This might become a maintenance problem in big projects, but is always better than to manage the paths within HTML script-tags.

One File, One Object or Function

Associating file names with module names makes it necessary to define just one module per file. Surely a module could return an object holding several functions and other objects. But generally the number of JS files would increase when working with requireJs. Which is not JS tradition, but it's time that it becomes one :-)

Mind that it is also possible to give a name to a module that is different from the name of the file where it is defined. The optional first parameter to define() is the name of the module.

Tree Structure in code, Tree Structure in Dependency Declarations

  • Every JS code establishes a tree structure by defining nested functions and objects.
  • Every dependency declaration is part of an (invisible) dependency tree.

Thinking of JS code, a lot of nestings come to my mind, and indentation levels up to 20 and even more, and consequently blurred visibility scopes. Would be nice to see these indentation levels get smaller and being moved to the dependency tree. This would also increase code encapsulation (not polluting global namespace).

But together with the one-file-one-module philosophy a problem rises: how many files will the browser have to load from server? Performance will suffer when modules are distributed over hundreds of files. Therefore requireJs offers an optimizer that will organize all scripts of a page into one big file. Hm - so what was it about loading JS on demand only?

Function or Object?

You never know what you get through the dependency declaration: is it a function or an object?
Dereferencing a function by foo.bar(); most likely will result in an error. Calling an object as foo(); will do the same.

Best would be to introduce a convention that a module always is an object. That way also constants related to its functionality could be defined within the module.

Summary

Here is a link to a longer introduction about AMD, Common-JS and the problem in general.

Here is a link to a tutorial how to use requireJs together with jQuery.

Maybe the dependency problem will be solved by EcmaScript 6. Read this Blog about what can be expected.

Personally I am a little stunned about the fact that AMD features script loading on-demand-only, while requireJs provides an optimizer to organize all modules into one big JS file. In case "Asynchronous Definition" is meant for future applications, in future most likely there will be some browser-built-in script loader. Or such code will be generated only, driven by Dart or TypeScript or some other language that is better than the widely criticized JavaScript.



Keine Kommentare: