Skip to content

Latest commit

 

History

History
788 lines (559 loc) · 46.2 KB

ch6_large_js_apps.asciidoc

File metadata and controls

788 lines (559 loc) · 46.2 KB

Modularizing Large-Scale JavaScript Projects

Reducing the application startup latency and implementing lazy loading of certain parts of the application are the main reasons for modularization.

A good illustration of why you may want to consider modularization is a well designed Web application of the Mercedes Benz USA. This Web application serves people who live in America and either own or consider purchasing cars from this European car manufacturer.

One of their purchasing options is called "European Delivery". An American resident who chooses this particular package can combine a vacation with her car purchase. She flies to the Mercedes Benz factory in Europe, picks up her car, and has a two week vacation, driving her new vehicle around in Europe. After the vacation is over, the car is shipped to her hometown in the US. Needless to say, this program adds several thousand dollars to the price of the car.

From an application design point of view, we don’t need to include the code that supports the European Delivery to each and every user’s who decided to visit mbusa.com. If the user visits the menu Owners and clicks on the European Delivery link, then and only then the required code and resources be pushed to the user’s computer or mobile device.

The snapshot in MB USA: Europian Delivery was taken after clicking on this link with Chrome Developer tools panel open.

image
Figure 1. MB USA: Europian Delivery

As you see, 1.9Mb worth of code and other resources have been downloaded as a result of this click. Should the application architects of the MB USA decided to bring this code to the user’s device on the initial load of http://mbusa.com, the wait time would increase by another second or more. This is unnecessary because only a tiny number of American drivers would be interested in exploring the European Delivery option. This example illustrates the use case where modularization and lazy loading is needed.

Our Save The Child application is not as big as the one by Mercedes Benz. But we’ll use it to give you an example of how to build modularized Web applications that won’t bring the large and monolithic code to the client’s machine, but will load the code on as needed basis. We’ll also give an example of how to organize the data exchange between different programming modules in a loosely coupled fashion.

Users consider a Web application fast for one of two reasons: either it’s actually fast or it gives an impression of being fast. Ideally, you should do your best to create a Web application that’s very responsive.

Tip
No matter how slow your Web application is, it should never feel like it’s being frozen.

This chapter is about modularization techniques that will allow quick rendering of the first page of your Web application by the user’s browser while loading the rest of the application in the background or on demand. We will continue refactoring the Save The Child application to illustrate using modules.

In this chapter we’re going to discuss the following frameworks for modularization of JavaScript projects:

Modularization basics

Modules are code fragments that implement certain functionality and are written using a specific techniques. There is no out-of-the box modularization scheme in JavaScript language. The upcoming ECMAScript 6 specification tends to resolve this by introducing the module concept in the JavaScript language itself. This is the future.

You may ask, "Aren’t .js files modules already?" Of course, you can include and load each JavaScript file using the <script> tag. But this approach is error prone and slow. Developers have to manually manage dependencies and file loading order. Each <script> tag results in the additional HTTP call to the server. Moreover, the browser blocks rendering until it loads and executes JavaScript files.

As the application gets larger the number of script files grows accordingly, which is illustrated in the following code sample.

Multiple <script> tags complicate controlling application dependencies
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8">

    <title>Save The Child | Home Page</title>
    <link rel="stylesheet" href="assets/css/styles.css">
</head>
<body>
<!-- page body -->

<!-- body content is omitted -->

    <script src="components/jquery.js></script>     <!--(1)-->
    <script type="text/javascript" src="app/modules/utils/load-html-content.js"></script>
    <script type="text/javascript" src="app/modules/utils/show-hide-div.js"></script>
    <script type="text/javascript" src="app/modules/svg-pie-chart.js"></script>
    <script type="text/javascript" src="app/modules/donation.js"></script>
    <script type="text/javascript" src="app/modules/login.js"></script>
    <script type="text/javascript" src="app/modules/utils/new-content-loader.js"></script>
    <script type="text/javascript" src="app/modules/generic-module.js"></script>
    <script type="text/javascript" src="app/modules/module1.js"></script> <!--(2)-->
    <script type="text/javascript" src="app/modules/module2.js"></script>
    <script type="text/javascript" src="app/config.js"></script>
    <script type="text/javascript" src="app/main.js"></script> <!--(3)-->
