Modular JavaScript with RequireJS

Author

The role of JavaScript for application development has seen a serious uptick over the past few years. Just a few years ago, we would use JavaScript for client-side form validation and some AJAX and UI tricks using Prototype and Scriptaculous. Even more sophisticated frameworks started to pop up, like Dojo, ExtJS and the JQuery / JQuery UI combo. It became possible to create feature-rich applications with near-desktop fidelity, all using JavaScript in the browser.

With that growth though, JavaScript started to creak at the seams with some of the limitations of the language. Douglas Crockford’s “JavaScript: the Good Parts” provided a thoughtful guide for how to avoid the dangerous areas of the language, but there are some constructs present in languages like Java, C++ and C# that have to be improvised in JavaScript.

One of the biggest gaps has been in modular programming. JavaScript has no direct language support of namespaces or module loading. This is the sort of modularity addressed by the package keyword in Java or namespace in C# or C++. Various libraries have created their own workarounds to this code modularity issue, but there isn’t anything native in the current ECMAScript specification to address this.

Fortunately, out of this vacuum, two strong frameworks have emerged. For server-side work, NodeJS embraced the CommonJS standard. For client-side work, Asynchronous Module Definition (AMD), as implemented in RequireJS, has emerged as a rallying point for writing modular code.

Making effective use of RequireJS requires a mindset shift for developers. In the “good” old days, a developer would successively load a series of JavaScript files through the <script> tag. This was simple enough for a few files, but it quickly turns in to a maintenance nightmare in a JavaScript-heavy application. Without a clear way to define modules, JavaScript code can quickly become convoluted and difficult to maintain. Loading multiple JavaScript files via the <script> tag can also drag down application performance as the user has to wait for the scripts to load to interact with the application.

RequireJS brings sanity to this situation. It provides not only a clear definition for what is a module, but it also provides the mechanism to load modules asynchronously. This improves code maintainability and also performance by allowing a page to only load the modules it needs.

Modules

The key concept with RequireJS and AMD is the module. A module is simply a JavaScript function that returns a value. That value is typically another function or a JavaScript object. For example, a simple module that defines a way to convert a String to uppercase might look like this:

define(function() {
  return function(s) { return s.toUpperCase(); };
});

The global define() function is created by the RequireJS library and allows the definition of a module. Convention is to use the name of the file as the name of the module, so if we save this snippet as upper.js we could make use of this module like this:

require(['upper'],function(upper) {
  console.log( upper('hello world') );
});

In this case, we use the global RequireJS require() method to specify some code we want to execute, along with its dependencies, which is the upper module created above. RequireJS express this relationship by having the first parameter to the require() call be an array of the modules our code will depend on. RequireJS will deal with loading the dependencies and will pass each dependency, in order, to the function that is the module definition. The parameter name is arbitrary, but convention is to use the name of the module.

This is a pretty basic example to demonstrate what a module is. Now we’ll put some rubber to the road and show a more complete picture. The code for this is up on my GitHub account, so you can grab it for yourself.

This example uses RequireJS, JQuery, Twitter Bootstrap, the doT templating engine, and some RequireJS plugins to create a simple page that generates a table via templates and a modal dialog to edit the row when clicked on. We’ll be using a JSON file as the datasource, so there is no updating, but this eliminates the need for a backend in the example.

Note that we’re using the version of RequireJS which includes JQuery. Since JQuery is not AMD-aware, this combined RequireJS/JQuery file is the easiest way to include JQuery as a module.

The Main Page

The <head> element of index.html sets up RequireJS and the Bootstrap CSS file. Note the data-main attribute on the <script> tag on line 7 that loads RequireJS. This specifies the overall application-level JavaScript file used by RequireJS for configuration. It means RequireJS will attempt to load a file called main.js under the scripts directory.

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
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>RequireJS Sample</title>
  <link rel="stylesheet" href="styles/bootstrap.css">
  <script src="scripts/require-jquery.js" data-main="scripts/main"></script>
</head>
<body>
 
<div class="container">
 
  <div class="page-header">
    <h1>Customers
      <small>A RequireJS version</small>
    </h1>
  </div>
 
  <div class="row">
    <div class="span8">
      <table class="table-bordered table" id="customer-table">
        <thead>
        <tr>
          <th>First Name</th>
          <th>Last Name</th>
          <th>Action</th>
        </tr>
        </thead>
        <tbody id="customer-rows">
 
        </tbody>
      </table>
    </div>
  </div>
</div>
 
<div class="modal hide fade" id="customer-modal">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal">×</button>
    <h3>Customer</h3>
  </div>
  <div class="modal-body" id="modal-body">
 
  </div>
  <div class="modal-footer">
    <a href="#" class="btn btn-success save-customer">Save</a>
    <a href="#" class="btn" data-dismiss="modal">Close</a>
  </div>
</div>
 
</body>
</html>

The rest of the index.html file sets up an empty table where we’ll display the rows of data using a template. It also defines a Bootstrap modal dialog, again with an empty body which will get populated with a template.

RequireJS Configuration

The contents of the main.js file configure RequireJS, and, in this case, make use of some modules and execute some code. The configuration is handled via the require.config() call at the top of the file, and the code at the bottom:

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
require.config({
  paths: {
    bootstrap: 'lib/bootstrap.min',
    doT: 'lib/doT.min',
    views: 'templates/templates',
    datasource: 'data/json-datasource'
  },
  shim: {
    'bootstrap': ['jquery']
  }
});
 
require(['jquery','datasource','views','bootstrap','domReady!'],
    function($,datasource,views){
 
  $('#customer-rows').html( views.tablerow( datasource.list() ));
 
  $('#customer-table').on('click','a.edit-customer', function(e) {
    e.preventDefault();
    var idx = $(this).data('id');
    $('#modal-body').html( views.dialog( datasource.get(idx) ));
    $('#customer-modal').modal('show');
  });
 
  $('#customer-modal').on('click','a.save-customer', function(e){
    e.preventDefault();
    datasource.update( $('#customer-form').serialize() );
    $('#customer-modal').modal('hide');
  });
});

RequireJS has numerous, well-documented configuration options. We use the paths option to specify the location of the external dependencies we plan on using. The paths option keeps us from having to hard-code the physical path to a module. For example, instead of specifying ['lib/bootstrap'] as a module dependency, configuring the path lets us do this ['bootstrap']. This provides a nice level of abstraction which will also make code maintenance easier if we need to change something.

Note that none of the dependencies defined in the config have a file extension. RequireJS will automatically append the .js extension when it attempts to load the actual file. The location of the dependencies is relative to the location of the main.js file, although that can be altered via configuration options.

The shim provides a way of specifying dependencies that aren’t configured as AMD modules. Here we’re indicating that the bootstrap module depends on jquery and it must be loaded first. Since there is no guarantee on loading order for modules defined as dependencies, the shim option allows for the easy integration of non-AMD libraries that need dependencies themselves.

The bottom half of main.js actually does some work. The require() method is used to load our dependencies and execute some code. On line 13 we indicate we are going to use the jquery, datasource, views, and bootstrap modules. We also require the domReady! module. This is one of the core plugins for RequireJS. Since we want to manipulate the DOM in our function, we need to ensure it is ready. RequireJS loads modules asynchronously, so it could conceivably load all our modules before the DOM is ready to go if we had a large HTML page. The domReady! plugin will wait to ensure the DOM is ready before calling our function. The exclamation point in the name is a plugin feature that shortcuts the plugin to not require us to declare it as a parameter to our function.

On line 14 we define the function that will get called when all the required modules are loaded. Each parameter to the function corresponds to one of the modules we required, in order. Since Bootstrap is not AMD-aware, and just piggybacks on JQuery, we don’t need to add a parameter for it.

Line 16 has a lot going on. We’re calling the list() method of the datasource module to get a JSON array of the rows we are going to render in the table. We then pass the results to the tablerow() method of the views module, which will generate the HTML for the table body. Finally, we use some JQuery to insert the generated HTML into the table on the page.

Lines 18 and 25 add some event listeners to both the Edit anchor tag in the table rows and the Save button on the modal. They follow the same pattern above, making use of the datasource and views modules to get data and format it for display.

