Blog-Archiv

Samstag, 28. Februar 2015

Yet Another JavaScript AMD Loader

JavaScript dependency management via AMD is something that will stay. At least in our memories. It is an impressive insight into functional thinking.

The question rises "How do they do that?", provide dependency management by means of the language itself, not by some built-in mechanism?

On the requireJs page you can read about the basic ideas how to load scripts by JS. RequireJs itself uses adding script tags to HTML page head. RequireJs has about 2000 lines of code, and it is a standard library.

I was curious how many lines of code would be needed to have a basic AMD loader. Here are my sketches. My target was understandable source code. No hasOwnProp(), isFunc() etc.

WARNING: This article presents an AMD loader that really works, but it has restrictions:

  • you can not have more than one define() call per HTML page (put together all JavaScript of a page into just one initialization define())
  • you can not have more than one define() call per module file

Any "imperative" call to define(), after the first call, could confuse the loader in that it does wrong module-name associations for scripts that call define() because they were loaded as modules ("non-imperative" call).

The core problem is the fact that you can not associate a name to a module that calls define() except by an onload listener that holds the expected name. This listener is called asynchronously after the script has been executed. In the meantime, an "imperative" call to define() would put a wrong module into the variable the listener is waiting for its expected name.

Building Blocks

Load a Script using JS

Here is a JS implementation that adds a script tag to HTML head and registers a listener for finished-loading.

Mind that this does not work on IE-8 and older due to missing addEventListener. My aim is good readable code, not solutions that work on every browser. Understandable JS code is rare, so enjoy it :-)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
  var loadScript = function(scriptUrl, scriptLoadedCallback) {
    var scriptElement = document.createElement("script");
    
    var finishedLoading = function(event) {
      if (event.type === 'load' || // seen in require.js, checks if script is loaded
            ("/^(complete|loaded)$/".test((event.currentTarget || event.srcElement).readyState)))
      {
        scriptElement.removeEventListener('load', finishedLoading, false);
        if (scriptLoadedCallback)
          scriptLoadedCallback(scriptUrl);
      }
    };
      
    scriptElement.addEventListener('load', finishedLoading, false);
    scriptElement.type = "text/javascript";
    scriptElement.src = scriptUrl;
    
    document.head.appendChild(scriptElement);
  };

It is fun to write readable source-code. Hard to read is the part where the event is checked whether it is a finished-loading event. I took this from requireJs, and tried to make it comprehensible by commenting it.

Try this out, it actually works. Although it is readable code :-)

But most likely it won't work with an external URL like http://site/beautiful/script.js. Most browsers will not let you load scripts from external sites through JavaScript. This is called cross-site-scripting and is a security hazard. Only URLs relative to the HTML page will work through JavaScript, like e.g. js/app/main.js. Nevertheless you can add an external script-URL to the HTML page in the traditional way.

Load Dependencies Recursively

Caused me some effort. Loading a script is nice, but this script could load other scripts, which again could load ... a recursive task. A dependency tree, hopefully without cycles. Recursive thinking always takes some time.

Here is an example of what is to be done. This "module" defines one dependency, and then gives a factory function which creates the module when executed. The anonymous function(message) will be the module finally. The dependency "app/messages.js" must be loaded and passed as parameter to the factory function function(appMessages). This is the AMD way to import modules.

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

Why is there a factory function? Wouldn't it be better to immediately return the function that should represent the module?
The AMD specification does not give reasons for that, but I guess it is there to give the caller a chance to configure the module before actually executing some functionality of it.
In this case the alert dialog uses a message from the parameter of the outer factory function.

And here is a way to implement define(), in a hopefully readable manner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  var currentModule;

  var define = function(dependencies, moduleFactory) {
    var parameters = [];
    
    if (dependencies.length) // the first parameter is an array
      parameters = evaluateParametersFromDependencies(dependencies, moduleFactory);
    else if ( ! moduleFactory ) // happens when no dependencies given
      moduleFactory = dependencies; // parameter in fact is the moduleFactory
    else
      throw "Dependencies must be array of strings, but was: "+dependencies;
    
    var dependenciesResolved = (parameters !== null);
    if (dependenciesResolved)
      if (typeof moduleFactory === "function")
        currentModule = moduleFactory.apply(context, parameters);
      else
        currentModule = moduleFactory;
    else
      currentModule = undefined;
  };

This allows only two parameters, the dependencies string array and the module (which is different from the AMD specification that allows the module name as first of three parameters).