</body> <!--(4)-->
</html>
  1. Loading the jQuery first because all other modules depend on it.

  2. Other application components may also have internal dependencies on other scripts. Those scripts need to be loaded before the respective components. Having the proper order of these script tags is very important.

  3. The script for the main Web page should be loaded after all dependencies have finished loading.

  4. We’re putting script elements at the end of the document, as it blocks as little content as possible.

As you can see, we need a better way to modularize applications than simply adding <script> tags. As our first step, we can use the Module design pattern and leverage the so-called "Immediately Invoked Function Expressions".

Next, we’ll introduce and compare two popular JavaScript solutions and modularization patterns - CommonJS and Asynchronous Module Definition (AMD), which are alternative approaches to modularization. Both CommonJS and AMD are specifications defining sets of APIs.

You’ll learn pros and cons of both formats later in the chapter, but the AMD module format plays nicely with the asynchronous nature of Web. You’ll also see the use of the AMD module format and RequireJS framework to implement the modularized version of the Save The Child application.

Also, you’ll see how to use the RequireJS APIs to implement on-demand ("lazy") loading of the application components (e.g. What We Do, Ways To Give et al.)

The upcoming ECMAScript 6 specification suggests how to handle modules, and how to start using ES6 module syntax today with the help of third-party tools like transpiler. The ES6 module syntax can be compiled down to existing module solutions like CommonJS or AMD. You can find more details about CommonJS, AMD and the ES6 module format in the corresponding sections of this chapter.

After application modules are asynchronously loaded, they need to communicate to each other. You can explicitly specify the components dependencies, which is fine as long as you have a handful of components. A more generic approach is to handle inter-module communications in a loosely coupled fashion using the Mediator pattern, CommonJS or AMD formats. By loosely coupled we mean that components are not aware of each other’s existence.

The next section reviews various approaches and patterns of modular JavaScript applications.

Roads To Modularization

Although JavaScript has no built-in language support of modules, the developers' community has managed to find a way for modularization using existing syntax constructs, libraries and conventions to emulate modules-like behavior. In this section, we’ll explore a few options for modularizing your application.

  • The Module Pattern

  • CommonJS

  • Asynchronous Module Definition (AMD)

Of these three, the Module pattern doesn’t require any additional frameworks and works in any JavaScript environment. The CommonJS module format is widely adopted for the server-side JavaSctipt, while the AMD format is popular in the applications running in Web browsers.

The Module Pattern

In software engineering, the Module pattern was originally defined as a way to implement encapsulation of reusable code. In JavaScript, the Module pattern is used to emulate the concept of classes. We’re able to include both public and private methods as well as variables inside the single object, thus hiding the encapsulated code from other global scope objects. Such encapsulation lowers the likelihood of conflicting function names defined in different scripts that could be used in the same application’s scope.

Ultimately, it’s just some code in an immediate-invoked function expression (IIFE) that creates a module object in the internal scope of a function and exposes this module to the global scope using the JavaScript language syntax. Consider the following three code samples illustrating how the Module pattern could be implemented using IIFEs.

Creating a closure that hides implementation of login module
link:include/ch6_iife_loginModule.js[role=include]
  1. Assigning the module object that was created in the closure to the variable loginModule.

  2. Because of the JavaScript’s function scoping, other parts of the code can’t access the code inside the closure. With this approach you can implement encapsulation and private members.

Injecting the module into the global object
link:include/ch6_iife_global_loginModule.js[role=include]
  1. Instead of exporting the module to a variable as in the previous example, we’re passing the global object as a parameter inside the closure.

  2. Attaching the newly created object to the global object. After that the loginModule object can be accessed from the external application code as window.loginModule or just loginModule.

Introducing namespaces in the global object
link:include/ch6_iife_namespace_loginModule.js[role=include]
  1. Here we have modification of the approach described in previous snippet. To avoid name conflicts, create a namespace for our application called ssc. Note that we check for this object existence in the next line.

  2. Now we can logically structure application code using namespaces. The global ssc object will contain only the code related to the Save The Child application.

The Module patterns works well for implementing encapsulation in a rather small applications. It’s easy to implement and is framework-agnostic. However this approach doesn’t scale well because when working with an application with the large number of modules you may find yourself adding lots of boilerplate code checking objects' existence in the global scope for each new module. Also, you need to be careful with managing namespaces: since you are the one who put an object into the global scope, you need to think how to avoid accidental names conflicts.

The Module pattern has a serious drawback - you still need to deal with manual dependency management and manually arrange <script> tags in the HTML document.

CommonJS

CommonJS is an effort to standardize JavaScript APIs. People who work on CommonJS APIs have attempted to develop standards for various JavaScript API (similar to standard libraries in Java, Python and etc) including standards for modules and packages. The CommonJS module proposal specifies a simple API for declaring modules, but mainly on the server-side. The CommonJS module format was optimized for non-browser environments since the early days of the server-side JavaScript.

On the Web browser side, you always need to consider potentially slow HTTP communications, which is not the case on the server. One of the solutions suitable for browsers is to concatenate all scripts into a handful of bundles to decrease the number of HTTP calls, which was not a concern for the server-side JavaScript engines because file access is nearly instantaneous. On the server side separation of the code allowed to dedicate each file to exactly one module for ease development, testing and maintainability.

In brief, the CommonJS specification requires the environment to have three free variables: require, exports, and module. The syntax to define the module is called authoring format. To make the module loadable by a Web browser it has to be transformed into transport format.

link:include/ch6_commonjs.js[role=include]
  1. If a module requires other modules, declare references to those modules inside the current module’s scope by using the require function. You need to call require(id) for each module it depends on. The module id has slashes defining the file path or a URL to indicate namespaces for external modules. Modules are grouped into a packages.

  2. The exports object exposes the public API of a module. All objects, functions, constructors that your module exposes must be declared as properties of the exports object. The rest of the module’s code won’t be exposed.

  3. The module variable provides the metadata about the module. It holds such properties as id and a unique uri of each module. The module.export exposes exports object as its property. Because objects in JavaScript are passed as references, the exports and module.exports point at the same object.

Warning
The snippet above may give you an impression that the module’s code is executed in the global scope, but it’s not. Each module is executed in its own scope which helps to isolate them. This works automatically when you write modules for NodeJS environment running on the server. But to use CommonJS module format in the Web browser you need to use an extra tool to generate transport format from authoring format. Browserify takes all your scripts and concatenates them it to one large file. Besides the module’s code the generated transport bundle will contain the boilerplate code that provides CommonJS modules runtime support in the browser environment. This build step complicates the development workflow. Usually developers perform the code/save/refresh browser routine, but it doesn’t work in this case and requires an extra steps as you need to install the additional build tool and write build scripts.

Pros to using CommonJS:

  • It’s a simple API for writing and using modules.

  • Such a pattern of organizing modules is widespread in the server-side JavaScript, e.g. NodeJS.

Cons to using CommonJS:

  • Web browsers don’t automatically create the scoped variables require, exports, module hence the additional build step is required.

  • The require method is synchronous, but there is no exact indication if dependent module’s values are fully loaded because of the asynchronous nature of Web browsers. There is no event to notify the application that 100% of the required resources is loaded.

  • CommonJS API is suitable for loading .js files, but it can’t load other assets like CSS and HTML.

Note
If you want to write modules in the format that can be used in both browser and server’s environments read our suggestions in the Universal Module Definition section on this chapter.

Further reading:

Asynchronous Module Definition

The AMD module format itself is a proposal for defining modules where both the module and dependencies can be asynchronously loaded. The AMD API is based on this specification.

AMD began as a draft specification for module format in CommonJS, but since the full agreement about its content was not reached, the further work on module’s format moved to the amdjs Github page.

The AMD API have the following main functions:

  • define for facilitating module definition. This function takes tree arguments:

    • The optional module id

    • An optional array of modules' IDs of dependencies

    • A callback function (a.k.a factory function), which will be invoked when dependencies are loaded.

      The signature of a define function
      define(
          module_id,          // (1)
          [dependencies],     // (2)
          function {}
      );
      1. This string literal defines module_id that will be used by the AMD loader for loading this module.

      2. An optional array of dependencies' ids.

The factory function{} above will only be executed once.

For example, the Save The Child application has a menu Way To Give, which in turn depends on other module called otherContent. If the user clicks on this menu, we can load the module that can be defined in the wayToGive.js as follows:

The definition of the wayToGive module
link:include/ch7_define_wayToGive.js[role=include]
  1. This code doesn’t have the optional module_id. The loader will use the file name without the .js extension as module_id. Our module has one dependency on the module called otherContent. The dependent module instance will be passed in the factory method as variable otherContent.

  2. We can start using the dependency object immediately. AMD loader have taken care of loading and instantiation of this dependency.

  3. The module returns constructor function to be used for creation of new objects.

  • The require function takes two arguments

    • An array of module IDs to load. Module ID is a string literal.

    • A callback to be executed once those modules are available. The modules loaded by IDs are passed into the callback in order. Here is example of the require function usage.

      The example of require function usage
      require(["main"], function() {
          console.log("module main is loaded");
      });

Pros to using AMD:

  • It’s a very simple API that has only two functions - require and define.

  • A wide variety of loaders is available. You’ll find more coverage on loaders in the RequireJS section.

  • The CommonJS module authoring format is supported by the majority of loaders. You’ll see an example of modules later in the Using CommonJS module format in RequireJS section.

  • Plugins offer an immense amount of flexibility.

  • AMD is easy to debug.

    Consider the following error messages that JavaScript interpreter may throw:

    There was an error in /modules/loginModule.js on line 42

    vs

    There was an error in /built-app.js on line 1984

    In modularized applications you can easier localize errors.

  • Performance: module are loaded only when required hence the initial portion of the application’s code become smaller.

Cons to using AMD:

  • The dependency array can get rather large for complex modules.

    link:include/ch7_define_large_array_of_deps.js[role=include]
    1. In the real-world enterprise applications the array of dependency modules might be pretty large.

  • Human errors can result in mismatch between dependency array and callback arguments.

    link:include/ch7_define_module_ids_order.js[role=include]
    1. The mismatch of module IDs and factory function arguments will cause module usage problems.

Universal Module Definition

Universal Module Definition (UMD) is a series of patterns and code snippets that provide compatibility boilerplate to make modules environment-independent. Those patterns can be used to support multiple module formats. UMD is not a specification or a standard. You need to pay attention to UMD patterns in case when your modules will run in more than one type of environment (e.g. a Web browser and on the server side engine running NodeJS). In most cases, it makes a lot of sense to use a single module format.

Here is an example of the module definition in UMD notation. In the following example, the module can be used with the AMD loader and as one of the variations of the Module Pattern.

link:include/ch7_umd_amd_globals.js[role=include]
  1. If the AMD loader is available proceed with defining the module according to AMD specification.

  2. If the AMD loader isn’t present, use the factory method to instantiate the object and attach it to the window object.

  3. Passing the top-level context and providing a implementation of a factory function.

You can find more information about UMD and commented code snippets for different situations in the UMD project repository.

ECMAScript 6 Modules

The ECMAScript 6 (ES6) specification is an evolving draft outlining changes and features for the next version of the JavaScript language. This specification is not finalized yet and the browsers' support for anything defined in ES6 will be experimental at best and cannot be relied upon for Web applications that must be deployed in production mode in multiple browsers.

One of the most important features of ES6 specification is the module syntax. Here is an example of some login module definition:

Login module definition
export function login(userNameValue, userPasswordValue) {
    return userNameValue + "_" + userNameValue;
}

The keyword export specifies the function or object (a separate file) to be exposed as a module, which can be used from any other JavaScript code as follows:

Main application module
import {login} from './login'
var result = login("admin", "password");

With the import keyword we assign instance of login() function imported from login module.

ES6 Module Transpiler

Although ES6 standard is not implemented yet by most of browsers you can use the third party tools to get a taste of upcoming enhancements in JavaScript language. ES6 Module Transpiler library developed by the Square Engineers helps using the module authoring syntax from ES6 and compile it down to the transport formats that you learned earlier in this chapter.

Consider the following module circle.js:

A circle.js module
function area(radius) {
    return Math.PI * radius * radius;
}

function circumference(radius) {
    return 2 * Math.PI * radius;
}

export {area, circumference};

This module exports two functions: area() and circumference().

The main application’s script main.js can use these functions
import { area, circumference } from './circle';     // (1)

console.log("Area of the circle: " + area(2) + " meter squared");   // (2)
console.log("Circumference of the circle: " + circumference(5) + " meters");
  1. The import keyword specifies the objects we want to use from the module.

  2. A sample use of the imported functions

The ES6 Module Transpiler’s command compile-module can compile the module to be compliant with CommonJS, AMD, or the code that implements the Module pattern. With the type command line option you can specify that output format will be: amd, cjs and globals.

compile-modules circle.js --type cjs --to ../js/
compile-modules main.js --type cjs --to ../js/
  • CommonJS format

    circle.js
    "use strict";
    function area(radius) {
        return Math.PI * radius * radius;
    }
    
    function circumference(radius) {
        return 2 * Math.PI * radius;
    }
    
    exports.area = area;
    exports.circumference = circumference;
    main.js
    "use strict";
    var area = require("./circle").area;
    var circumference = require("./circle").circumference;
    
    console.log("Area of the circle: " + area(2) + " meter squared");
    console.log("Circumference of the circle: " + circumference(5) + " meters");

Should we compiled the modules into the AMD format using the option amd, we would have received a different output in the AMD format.

  • AMD format

    circle.js
    define("circle",
      ["exports"],
      function(__exports__) {
        "use strict";
        function area(radius) {
            return Math.PI * radius * radius;
        }
    
        function circumference(radius) {
            return 2 * Math.PI * radius;
        }
    
        __exports__.area = area;
        __exports__.circumference = circumference;
      });
    main.js
    define("main",
      ["./circle"],
      function(__dependency1__) {
        "use strict";
        var area = __dependency1__.area;
        var circumference = __dependency1__.circumference;
    
        console.log("Area of the circle: " + area(2) + " meter squared");
        console.log("Circumference of the circle: " + circumference(5) + " meters");
      });

Using the globals option in the compile-modules command line produces the code that can be used as described in the Module Pattern section earlier in the chapter.

  • Browser globals

    circle.js
    link:include/ch6_es6_globals_circle.js[role=include]
    main.js
    link:include/ch6_es6_globals_main.js[role=include]

To learn the up-to-date information on the ES6 browsers' support visit ECMAScript 6 compatibility table.

Note
TypeScript is an open-source language from Microsoft that compiles to JavaScript and brings object-oriented concepts like classes and modules to JavaScript. It has a module syntax, which is very similar to what ES6 standard proposes. The TypeScript compiler can produce CommonJS and AMD module formats. You can learn more about TypeScript from language specification.

Dicing the Save The Child Application Into Modules

Now that you know the basics of AMD and different modularization patterns, let’s see how you can dice our Save The Child application into smaller pieces. In this section we’ll apply the AMD-complaint module loader from the framework RequireJS.

Tip
curl.js offers another AMD-compliant asynchronous resource loader. Both curl.js and RequireJS have similar functionality, and to learn how they differ follow this thread on RequireJS group.

Let’s start with a brief explanation of the directory structure of the modularized Save The Child application.

fig 07 05
Figure 2. A directory structure of Save The Child
  1. All application’s JavaScript files reside in the app/modules directory.

    Inside the modules directory you can have as many nested folders as you want, e.g. utils folder.

  2. The application assets remain the same as in previous chapters.

  3. We keep all Bower-managed dependencies in the bower_components directory such as RequireJS, jQuery etc.

  4. The dist directory serves as the location for the optimized version of our application. We will cover optimization with r.js later in the Using RequireJS Optimizer section.

  5. The QUnit/Jasmine tests will live in the test directory. Testing will be covered in Chapter 8.

We are not going to dice the Save The Child application into multiple modules, but will just show you how to start this process. The modules graph of Save The Child illustrates the modules' dependencies. For example, the main module depends on login, svg-pie-chart, campaigns-map, donation, and generic. There is also a group of modules that will be loaded on demand: whereWeWork, whatWeDo, wayToGive, whoWeAre.

fig 07 08
Figure 3. The modules graph of Save The Child

To dice the application into modules you’ll need the the modularization framework RequireJS, which can either be downloaded from its github repository or you can install it using a package manager Bower that was explained in previous chapter on automation tools.

Once RequireJS downloaded and placed into the project directory, add it to the index.html file as demonstrated in Adding RequireJS to the web page snippet.

Adding RequireJS to the web page
<!DOCTYPE html>
<head>
    <!-- content omitted -->
</head>
<body>
<!-- page body -->

<script src="bower_components/requirejs/require.js"
        data-main="app/config"></script> <!--(1)-->

</body>
</html>
  1. Once the RequireJS library is loaded it will look for the data-main attribute and attempt to load the app/config.js script asynchronously. The app/config.js will become the entry point of our application.

Inside RequireJS configuration: config.js

RequiredJS uses a configuration object that includes modules and dependencies that have to be managed by the framework.

link:include/ch6_require_config.js[role=include]
  1. The RequireJS documentation has a comprehensive overview of all configuration options. We’ve included some of them here.

  2. The paths configuration option defines the mapping for module names and their paths. The paths is used for module names and shouldn’t contain file extensions.

  3. After configuring the modules' paths we’re loading the 'main' module. The navigation of our application flow starts there.

Writing AMD Modules

Let’s take a closer look at the module’s internals that make it consumable by the RequireJS module loader.

link:include/ch7_module_contentLoader.js[role=include]
  1. As we discussed in the Asynchronous Module Definition section, the code that you want to expose as a module should be wrapped in the define() function call. The first parameter is an array of dependencies. The location of dependencies files is defined inside the config file. The dependency object doesn’t have the same name as the dependency string id. The order of arguments in the factory function should be the same as the order in the dependencies array.

  2. In this module we export only in the constructor function that returns the object that uses the render function to draw the visual component on the screen.

  3. The contentLoader object loaded from app/modules/util/new-content-loader.js (see the paths property in RequireJS config object), is instantiated by RequireJS and is ready to use.

RequreJS also supports the CommonJS module format with a slightly different signature of the define() function. This helps to bridge the gap between AMD and CommonJS. If your factory function accepts parameters but no dependency array, the AMD environment assumes that you wish to emulate the CommonJS module environment. The standard require, exports, and module variables will be injected as parameters into the factory.

Here is an example of CommonJS module format with RequireJS.

Using CommonJS module format in RequireJS
link:include/ch7_commonjs_require.js[role=include]
  1. The factory receives up to three arguments that emulate the CommonJS require, exports, and module variables.

  2. Export your module rather than returning it. You can export an object in two ways - assign the the module directly to module.exports as shown in this snippet, or set the properties on the exports object.

  3. In CommonJS, dependencies are assigned to local variables using the require(id) function.

Loading Modules On-Demand

As per the Save The Child modules graph, some components shouldn’t load when the application starts. Similar to Mercedes Benz website example, some functionality of Save The Child can be loaded later when user needs it. The user might never want to visit the "Where we work" section. Hence this functionality is a good candidate for the load on-demand module. You may want to load such a module on demand when the user clicks the button or selects a menu item.

At any given time a module can be in one of three states:

  • not loaded (module === null)

  • loading is in progress (module === 'loading')

  • fully loaded (module !== null).

Loading the module on demand
link:include/ch7_module_ondemand.js[role=include]
  1. Checking if module loading in progress.

  2. Don’t re-load the same module. If the module was already loaded just call the method to render the widget on the Web page.

  3. Setting the module into the intermediate state until it’s fully loaded.

  4. Once the whereWeWork module is loaded, the callback will receive the reference to this module - instantiate whereWeWork and render it on the page.

Let’s apply the technique demonstrated in Loading the module on demand snippet for Save The Child application to lazy load the "Who We Are", "What We Do", "Where We Work" and "What To Give" modules only if the user clicked on the corresponding top bar link.

The Main Module
link:include/ch7_main_module.js[role=include]
  1. The first argument of the define function is an array of dependencies.

  2. Here we’re using the approach described in the Loading the module on demand section. This factory function produces the handler for the button click event. It uses RequireJS API to load the module once the user clicked on the button.

  3. Instantiate the click handler function using onDemandLoadingClickHandlerFactory and assign it to the button defined in the module config.

  4. An array of modules that can be loaded on demand.

  5. In the last step, we need to initialize each module button with the lazy loading handler.

RequireJS plugins

RequireJS plugins are special modules that implement specific API. For example, the text plugin allows to specify a text file as a dependency, cs! translates CoffeeScript files into JavaScript. The plugin’s module name comes before the ! separator. Plugins can extend the default loader’s functionality.

In the Save The Child application we use the order.js plugin that allows to specify the exact order in which the dependencies should be loaded. You can find the full list of the available RequireJS plugins at the following wiki page.

Using RequireJS Optimizer

RequireJS comes with the optimization tool called r.js, which is a utility that performs module optimization. Earlier in this chapter, we’ve specified the dependencies as an array of string literals that are passed to the top-level require and define calls. The optimizer will combine modules and their dependencies into a single file based on these dependencies.

Furthermore, r.js integrates with other optimization tools like UglifyJS and Closure Compiler to minify the content of script files. We are going to use the JavaScript task runner Grunt that you learned in previous chapter on automation tools.

Let’s configure our Grunt project to enable the optimization task. Here’s the command to install RequireJS, and Grunt’s task packages clean, concat and uglify and save them as development dependencies in the file package.json:

Adding dependencies to package.json
> npm install grunt-contrib-requirejs\
grunt-contrib-concat grunt-contrib-clean\
grunt-contrib-uglify --saveDev

The following listing describes the script to setup RequireJS optimizer and the related optimization tasks for Grunt. You’ll need to run this script to generate optimized version of the Save The Child application.

link:include/ch7_rjs_Gruntfile.js[role=include]
  1. The clean task cleans output directory. In the files section of task config we specify what folder should be cleaned.

  2. The requirejs task. The configuration properties of requrejs task are self-explanatory. mainConfigFile points at the same file that data-main attribute of RequireJS script tag. out parameter specifies output directory where optimized script will be created.

  3. The concat task combines/concatenates optimized modules code and RequireJS loader code.

  4. The uglify task minifies provided files (see src properties) using UglifyJS - a compressor/minifier tool. UglifyJS produces significantly smaller version of the original script by reducing the Abstract Syntax Tree (AST) and changing local variable names to single-letters.

  5. Loading plugins that provide necessary tasks.

  6. The default task to execute all tasks in order.

Run the Save The Child application built with RequireJS and monitor the network traffic in Chrome Developer Tools. You’ll see many HTTP requests that load modules asynchronously. As you can see from the from following screenshot, 12 out of 14 browser’s requests are for loading all required modules. The modules that may be loaded on demand are not here.

fig 07 02
Figure 4. Unoptimized version of the Save The Child Application

The next screenshot shows loading of the Save The Child application optimized with RequireJS optimizer. We’ve managed to pack all our modules, their dependencies and loader’s code into a single file, which considerably decreased the number of the server-side calls.

fig 07 03
Figure 5. Loading of optimized version of the Save The Child
Tip
Read more on optimization topics in the RequireJS documentation site under ``Optimization'' section.

RequireJS took care of the optimal module loading, but you should properly arrange the inter-module communication. The Save The Child application doesn’t have modules that heed to exchange data so we’ll describe how to properly arrange inter-module communications in a separate application.

Tip
Google has created PageSpeed Insights, a Web tool that offers suggestions for improving the performance of your Web application on all devices. Just enter the URL of your application and a second later you’ll see some optimization suggestions.

Loosely-Coupled Inter-Module Communications With Mediator

Almost any complex enterprise Web application consists of a number of components and modules.

A simple approach of arranging communications between the components is to allow all these components directly accessing public properties of each other. This would produce an application with tightly coupled components that know about each other and removal of one component could lead to multiple code changes in the application.

A better approach is to create loosely coupled components that are self-contained, do not know about one another and can communicate with the "outside world" by sending and receiving events.

Creating UI of from reusable components applying messaging techniques requires creation of loosely coupled components. Say you’ve created a window for a financial trader. This window gets a data push from the server showing the latest stock prices. When the trader likes the price he may click on the Buy or Sell button to initiate a trade. The trading engine can be implemented in a separate component and establishing inter-component communications the right way is really important.

As you’ve learned from Chapter 2, Mediator is a behavioral design pattern that allows to unify communications of the application components. The Mediator pattern promotes the use of a single shared object that handles (mediates) communication between other objects. None of the components is aware of the others, but each of them knows about a single object - the mediator.

In Chapter 2 we’ve introduced an example of a small fragment of a trader’s desktop. Let’s reuse the same example, but this time not with postMessage but with the Mediator object.

In Figure Before the trader clicked on the Price Panel, the Pricing Panel on the left gets the data feed about the current prices of the IBM stock. When the user clicks on Bid or Ask button, the Pricing Panel just sends the event with the relevant trade information, e.g. a JSON-formatted string containing the stock symbol, price, buy or sell flag, date, etc.

fig 07 06
Figure 6. Before the trader clicked on the Price Panel
fig 07 07
Figure 7. After the trader clicked on the Price Panel

Here is a HTML code snipped that implements the above scenario.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>An example of Mediator Design Pattern</title>
    <script data-main="app/config" src="bower_components/requirejs/require.js"></script>
</head>
<body>
    <h1>mediator and RequireJS example</h1>

    <div id="pricePanel">       <!--(1)-->
        <p>IBM</p>
        <label for="priceInput">Bid:</label>
        <input type="text" id="priceInput" placeholder="bid price"/>

        <label for="priceInput">Ask:</label>
        <input type="text" id="priceInput" placeholder="bid price"/>
    </div>
    <div id="orderPanel">       <!--(2)-->
        <p id="priceText"></p>
        <label for="quantityInput">Quantity:</label>
        <input type="text" id="quantityInput" placeholder=""/>
        <button id="goButton">Go!</button>
        <button id="cancelButton">cancel</button>
    </div>

</body>
</html>
  1. This div element contains Pricing Panel with Bid and Ask controls.

  2. This div element contains Order Panel with Quantity, Go and Cancel controls.

As we stated before, we need a Mediator to handle communication between the application components. The components need to register themselves with the Mediator so it knows about them and can route communications. The following snippet is a sample Mediator implementation (we use define and require from RequireJS here).

The implementation of Mediator pattern.
link:include/ch7_mediator.js[role=include]
  1. Return the private object that stores registered components.

  2. With the Mediator.register() function we can store components in the associative array. The Mediator is a singleton object here.

  3. Assigning the mediator instance to the component being registered.

  4. Registering component in the array using provided name as key.

  5. The component can invoke Mediator.broadcast() when it has some information to share with other application components.

  6. If a component has a function property with the name matching the pattern "on" + event, e.g. onClickEvent, the Mediator will invoke this function in the context of source object.

The following code sample shows the main entry point of the application that uses the above mediator.

link:include/ch7_mediator_usage.js[role=include]
  1. Required modules will be loaded by RequireJS.

  2. Registering our components with the Mediator.

  3. Adding the click event listener for the Bid Price component.

  4. When the user clicks on the bid price the Mediator will broadcast the BidClick event to all registered component. Only the component that has this specific event handler with the name matching the pattern "on" + event will receive this event.

Let’s see how the code of the PricePanel and OrderPanel components looks like.

link:include/ch7_pricepanel.js[role=include]
  1. The setter of the Mediator object. Mediator injects its' instance during component registration (refer to The implementation of Mediator pattern. snippet).

  2. The getter of the Mediator object.

  3. The onBidClick event handler. The Mediator will call this function when the BidClick event will be broadcast. Using getter getMediator we can broadcast PlaceBid event to all registered components.

link:include/ch7_orderpanel.js[role=include]
  1. The Mediator’s getter and setter have the purpose similar to described in previous snippet.

  2. Defining the PlaceBid event handler - onPlaceBid().

As you noticed, both OrderPanel and PricePanel don’t know about the existence of each other, but nevertheless they can sent and receive data with the help of intermediary - the Mediator object.

The introduction of the Mediator increases reusability of components by decoupling them from each other. The Mediator pattern simplifies the maintenance of any application by centralizing the navigational logic.

Summary

The size of any application tends to increase with time, and sooner or later you’ll need to decide how to cut it into several loadable blocks of functionality. The sooner you start modularizing your application the better.

In this chapter we’ve reviewed several options available for writing modular JavaScript using modern module formats. These formats have a number of advantages over using just the classical Module Pattern. These advantages include avoiding creating global variables for each module and better support for static and dynamic dependency management.

Understanding various technologies and frameworks available in JavaScript, combined with the knowledge of different ways of linking modules and libraries is crucial for developers who want their JavaScript applications to be more responsive.