Note in this example we are using the JQuery $ passed as a parameter to our function. JQuery is still going to populate the global namespace with the $, since it is not AMD-aware. One of the goals of AMD is to prevent pollution of the global namespace so you should only make use of parameters passed to your function and not make any assumptions about the global namespace.

The Views

The templates.js file in the scripts/templates directory handles loading the doT templates from HTML files and generating the content based on the JSON data provided:

1
2
3
4
5
6
7
8
9
10
define(['doT',
        'text!templates/table-row.html',
        'text!templates/dialog.html'],
        function(doT, tablerowText, dialogText){
 
  return {
    tablerow: doT.template(tablerowText),
    dialog: doT.template(dialogText)
  };
});

There is a lot going on in these 10 lines of code. We include the doT module as our first dependency. On lines 2 and 3, we make use of another RequireJS core plugin: the text plugin. This plugin will load a text file and treat it as a dependency. The path to the file comes after the exclamation mark.

Line 4 defines the function to be called when the dependencies are ready. Since we’re using the text plugin to load some files, the contents of each of those files are passed as parameters to this function. The names of the parameters are arbitrary, but the order must match the order the dependencies are specified in.

The contents of our module is what we return on line 6. We return a JavaScript object that contains two functions which point to the compiled doT templates.

The Datasource

The json-datasource.js file in the scripts/data directory is our simple datasource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
define(['json!data/customers'], function(customers){
 
  var getRow = function(id) {
    return customers[id];
  };
 
  var getAll = function() {
    return customers;
  };
 
  var update = function(data) {
    // do something cool with the data
  };
 
  return {
    get: getRow,
    list: getAll,
    update: update
  };
});

We only have one dependency, the json plugin. This is one of the Extra Plugins for RequireJS. It works similar to the text plugin, but it gives us the data as a JSON object instead of text. Here we are loading the data/customers.js file to get the JSON data. Since we dont specify a file extension, RequireJS will use .js by default.

This module returns a JavaScript object that exposes three functions for acccessing the data. Since we’re only reading data from a file, the update() function is a no-op, but the two read functions get data out of the JSON object we loaded from the file.

The Results

The results of all these modules is a simple page which looks like this:

The rows of customers are dynamically added via our views and datasource modules. These same two modules are then used to populate the content of the modal dialog when a user clicks on the Edit link in the table:

We can see the effect of the asynchronous loading via the Chrome Developer Tools. Here we can see HTML, CSS and require.js file all get loaded very quickly, then the remaining JavaScript files and templates are loaded asynchronously after the DOM is ready. This is loading from a simple Apache HTTPD instance on a shared hosting site.

This is a simple example of a few modules working together, but it shows some of the power and possibilities of using RequireJS in an application. We could easily refactor this code to put the dialog into a stand-alone module which inserts HTML into a page via a template, and then reuse the module across several pages.

Closing Thoughts

  1. RequireJS and AMD are cool, once they click. The key concept is understanding what a “module” really is. A module is just a function that returns a value that is passed as a parameter to another module. This can be a simple value, for example a PI module that returns 3.14, but it is usually a function or JavaScript object containing several functions.
  2. RequireJS includes the r.js utility to package up all the modules into a single JavaScript file. There is debate on whether this is a good or bad practice, and it is definitely not something you would use during development, but it gives you options.
  3. ECMAScript will eventually have support for modules via the Harmony Project, and that support will probably show up pretty quickly in the fast-moving world of Chrome and Firefox, but it could be a while before Internet Explorer catches up, so RequireJS is a good choice for today.
  4. RequireJS plays nicely with other JavaScript libraries, and there are plenty of good tutorials out there. If you’re a fan of Backbone, the Backbone Tutorials site has a great tutorial on using RequireJS with Backbone. There is a similar good tutorial for KnockoutJS.
  • http://www.andysnotebook.com andydavies

    Interesting article…

    But if you’re going to post a waterfall can you use one that’s from a realistic scenario rather than a local HTTP server?

    How do things like requirejs handle merging of JS requests?

  • Pingback: Linkage: July 5th, 2012 to July 10th, 2012 | ben lowery