If the dependencies are all present, define() calls either the factory function, or it takes the given object as module. It puts the module, whatever it is, into the instance variable currentModule. But if a null parameter array is returned from evaluateParametersFromDependencies(), the module is set to undefined.

Let me explain later what happens with the variable currentModule. Here is an implementation of evaluateParametersFromDependencies().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  var modules = {};
  var unresolved = [];
  var currentDependant = "main.js"; // to be initialized on load

  var evaluateParametersFromDependencies = function(dependencies, moduleFactory) {
    var parameters = [];
    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (typeof dependency === "string") { // dependency is an identifier
        var loadedModule = modules[dependency]; // try to get it from modules map
        if (loadedModule) { // is present
          parameters[i] = loadedModule; // adopt it as parameter
        }
        else { // must load module
          unresolved.push({ // schedule this for later
            dependant: currentDependant,
            dependencies: dependencies,
            moduleFactory: moduleFactory
          });
          loadDependency(dependency);
          return null; // cancel define() as this is going asynchronous
        }
      }
      else {
        parameters[i] = dependency; // simply pass through anything else than string
      }
    }
    return parameters; // respond to calling define() with valid parameters
  };

This function evaluates the parameter array from dependencies. Its arguments are exactly the same as those of the calling define().

In best case all parameters can be evaluated to already loaded modules, then the define will succeed.
When one of the dependencies has not yet been loaded, it returns null, and the define will cancel. But before returning null it pushes the unresolvable definition onto a stack and calls loadDependency() for the missing dependency. This adds a script tag and thus goes asynchronous. Meaning the JS execution is over for now, and the browser will call us when the script is loaded.

Here is an implementation of loadDependency().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  var loadDependency = function(dependency) {
    loadScript(dependency, function() { // finished-loading callback
      if (currentModule) { // dependency was loaded successfully
        modules[dependency] = currentModule; // store the module to map

        while (currentModule && unresolved.length > 0) { // try to define dependants
          var toResolve = unresolved.pop();
          currentDependant = toResolve.dependant;
          define(toResolve.dependencies, toResolve.moduleFactory);
          if (currentModule)
            modules[currentDependant] = currentModule;
        }
      }
      // else: another finished-loading callback will continue
    });
  };

The finished-loading callback does nothing when the definition of the dependency is delayed to a subsequent dependency (currentModule undefined). But when the dependency could be defined, it

  • puts the new module from currentModule variable into the modules map
  • resolves queued definitions by calling define() as long as modules can be created

The stack is the recursion that processes the whole dependency tree.

Because only one script tag is added to page head at a time, no concurrency problems around currentModule can occur.

That's all, from these building blocks an AMD loader can be implemented.
Surely some more logic and convenience will be needed, especially concerning script URLs and module names, and to detect cyclic dependencies.

All Together Now

Here comes my solution with an example. It should work in all browsers except IE-8 and older.

Strategy:

  1. browser loads the define.js script through an HTML script tag
  2. that script loads the script referenced in data-main attribute
  3. the first define() call there triggers dependency resolution
  4. any define() call always checks the modules map for dependencies already loaded before actually loading one
  5. any CSS will be loaded without waiting for its arrival (CSS is recognized by extension .css)
  6. any JS will be loaded with a registered finished-loading listener, but only the first JS dependency will be loaded, not all
    • the current dependency parameters are pushed onto a stack
    • then the running define() is aborted
  7. the dependency calls define() as soon as it was loaded
  8. when it has dependencies, it goes the same way as 3. went, but when not, it puts its module into the global currentModule variable
  9. the finished-loading listener checks whether a module was defined and puts the currentModule into global modules map when defined
  10. the finished-loading listener again calls define() with the dependency parameters taken from stack, actually starting from 4. again.

Features:

  • everything is a define(), no require() here
  • checks for dependency cycles and throws an error in such a case
  • can load CSS
  • can load JS that was not written as AMD define; to avoid global variables completely, use the define.getLoadedModule override like shown in demo.js
  • dependency module names are paths relative to the data-main script (demo.js), or, when data-path was declared, relative to that path (see demo.html example below)
  • data-main attribute in demo.html points to the main script
  • data-allow-cyles attribute enables loading when the dependency graph has cycles (default does not allow)
  • data-logging attribute switches on logging (default is off)
  • go to some JS minifier and compress it, it has 2395 bytes

You can not load external scripts with this loader! It is just for resources relative to the calling HTML page.


What is the result of the example page below?
A centered title "INFO: Hello World" should be displayed on the page, below will be the HTML source of the page after all modules were loaded, that source is syntax-highlighted by highlight.js, then a dialog should appear saying "INFO: Hello World", and finally "INFO: Good Bye World" should appear as footer.
This is what is implemented in demo.js.

Example Directory Structure

  • html
    • demo.html
    • scripts
      • demo.js
  • js
    • app
      • messages.js
      • ui
        • dialog.js
    • lib
      • define.js
      • highlight.min.js
    • util
      • output.js

The js directory is the JavaScript library root. With this AMD loader you will be forced to hold all your JS files under that root and reference it from all your web pages by relative paths.

Example Files

html/demo.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>

<body>

  <script type="text/javascript"
      src="../js/lib/define.js"
      data-main="scripts/demo.js"
      data-path="../js/"
      data-logging="true"
      data-allow-cycles="false"
  >
  </script> 

</body>
</html>

html/scripts/demo.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Script referenced in "data-main" attribute.
 */

define(
  [
    "app/ui/dialog.js",
    "util/output",    // trailing .js is optional
    "app/messages.js",
    "lib/highlight-default.min.css",
    "lib/highlight.min.js"
  ],
  function(
      print,
      out,
      msgs)
  {
    out(msgs.hello);
    
    var scriptCode = document.createElement("pre");
    scriptCode.textContent = document.documentElement.outerHTML;
    document.body.appendChild(scriptCode);
    scriptCode.setAttribute("class", "html");
    hljs.highlightBlock(scriptCode);
  
    print(msgs.hello);
    
    out(msgs.goodBye);
  }
);

This variant is using the global variable hljs.
If you want to avoid global variables in any case, use the one to the right.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
 * This is for those who dislike global variables.
 * Pass highlightjs, which is not an AMD module,
 * to the main script as an argument (hljs)
 * by overriding the define.getLoadedModule() function.
 */
(function() {
  var getLoadedModule = define.getLoadedModule;

  define.getLoadedModule = function(dependency) {
    if (dependency === "lib/highlight.min") // dependencies do not have ".js"
      return hljs;
    return getLoadedModule(dependency);
  };
}());

define(
  [
    "app/ui/dialog.js",
    "util/output",
    "app/messages.js",
    "lib/highlight-default.min.css",
    "lib/highlight.min.js"
  ],
  function(
      print,
      out,
      msgs,
      cssDummy,
      hljs)
  {
    out(msgs.hello);
    
    var scriptCode = document.createElement("pre");
    scriptCode.textContent = document.documentElement.outerHTML;
    document.body.appendChild(scriptCode);
    scriptCode.setAttribute("class", "html");
    hljs.highlightBlock(scriptCode);
  
    print(msgs.hello);
    
    out(msgs.goodBye);
  }
);

js/app/messages.js

define(
  /* comment this in to test cycle detection
  [
    "app/ui/dialog"
  ],
   */
  {
    infoHeader: 'INFO',
    hello:      'Hello World',
    goodBye:    'Good Bye World'
  }
);

js/app/ui/dialog.js

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

js/util/output.js

define(
  [
    "app/messages"
  ],
  function(appMessages) {
    return function(message) {
      var paragraph = document.createElement("div");
      paragraph.innerHTML = "<i>"+appMessages.infoHeader+":</i> <b>"+message+"</b>";
      paragraph.style.cssText += "text-align: center; font-size: 200%;";
      document.body.appendChild(paragraph);
    };
  }
);

js/lib/highlight.min.js
js/lib/highlight-default.min.css

Download both files from highlightjs website.


The Loader Implementation

js/lib/define.js

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
"use strict";

/**
 * This is a very basic AMD script loader.
 * It does not load scripts concurrently (although it loads asynchronously).
 * It has no require() function, just define(dependencies, moduleOrFactory).
 * It does not allow to pass the module name as first parameter to define().
 * The extension ".js" in module names is optional.
 * 
 * CAUTION:
 * Call define() only once from a HTML page!
 * The loader assumes that define(), after the initial call, is called
 * by modules only. Calling it a second time could break module assignment.
 * The same could happen when calling define() more than once from a module!
 * 
 * See AMD specification at https://github.com/amdjs/amdjs-api/wiki/AMD
 * 
 * @param context the execution context for any factory-function given by a module,
 *     in other words this will be the first parameter to factoryFunction.apply().
 */

var define = (function(context)
{
  /* Base URL of the directory where modules are in, set by data-path attribute. */
  var baseJsUrl = "";
  
  /* The currently loaded module as left by define(), object or function. */
  var currentModule;
  var currentModuleName = "anonymous script";
  
  /* Module map with key = script path, value = loaded module. */
  var modules = {};
  
  /* List of modules still to load. */
  var unresolved = [];
  
  /**
   * This is the returned define() implementation. It resolves given dependencies
   * and stores the defined module into currentModule variable when no
   * unresolved dependencies existed, else leaves it undefined and defers
   * module definition until resolution of the depencency (asynchronous).
   * @param dependencies array of String containing the module identifiers
   *     this module depends on.
   * @param moduleOrFactory an object representing the module, or a factory function
   *     creating a function or object representing the module,
   *     can also be undefined to just load some scripts.
   */
  var define = function(dependencies, moduleOrFactory) {
    var loadingModuleName = currentModuleName;
    log("Define called by '"+loadingModuleName+"'");
    
    var parameters = [];
    if (dependencies.length) /* the first parameter is an array */
      parameters = evaluateParametersFromDependencies(dependencies, moduleOrFactory);
    else if ( ! moduleOrFactory ) /* happens when no dependencies given */
      moduleOrFactory = dependencies; /* parameter in fact is the module */
    else
      throw "Dependencies must be array of strings, but was: "+dependencies;
    
    var dependenciesResolved = (parameters !== undefined);
    if (dependenciesResolved)
      if (typeof moduleOrFactory === "function")
        currentModule = moduleOrFactory.apply(context, parameters);
      else
        currentModule = moduleOrFactory;
    else
      currentModule = undefined;
  
    log("Define of '"+loadingModuleName+"' "+(dependenciesResolved ? "finished" : "was delayed"));
  };

  /**
   * Tries to pick up all dependencies from module map and returns a ready-made
   * parameter array when this succeeds, else returns undefined and asynchronously
   * loads the dependency.
   * @param dependencies same as in define().
   * @param moduleOrFactory same as in define().
   * @returns null when not all modules are present, else a parameter
   *     array containing all dependencies as modules.
   */
  var evaluateParametersFromDependencies = function(dependencies, moduleOrFactory) {
    log("Module '"+currentModuleName+"' has dependencies "+dependencies);
    
    var parameters = [];
    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (typeof dependency === "string") { /* dependency is an identifier */
        dependency = cutExtension(dependency);
        var loadedModule = modules[dependency]; /* try to get it from modules map */
        if (loadedModule) /* is present*/
          parameters[i] = loadedModule; /* adopt it as parameter */
        else if (continueDeferred(dependency, dependencies, moduleOrFactory))
          return undefined; /* will be continued by a finished-loading event */
        else
          parameters.push({}); /* dummy module when finished-loading can not be caught */
      }
      else {
        parameters.push(dependency); /* simply pass through anything else than string */
      }
    }
    return parameters; /* respond to calling define() with valid parameters */
  };
  
  var continueDeferred = function(dependency, dependencies, moduleOrFactory) {
    var url = baseJsUrl + dependency;

    if (dependency.lastIndexOf(".css") >= 0) {
      loadStyle(url);
      modules[dependency] = {}; /* dummy module for CSS */
    }
    else { /* is a JavaScript */
      var schedule = { /* schedule for later */
          moduleName: currentModuleName,
          dependency: dependency,
          dependencies: dependencies,
          moduleOrFactory: moduleOrFactory
      };
      if (noCycle(schedule)) {
        unresolved.push(schedule);
        loadScriptDependency(dependency, url);
        return true; /* cancel define() as this is going asynchronous */
      }
    }
    return false; /* do not wait for finished-loading */
  };

  var noCycle = function(schedule) {
    for (var i = unresolved.length - 1; i >= 0; i--) {
      if (unresolved[i].dependency === schedule.dependency) {
        var message = "ERROR: Cycle detected, or more than one define() calls, when trying to load "+schedule.dependency;
        for (var j = unresolved.length - 1; j >= i; j--)
          message += " after "+unresolved[j].dependency;
        
        log(message);
        if (define.allowCycles)
          return false;
      
        /* throw exception and prevent subsequent work */
        unresolved = [];
        throw message;
      }
    }
    return true;
  };
  
  /**
   * Loads given dependency asynchronously, installs a finished-loading listener.
   * Resolves unresolved modules when loading finished and a module was created.
   * @param dependency the identifier of the module to load.
   * @param scriptUrl the URL from where to load the dependency, with prepended base-URL.
   */
  var loadScriptDependency = function(dependency, scriptUrl) {
    if (scriptUrl.lastIndexOf(".js") < 0)
      scriptUrl = scriptUrl+".js"; /* append extension when necessary */

    currentModule = undefined; /* for the case that define() won't be called */
    currentModuleName = dependency;
    var currentUnresolved = unresolved.length; /* remember current state */
    log("Deferring to receival of script: "+dependency);
    
    loadScript(scriptUrl, function() { /* finished-loading callback */
      /* define() should have been called by the module, currentModule should have a value */
      log("Finished loading "+dependency);
      
      /* when no unresolved dependency was pushed, and also no module was defined,
         map a dummy module so that loading can continue */
      if ( ! currentModule && currentUnresolved === unresolved.length)
        currentModule = define.getLoadedModule(dependency);
      
      putModuleAndResolve(dependency); /* try to pop unresolved */
    });
  };
  
  var putModuleAndResolve = function(dependency) {
    if (currentModule) { /* a module was loaded successfully */
      modules[dependency] = currentModule; /* store the module to map */
        
      if (unresolved.length > 0) {
        var toResolve = unresolved.pop();
        currentModuleName = toResolve.moduleName;
      
        define(toResolve.dependencies, toResolve.moduleOrFactory);
      
        putModuleAndResolve(toResolve.moduleName);
      }
    }
    /* else: define() had to load another dependency */
  };

  /**
   * Loads a script by appending a script element to HTML document head
   * and registering a load-finished callback. This is asynchronous.
   * @param scriptUrl the script to load.
   * @param scriptLoadedCallback the function to call when loading finished, can be null.
   */
  var loadScript = function(scriptUrl, scriptLoadedCallback) {
    var scriptElement = document.createElement("script");
    
    var finishedLoading = function(event) {
      if (event.type === 'load' || /* checks if script is loaded, seen in require.js */
            ("/^(complete|loaded)$/".test((event.currentTarget || event.srcElement).readyState)))
      {
        scriptElement.removeEventListener('load', finishedLoading, false);
        if (scriptLoadedCallback)
          scriptLoadedCallback();
      }
    };
      
    scriptElement.addEventListener('load', finishedLoading, false);
    scriptElement.type = "text/javascript";
    scriptElement.src = scriptUrl;
    
    document.head.appendChild(scriptElement);
  };
  
  /**
   * Loads a CSS file by appending a link element to HTML document head
   * and registering a load-finished callback. This is asynchronous.
   * @param styleUrl the CSS file to load.
   */
  var loadStyle = function(styleUrl) {
    var linkElement = document.createElement("link");
    linkElement.type = "text/css";
    linkElement.rel = "stylesheet";
    linkElement.href = styleUrl;
    document.head.appendChild(linkElement);
  };
  
  var cutExtension = function(dependency) {
    var extensionIndex = dependency.lastIndexOf(".js");
    if (extensionIndex > 0)
      return dependency.substring(0, extensionIndex);
    return dependency;
  };
  
  var log = function(message) {
    if (define.logging && typeof console !== "undefined")
      console.log(message);
  };


  /* initialize, executed once when this factory function is called */
  
  (function() {
    /* find "data-main" and other attributes in document */
    var scripts = document.getElementsByTagName('script');
    
    var mainAttributeValue;
    var loggingAttributeValue;
    var allowCyclesAttributeValue;
    for (var i = 0; ! mainAttributeValue && i < scripts.length; i++) {
      var script = scripts[i];
      mainAttributeValue = script.getAttribute("data-main");
      loggingAttributeValue = script.getAttribute("data-logging");
      allowCyclesAttributeValue = script.getAttribute("data-allow-cycles");
      baseJsUrl = script.getAttribute("data-path");
    }
  
    if (mainAttributeValue) {
      if ( ! baseJsUrl ) {
        var lastSlashIndex = mainAttributeValue.lastIndexOf("/") + 1;
        baseJsUrl = mainAttributeValue.substring(0, lastSlashIndex);
      }
      loadScript(currentModuleName = mainAttributeValue); /* is asynchronous */
    }
    
    if (baseJsUrl && baseJsUrl.length > 0 && baseJsUrl.substring(baseJsUrl.length - 1) !== "/")
      baseJsUrl = baseJsUrl+"/";
  
    define.logging = (loggingAttributeValue === "true") ? true : false;
    define.allowCycles = (allowCyclesAttributeValue === "true") ? true : false;
  
    log("main="+mainAttributeValue);
    log("path="+baseJsUrl);
    log("allowCycles="+define.allowCycles);
    
    /**
     * Default fallback for loaded non-AMD modules, to be overridden.
     * @param dependency the non-AMD script loaded, without trailing ".js".
     * @return the loaded module object or function to store to modules map.
     */
    define.getLoadedModule = function(dependency) {
      if (dependency.indexOf("jquery") >= 0 && typeof $ !== "undefined")
        return $;
      return {}; /* empty dummy object */
    };
  }());
  
  
  return define; /* factory result */
  
}(typeof window !== "undefined" ? window : this)); /* pass a global JS context */

I hope I demystified AMD a little, and showed how JS code can be written in a readable and well-documented manner. This is what we need: simple and clear solutions. Long names for variables and functions. Header comments for modules, functions and instance variables. Code for humans, not for interpreters.

Have fun!



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.



Freitag, 13. Februar 2015

Interrupted LINUX Upgrade to Ubuntu 14.04

My netbook was out of time, having Ubuntu 12.04. (Isn't that incredible old ?-)
I decided to "hop" to 14.04. The update-tool had invited me so often. Now I made the fatal click on "Upgrade".

During the upgrade I dared to open the system monitor, just to see how much space 12.04 had occupied on disk. The monitor came up and then froze, then the window decorations (title bar, borders) disappeared from all open windows. The installation still worked and showed progress.

Then I dared to restart the window manager (which is responsible for window decorations). In LINUX, when you press Alt-Ctl-F1, you get to a command-line terminal. "lightdm" is the Ubuntu window manager. I logged in as root, and then launched

service lightdm restart

This was the end of fun. Returning to my graphical screen, by pressing Alt-Ctl-F7, I gazed at a black screen. No installer dialog running, no activity going on. Broke that installation. It told me before that it must not be interrupted!

Rebooted. Black screen. Rebooted in rescue mode. Here I initialized the network to be able to manually continue with the interrupted installation. In such a situation you are dead without a second computer to browse the internet for help. Found the advice to do (as root)

dpkg --configure -a
apt-get update
apt-get upgrade

I repeated the two apt-get commands until all packages were installed correctly. Rebooted. But I had been in a kernel upgrade, so half of the new operating system was installed, and the old one was booting.

The command to install the new kernel is

apt-get dist-upgrade

After that everything was OK, apt-get told me that I am on the newest Ubuntu 14.04. Rebooted. There was the new grub 2. Worked. But again a black screen awaited me.

So now I suspected that my netbook is too weak for Ubuntu 14.04 that requires a good graphic card to do hardware acceleration for its compiz effects. I decided to switch to a lightweight desktop environment. Lubuntu has one. It is called LXDE. Pressing Alt-Ctl-F1 I got a terminal screen, logged in as root, and launched

apt-get remove ubuntu-desktop unity compiz

I used a lot more names in the remove command, to be found here on Ubuntu Forum, but I believe that the above command would have been sufficient to do it: remove Unity and compiz.

Then I installed the Lubuntu desktop environment (you can change them like a skin):

apt-get install lubuntu-desktop

Rebooted. Black screen. Now I was upset :-)
After all I began to look for error messages. The dmesg logging rarely is helpful. In this case I suspected that the window manager had a problem, and found out by

cat /var/log/lightdm/lightdm.log

There it said something about "no configuration found". I started to look for lightdm configuration files, and how I could disable my auto-login, so that I may see at least a login screen after booting. Found this in

vi /etc/lightdm/lightdm.conf

Changed the content of that file to

[SeatDefaults]
user-session=lubuntu
greeter-session=lightdm-gtk-greeter
autologin-user=

Did not touch any other line there. Rebooted. There was the login window! Logged in. Waited, long. There was my user session! Having my old desktop icons, the auto-starting Firefox loading all my tabs.

But none of the windows had a title bar, and none could be resized. Missing window decorations. So I need to find out what I have to reinstall to get rid of that. I already had done a

apt-get install --reinstall lightdm

Do I have to reinstall window decorations separately? Finally I returned to the login screen and switched the graphical environment in top-right corner from "GNOME" to "Lubuntu". Logged in again, there were the window decorations. For some reason Gnome does not provide window decorations here ... removed it by

apt-get remove --purge gnome-session-flashback

Lubuntu surprises were waiting: it has no graphical touchpad configuration! You can do it on command line using synclient, or you can

sudo apt-get install gsynaptics

After that you can call it immediately by typing gpointing-device-settings, or you can look for "Pointing Devices" menu item under "Preferences".

I wonder why tapping is not off by default. Who has so much feeling to NOT tap on some button by coincidence when moving the mouse by touchpad?

Next gotcha: how to get German keyboard? Lubuntu forums recommend a setxkbmap command in /etc/xdg/lxsession/Lubuntu/autostart, but it is simpler. Right-click on the "US" symbol in taskbar, click popup menu item "Properties", then uncheck the "Keep system layouts" checkbox. After that you can add another "Keyboard layout" in the left-side list. Move it to top to make it default.

Slowly I start to understand why Lubuntu is called lightweight. Because nobody cares about it. When clicking upon "Shutdown" nothing happens. Only doubleclick brings the shutdown dialog, but then twice :-) After a reboot my touchpad settings have been forgotten. Oh my! Finally I got that all as personal settings ... following I wrote in my

  • $HOME/.config/lxsession/Lubuntu/autostart
@synclient MaxTapTime=0
@syncdaemon -d -t
@setxkbmap -layout "de,us"

As it is with LINUX, you do a lot of things, and finally it works but you don't know why, and you don't want to waste even more time by trying out what was the problem.
This was a quite long evening. Lubuntu is not for the impatient :-)

Here is a Blog of someone who managed to find out how to add launchers to the taskbar (Gnome "Panel").

Update

Hibernation does not work when closing the lid?

Before searching in Ubuntu forums and editing polkit configurations (policy kit), just open Preferences menu, go to Power Manager and check what is the action taken upon "Close lid". On my installation it was NOT "Suspend", so I had to set it manually. After that the computer really suspended when I closed the lid (although it takes some seconds).



Dienstag, 10. Februar 2015

JS Folding

Web pages offer more and other possibilities than print pages. One is folding. It can hide text that may not be important on first glance. But it can also serve to implement expansible trees.

You can connect an expansion trigger to something to expand. Click on the left-side triangle to see how this is done.

The JavaScript used in this "Disclosure" exposes a function called toggle() that

  • sets the given content element to either visible or invisible via content.style.display = ...., and
  • changes the expansion symbol according to new state via trigger.innerHTML = .....

That function can be called by any element's onclick event listener, with (1) the expand-element itself and (2) the content-element to toggle as parameters.

A span element serves as expand-element here, holding the expand-symbol, the onclick event listener, and style="cursor: pointer" to show the hyperlink-cursor on mouse-over. To reference the content-element, the global document.getElementById() function is used. All that remains to do now is set the referenced id on the content-element as id="expand_example".

Mind: folded text will not be found by a page text search!

CSS Expand Control

Here are some JS statements to define expand controls.

  var blackTriangleRight = "\u25B6";
  var blackTriangleDown = "\u25BC";
  var whiteTriangleRight = "\u25B7";
  var whiteTriangleDown = "\u25BD";
  var blackTriangleRightSmall = "\u25B8";
  var blackTriangleDownSmall = "\u25BE";
  var whiteTriangleRightSmall = "\u25B9";
  var whiteTriangleDownSmall = "\u25BF";
  var plus = "+";
  var minus = "\u2012";

And here is their HTML representation.

&#x25B6;&#x25BC;
&#x25B7;&#x25BD;
&#x25B8;&#x25BE;
&#x25B9;&#x25BF;
++&#x2012;

But it is boring to write that in HTML. You could also prepend it ....

.... using JS and CSS classes with pseudo-elements (click here to see how this is done)!

That means, you set or reset a CSS class named expanded on every click, which causes the CSS ::before pseudo-element rules to toggle the expand-control.

The biggest problem is the addressing of the content-element. It would be nice to have another technique than id references. For which I see two solutions: connecting the expansion-trigger ...

  1. to the next sibling (the trigger having the same parent as the content)
  2. to a single nested child element (so the trigger being the parent of the content)

The first one is nice for disclosures. The second is nice for trees.

Disclosures, Stack Panels, Accordions

Web development has created a lot of names for UI controls. Disclosures, Stack Panels, and Accordions do nearly all the same. You could regard Accordion and Stack Panel to be a list of Disclosures that allow just one "fold" open at a time.

Click on this text to open a JS solution for disclosures.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html>

<head>
  <style type="text/css">
    .expandControl {
      cursor: pointer;
    }
    .expandControl::before {
      content: "\025B7";
    }
    .expandControl.expanded::before {
      content: "\025BD";
    }
  </style>
</head>

<body>

  <p class="expandControl">
    Click on the left-side triangle to open a JS solution for Disclosures.
  </p>

  <div>
    Content 1 goes here ....
  </div>


  <script type="text/javascript">
    var toggle = function(trigger, content) {
      if (content.style.display === '') {
        content.style.display = 'none';
        trigger.className = "expandControl";
      }
      else {
        content.style.display = '';
        trigger.className = "expandControl expanded";
      }
    };
    
    var init = function(root) {
      root = root || document.documentElement;
      var expandControls = root.getElementsByClassName("expandControl");
      
      for (var i = 0; i < expandControls.length; i++) {
        var expandControl = expandControls[i];
        var content = undefined;
        
        // find next sibling - quite difficult with the w3c DOM API!
        var parent = expandControl.parentNode;
        var children = parent.children;
        var previous = undefined;
        for (var j = 0; j < children.length && ! content; j++) {
          var element = parent.children[j];
          if (previous === expandControl)
            content = element;
          else
            previous = element;
        }
          
        if (content)
          connect(expandControl, content);
      }
    };
    
    var connect = function(expandControl, content) {
      expandControl.addEventListener("click", function(event) {
        event.stopPropagation(); // needed to prevent ancestor items to also receive that click
        toggle(expandControl, content);
      });
      content.style.display = "none";
    };
    
    init();
    
  </script>

</body>
</html>

This is really smart. No id references, no onclick listeners, you just have to set the expandControl CSS class on each expansion trigger, the JS init() function does the rest. Maybe it should output a warning if it did not find any follower sibling for some expand-control.

Trees

For trees it is different. Every LI element is an expand-control, thus we don't want to set the CSS class manuallly on each of them. Further there should be an triangle only on elements that actually contain sub-lists. When not, an indentation placeholder must be there instead.

It does not make much sense to turn a numbered list into a tree. So I restrict this to UL lists (unnumbered lists).

Here is an example list with sub-lists that I want to turn into an expansible tree:

  • item 1
    • item 1.1
    • item 1.2
    • item 1.3
      • item 1.3.1
      • item 1.3.2
      • item 1.3.3
    • item 1.4
  • item 2

Open this disclosure to see a JS solution for trees on nested lists.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<!DOCTYPE html>
<html>

<head>
  <style type="text/css">
    ul {
      list-style-type: none;
      padding-left: 1em;
    }
    ul li.expandControl::before {
      content: "\025B7";
      cursor: pointer;
      padding-right: 0.4em;
    }
    ul li.expandControl.expanded::before {
      content: "\025BD";
      cursor: pointer;
      padding-right: 0.4em;
    }
    ul li.expandControlEmpty::before {
      content: "\020";
      padding-left: 1.2em;
    }
  </style>
</head>

<body>

    <ul>
       <li>item 1
       <ul>
          <li>item 1.1</li>
          <li>item 1.2</li>
          
          <li>item 1.3
            <ul>
              <li>item 1.3.1</li>
              <li>item 1.3.2</li>
              <li>item 1.3.3</li>
            </ul>
          </li>

          <li>item 1.4</li>
        </ul>
      </li>

      <li>item 2</li>
    </ul>


  <script type="text/javascript">
    var toggle = function(trigger, content) {
      if (content.style.display === '') {
        content.style.display = 'none';
        trigger.className = "expandControl";
      }
      else {
        content.style.display = '';
        trigger.className = "expandControl expanded";
      }
    };
    
    var init = function(root) {
      root = root || document;
      var expandControls = root.getElementsByTagName("LI");
      
      for (var i = 0; i < expandControls.length; i++) {
        var expandControl = expandControls[i];
        // find child list
        var children = expandControl.children;
        var subList = (children.length === 1 && children[0].tagName === "UL")
            ? children[0]
            : undefined;
        connect(expandControl, subList);
      }
    };
    
    var connect = function(expandControl, content) {
      expandControl.className = content ? "expandControl" : "expandControlEmpty";
      expandControl.addEventListener("click", function(event) {
        event.stopPropagation(); // needed to prevent ancestor items to also receive that click
        if (content)
          toggle(expandControl, content);
      });
      
      if (content)
        content.style.display = "none";
    };
     
    init();
    
  </script>

</body>
</html>

The init() JS function fetches all LI elements and connects them with their nested UL list, when existing. Because browsers pass unconsumed events to parent elements ("event bubbling"), the click event must also be caught on empty LI items, otherwise the parent LI would answer to the mouse event by collapsing.

And here is how the the list above looks (and acts) after having converted it to a tree:

  • item 1
    • item 1.1
    • item 1.2
    • item 1.3
      • item 1.3.1
      • item 1.3.2
      • item 1.3.3
    • item 1.4
  • item 2


Feel free to visit my homepage to see more advanced JS code about disclosures and trees. On bottom of the page you will have the full highlighted JS source in disclosures :-)