Modularizing JavaScript with RequireJS

Modularizing JavaScript with RequireJS

By Tim Pollock, OCI Principal Software Engineer

August 2015


Introduction

Structuring code is an important part of software development, and it seems to be especially so with JavaScript. Problems with global variables and dependencies between modules are often a problem as projects grow more complex. 

Modularization is often accomplished with the Module Pattern, where modules are defined as functions that encapsulate private members and methods, and which expose public methods. Developers are still responsible for ordering script loading so that the dependencies between modules are properly maintained.

RequireJS makes this easier, and this article demonstrates its use. We'll start with a simple example, to show how RequireJS works, then move on to a more realistic example to demonstrate how it may help you structure your code.

A Simple Example

The examples shown in this article are available for download here. The code for this example is located in the simple_example_no_require folder.

This example is a very simple html file that loads two scripts. The first script provides some text, and the second script calls the first one to get the text and add it to the DOM.

s1.js

  1. var s1 = (function() {
  2.     console.log("s1");
  3.     return { text: 'test' };
  4. })();

s2.js

console.log("s2");
document.body.appendChild(document.createTextNode(s1.text));

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <body>
  4. <script src="s1.js"></script>
  5. <script src="s2.js"></script>
  6. </body>
  7. </html>

When index.html is opened in Chrome you will see in the JavaScript Console that s1 is loaded before it is used by s2:

s1
s2

If the order of the two scripts in index.html is changed (s2 loaded before s1), an error will occur:

s2
Uncaught ReferenceError: s1 is not defined
s1

Of course, in this trivial example, the problem is obvious, but as code gets more complex, it's common to have problems like this when refactoring code. This may also happen when modules are loaded from a Content Delivery Network (CDN), where network latencies may cause a script to not get loaded before it is used by another part of the code.

A Simple RequireJS Example

The code for this example is located in the simple_example_with_require folder in the examples.zip file for this article.

Next, let's take that simple example and modify it to use RequireJS to define the dependency of s2 on s1. First, we'll specify s1 as a module with define:

s1.js

  1. define(function() {
  2. console.log("s1");
  3. return { text: 'test' };
  4. });

That's not that different from the first version of s1. RequireJS uses define to define a module that some other part of the code will depend upon. Next, let's modify s2 to depend on s1:

s2.js

  1. require(['s1'], function(s1) {
  2. console.log("s2");
  3. document.body.appendChild(document.createTextNode(s1.text));
  4. });

The 's1' in the square brackets notifies RequireJS that s1.js needs to be loaded before the function that follows it is executed. Once s1.js is loaded, the module it defines is provided as the s1 parameter of the s2 module's factory function, and used in the s1.text call.

If additional dependencies exist, they may be added within the square brackets. For instance, a module that depends on s1.js, s2.js and s3.js would declare those dependencies as require(['s1', 's2', 's3'], function (s1, s2, s3) {...}).

To finish this example, we'll modify index.html to have just one script tag which pulls in require.js and specifies the JavaScript file defining the entry point to our code:

index.html

  1. <DOCTYPE html>
  2. <html>
  3. <body>
  4. <script data-main="s2.js" src="require.js"></script>
  5. </body>
  6. </html>

In the code above, data-main specifies the JavaScript file that contains the application entry point. Since s2 depends on s1, RequireJS will load s1 before executing s2.

A Poorly Structured Example

The code for this example is located in the example1 folder in the examples.zip file for this article.

This next example has everything in just one index.html file. It loads some CSS and JavaScript files from a CDN, then defines some inline CSS and some additional JavaScript. There is nothing wrong with this design as long as it is small and easily understood. All too often, however, such code tends to grow in size and complexity, resulting in something that is difficult to maintain.

The code below creates a page with two input fields where first and last name may be entered, and a page element that joins those two inputs to display the full name. It uses jQuery, as well as Bootstrap classes to style the page elements, and Bootstrap JavaScript to provide a tooltip. It also uses Knockout to bind a view model to elements on the page.

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>RequireJS Example</title>
  5. <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css">
  6. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  7. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
  8. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js"></script>
  9. <style type="text/css">
  10. .panel-default > .panel-heading {
  11. background-color: linen;
  12. }
  13. </style>
  14. </head>
  15. <body>
  16. <div class="panel panel-default">
  17. <div class="panel-heading">Test Form</div>
  18. <div class="panel-body">
  19. <input type="text" class="form-control" placeholder="First name" data-bind="value: firstName, valueUpdate: 'keyup'" />
  20. <input type="text" class="form-control" placeholder="Last name" data-bind="value: lastName, valueUpdate: 'keyup'" />
  21. </div>
  22. <h3><span title="Click to edit" class="label label-success">Hello, <span id="name" data-bind="text: fullName"></span>!</span></h3>
  23. </div>
  24. <script type="text/javascript">
  25. var ViewModel = function(first, last) {
  26. var self = this;
  27. self.firstName = ko.observable(first);
  28. self.lastName = ko.observable(last);
  29. self.fullName = ko.computed(function() {
  30. return self.firstName() + ' ' + self.lastName();
  31. });
  32. };
  33.  
  34. $(document).ready(function() {
  35. ko.applyBindings(new ViewModel("John", "Smith"));
  36. })
  37. </script>
  38. </body>
  39. </html>

In this article, we modify the above example to develop a more modular, maintainable structure, to deal with module dependencies, and to dynamically load page content.

Moving CSS and Code to Separate Files

The code for this example is located in the example2 folder in the examples.zip file for this article.

We can improve the above example by moving our inline CSS and JavaScript into separate files. That will result in a main index.html file, and files for our CSS and JavaScript:

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>RequireJS Example</title>
  5. <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css">
  6. <link rel="stylesheet" type="text/css" href="common.css">
  7. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  8. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
  9. <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js"></script>
  10. <script type="text/javascript" src="main.js"></script>
  11. </head>
  12. <body>
  13. <div class="panel panel-default">
  14. <div class="panel-heading">Test Form</div>
  15. <div class="panel-body">
  16. <input type="text" class="form-control" placeholder="First name" data-bind="value: firstName, valueUpdate: 'keyup'" />
  17. <input type="text" class="form-control" placeholder="Last name" data-bind="value: lastName, valueUpdate: 'keyup'" />
  18. </div>
  19. <h3><span title="Click to edit" class="label label-success">Hello, <span id="name" data-bind="text: fullName"></span>!</span></h3>
  20. </div>
  21. </body>
  22. </html>

common.css

  1. .panel-default > .panel-heading {
  2. background-color: linen;
  3. }

main.js

  1. var ViewModel = function(first, last) {
  2. var self = this;
  3. self.firstName = ko.observable(first);
  4. self.lastName = ko.observable(last);
  5. self.fullName = ko.computed(function() {
  6. return self.firstName() + ' ' + self.lastName();
  7. });
  8. };
  9. $(document).ready(function() {
  10. ko.applyBindings(new ViewModel("John", "Smith"));
  11. })

We still have a problem with dependencies between parts of our code. If, in the index.html file above, the script tag for main.js is moved to above the script tag for jQuery, an error will occur since main.js will try to use jQuery before it is loaded:

Uncaught ReferenceError: $ is not defined

We'll fix that in the next example by using RequireJS to ensure that the script files are loaded before use.

A Better Structure with RequireJS

The code for this example is located in the example3 folder in the examples.zip file for this article.

This example will modify the previous one to use RequireJS, and modularize and improve the structure of the code. As you can see, this design results in two CSS files and three JavaScript files (as well as require.js):

This example will also use several files for optimization:

The index.html file is similar to the previous one, but with just one script tag. It uses app.css and app.js, both of which are optimized files generated by executing r.js (we'll explain more about that in a bit). Note that the file specified by data-main does not end in ".js", since data-main defines an entry point, and a JavaScript file is assumed.

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>RequireJS Example</title>
  5. <link rel="stylesheet" type="text/css" href="app.css">
  6. <script src="require.js" data-main="app"></script>
  7. </head>
  8. <body>
  9. <div class="panel panel-default">
  10. <div class="panel-heading">Test Form</div>
  11. <div class="panel-body">
  12. <input type="text" class="form-control" placeholder="First name" data-bind="value: firstName, valueUpdate: 'keyup'" />
  13. <input type="text" class="form-control" placeholder="Last name" data-bind="value: lastName, valueUpdate: 'keyup'" />
  14. </div>
  15. <h3><span title="Click to edit" class="label label-success">Hello, <span id="name" data-bind="text: fullName"></span>!</span></h3>
  16. </div>
  17. </body>
  18. </html>

The generated app.css file comes from optimizing main.css, which declares all of the CSS required by the page. It imports the Bootstrap CSS from a CDN, as well as the CSS that was originally inlined in the previous examples index.html file:

main.css

@import url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css');
@import url('common.css');

The common.css file is the same as the previously defined inline CSS:

common.css

  1. .panel-default > .panel-heading {
  2. background-color: linen;
  3. }

After optimization (described later), the app.css file looks like this:

app.css

@import url("https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/css/bootstrap.min.css");.panel-default > .panel-heading{background-color:linen;}

The generated app.js file comes from load.js, which configures the modules that are loaded from CDNs, then specifies the entry point (main.js):

load.js

  1. requirejs.config({
  2. paths: {
  3. jquery: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min',
  4. ko: 'https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min',
  5. bootstrap: 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min'
  6. }
  7. });
  8. requirejs(['main']);

In the configuration portion of the load.js file shown above, paths specifies how to obtain the jQuery, Knockout and Bootstrap modules that will later be used.

The main.js file creates a ViewModel object and binds it to the DOM once the dependencies for the module have been loaded:

main.js

  1. define(['jquery', 'ko', 'viewmodel'], function ($, ko, ViewModel) {
  2. $(document).ready(function() {
  3. ko.applyBindings(new ViewModel("John", "Smith"));
  4. });
  5. });

The viewmodel.js file is a module that defines the viewmodel used when binding data to DOM elements. As you can see, it depends on Knockout, and the function it defines will not be executed until Knockout is loaded:

viewmodel.js

  1. define (['ko'], function (ko) {
  2. var ViewModel = function(first, last) {
  3. var self = this;
  4. self.firstName = ko.observable(first);
  5. self.lastName = ko.observable(last);
  6. self.fullName = ko.computed(function() {
  7. return self.firstName() + ' ' + self.lastName();
  8. });
  9. };
  10. return ViewModel;
  11. });

The generated app.js file concatenates and uglifies all of the JavaScript, and looks like this:

app.js

define("viewmodel",["ko"],function(e){var t=function(t,n){var r=this;r.firstName=e.observable(t),r.lastName=e.observable(n),r.fullName=e.computed(function(){return r.firstName()+" "+r.lastName()})};return t}),define("main",["jquery","ko","viewmodel"],function(e,t,n){e(document).ready(function(){t.applyBindings(new n("John","Smith"))})}),requirejs.config({paths:{jquery:"https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min",ko:"https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min",bootstrap:"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min"}}),requirejs(["main"]),define("load",function(){});

The final files we'll cover in this example are the ones used by the optimization step to create app.css and app.js. RequireJS optimization is run by executing r.js in Node. We'll run it twice to create our optimized files, and for convenience we'll do so in a batch file, as follows:

build.bat

node r.js -o optimizeJs.js
node r.js -o optimizeCss.js

The -o command tells r.js to run the optimizer. In the batch file we'll run it once to create app.js and a second time to createapp.css.

optimizeJs

  1. ({
  2. paths: {
  3. jquery: "empty:",
  4. ko: "empty:",
  5. bootstrap: "empty:"
  6. },
  7. baseUrl: "./",
  8. out: "./app.js",
  9. name: "load"
  10. })

The load.js file gets jQuery, Knockout and Bootstrap from CDNs, and we could have done that in this optimization step to get the scripts during the build process and save them to our optimized app.js. What we really want, however, is to get the scripts when the user loads the page, so we'll omit that from the build process by using the empty scheme in the paths config. That results in the files being skipped during the optimization.

The baseUrl command indicates where the files reside that you want to optimize. The out argument specifies what you want the optimized file to be named, and the name argument indicates the file on which the optimization runs. In this example, load.js is the entry point of the application, so that file, and all of the files on which it depends, are optimized into app.js.

Another command that is commonly used is optimize, which, when omitted, defaults to 'uglify'. If you set optimize to 'none' you'll get the concatenation, but the code will remain in its original form, which is useful for debugging. So, you might have two build.js files: one for building app.js during the development process, and another for building app.js for deployment.

optimizeCss.js

  1. ({
  2.     optimizeCss:    "standard",
  3.     cssIn:          "./main.css",
  4.     out:            "./app.css"
  5. })

The configuration file for optimizing the CSS files is a little simpler. The optimizeCss command tells the optimizer how to optimize the CSS files. In this case, we use standard optimization, which inlines @import lines and removes all comments and white space.

The cssIn command specifies the file to be optimized, and the out command provides the name of the optimized file.

A Final Example Using Node.js

The code for this example is located in the example4 folder in the examples.zip file for this article.

The final example in this discussion of RequireJS will take the previous example and run it as a Node application. This example will show some additional features of RequireJS and dynamically loads page content in order to better structure the application. The files in this example are as follows:

In order to run this example, you'll need to install Node.js. The node_modules folder is created by running the package manager on the package.js file with this command:

npm install

The only module defined in package.json is Express.js, which simplifies our Node.js server code.

package.json

  1. {
  2.     "dependencies": {
  3.         "express": "^4.12.4"
  4.     }
  5. }

Using Express.js, we can define a simple server that will serve up our page.

server.js

  1. var express = require('express');
  2. var path = require('path');
  3. var app = express();
  4. app.use(express.static(path.join(__dirname, 'public/views')));
  5. app.use(express.static(path.join(__dirname, 'public/css')));
  6. app.use(express.static(path.join(__dirname, 'public/js')));
  7. app.get('/', function(req, res) {
  8. res.sendFile('index.html');
  9. })
  10.  
  11. var server = app.listen(3000, function() {
  12. console.log('Listening on http://localhost:%s', server.address().port);
  13. })

Our index.html file will be much simpler because we'll be dynamically loading the page content. It will have one <div> to which we will add our controls. In a real application this might be done in order to customize the page to the particular user visiting it.

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>RequireJS Example</title>
  5. <link rel="stylesheet" type="text/css" href="app.css">
  6. <script src="lib/require.js" data-main="app"></script>
  7. </head>
  8. <body>
  9. <div id='app' class='hidden'></div>
  10. </body>
  11. </html>

As in our previous example, app.css and app.js are our optimized files, and the first JavaScript file loaded by app.js is load.js.

In this example we demonstrate how we can use the RequireJS fallback feature to serve up our own JavaScript files when a CDN fails to provide a resource. In such a case we simply provide the JavaScript file ourselves by specifying the path in the configuration.

load.js

  1. requirejs.config({
  2. paths: {
  3. jquery: [
  4. 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min',
  5. 'lib/jquery.min'
  6. ],
  7. ko: [
  8. 'https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min',
  9. 'lib/knockout-min'
  10. ],
  11. bootstrap: [
  12. 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min',
  13. 'lib/bootstrap-min'
  14. ]
  15. }
  16. });
  17. requirejs(['main']);

Our main.js file uses a RequireJS plugin to determine when the DOM is ready. By wrapping our code within require(['lib/domReady!']) we ensure that the page has been fully loaded.

Since domReady is a plugin it ends in '!', which is used by plugins to separate the name of the plugin and the resource the plugin will provide. The resource associated with the domReady plugin is the document, so it's not necessary to explicitly specify it, but the loader plugin syntax still requires the '!'.

The first thing we do after the DOM is ready is to check if the CDN-loaded CSS file failed to load. We do that by looking for an element with a class provided by that CSS file (the .hidden class). If we don't find it, then we load it ourselves.

main.js

  1. define(['jquery', 'ko', 'viewmodel'], function ($, ko, ViewModel) {
  2. require(['lib/domReady!'], function (doc) {
  3. // Bootstrap.css CDN load failure fallback
  4. $(function () {
  5. if ($('.hidden:first').is(':visible') === true) {
  6. $('<link rel="stylesheet" type="text/css" href="bootstrap.min.css">').appendTo('head');
  7. }
  8.  
  9. $('#app').removeClass('hidden');
  10. });
  11.  
  12. $('#app').load('content.html', function() {
  13. var viewModel = new ViewModel("John", "Smith");
  14. ko.applyBindings(viewModel);
  15.  
  16. $('#name').click(function() {
  17. $('#input').load('input.html', function() {
  18. ko.applyBindings(viewModel, $('#input')[0]);
  19. $('#name').unbind('click');
  20. })
  21. })
  22. });
  23. });
  24. });

Once the document is ready, we load the page content into the app div. We also register a click handler so that, when the name control is clicked, we load additional controls for modifying first and last names.

Note that we only apply bindings to the input div once we've loaded that additional HTML. For this example, we'll just load the same HTML that the previous example used, less the input controls. For a real application, we may load different content based on the needs of the user viewing the page.

content.html

  1. <div class="panel panel-default">
  2. <div class="panel-heading">Test Form</div>
  3. <div class="panel-body">
  4. <div id="input"></div>
  5. </div>
  6. <h3>
  7. <span title="Click to edit" class="label label-success">
  8. Hello, <span id="name" data-bind="text: fullName"></span>!
  9. </span>
  10. </h3>
  11. </div>

When the name element is clicked, we load input.html into the input element. The HTML in content.html and input.html comprise the same HTML as in the previous example. We demonstrate loading it here to show how an application may be better structured by loading only that which is necessary for the page being viewed.

input.html

  1. <div class="input-group input-group-sm">
  2. <input type="text" class="form-control" placeholder="First name"
  3. data-placement="bottom" title="Enter first name"
  4. data-bind="value: firstName, valueUpdate: 'keyup'" />
  5. <input type="text" class="form-control" placeholder="Last name"
  6. data-placement="bottom" title="Enter last name"
  7. data-bind="value: lastName, valueUpdate: 'keyup'" />
  8. </div>

Summary

In this article, we reviewed how RequireJS may be used to improve the structure of your JavaScript code and to enforce the order in which modules are loaded. We began with a simple example where we showed how RequireJS works, then modified a more complex example, using RequireJS to demonstrate how to develop well-structured and more maintainable code.

In the last example, we reviewed a couple of additional RequireJS features (the fallback feature and the domReady plugin), and demonstrated how to dynamically load HTML, such that only necessary content will be loaded.

The day will come when the JavaScript specification that defines modules will be implemented in most browsers, making script loaders like RequireJS less (or un) necessary. Until then, RequireJS is a great way to modularize your JavaScript code and enforce the proper loading of resources required by modules.

References

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret