Chapter 8. Universal JavaScript for Web Applications

JavaScript was born in 1995 with the original goal of giving web developers the power to execute code directly in the browser and build more dynamic and interactive websites.

Since then, JavaScript has grown up a lot and it is today one of the most famous and widespread languages in the world. If, at the very beginning, JavaScript was a very simple and limited language, today it can be considered a complete general purpose language that can be used even outside the browser to build almost any kind of application. In fact, JavaScript now powers frontend applications, web servers, and mobile applications, as well as embedded devices such as wearable devices, thermostats, and flying drones.

This availability across platforms and devices is fostering a new trend among JavaScript developers, which is being able to simplify code reuse across different environments in the same project. The most meaningful case in relation to Node.js regards the opportunity to build web applications where it is easy to share code between the server (backend) and the browser (frontend). This quest for code reuse was originally identified with the term Isomorphic JavaScript, but it's now widely recognized as Universal JavaScript.

In this chapter, we are going to explore the wonders of Universal JavaScript, specifically in the field of web development, and discover many tools and techniques to be able to share most of our code between the server and the browser.

In particular, we are going to learn how modules can be used both on the server and the client and how to use tools such as Webpack and Babel to package them for the browser. We will adopt the React library and other famous modules to build the web interface and share the state of the web server with the frontend, and finally we are going to explore some interesting solutions to enable universal routing and universal data retrieval within our apps.

At the end of this chapter, we should be able to write React-powered Single-Page Applications (SPAs) that reuse most of the code that is already present in our Node.js server, resulting in applications that are consistent, easy to reason about, and easy to maintain.

Sharing code with the browser

One of the main selling points of Node.js is the fact that it's based on JavaScript and runs on V8, an engine that actually powers one of the most popular browsers: Chrome. We might think that that's enough to conclude that sharing code between Node.js and the browser is an easy task; however, as we will see, this is not always true, unless we want to share only small, self-contained, and generic fragments of code. Developing code for both the client and the server requires a non-negligible level of effort in making sure that the same code can run properly in two environments that are intrinsically different. For example, in Node.js we don't have the DOM or long-living views, while in the browser we surely don't have the filesystem or the ability to start new processes. Moreover, we need to consider that we can safely use many of the new ES2015 features in Node.js. We cannot do the same in the browser, as the majority of browsers are still stuck with ES5, and running ES5 code in the client will remain the safest option for a relatively long time before ES2015-enabled web browsers are ubiquitous.

So, most of the effort required when developing for both platforms is making sure to reduce those differences to a minimum. This can be done with the help of abstractions and patterns that enable the application to switch, dynamically or at build time, between the browser-compatible code and the Node.js code.

Luckily, with the rising interest in this new mind-blowing possibility, many libraries and frameworks in the ecosystem have started to support both environments. This evolution is also backed by a growing number of tools supporting this new kind of workflow, which over the years have been refined and perfected. This means that if we are using an npm package on Node.js, there is a good probability that it will work seamlessly on the browser as well. However, this is often not enough to guarantee that our application can run without problems on both the browser and Node.js. As we will see, a careful design is always needed when developing cross-platform code.

In this section, we are going to explore the fundamental problems we might encounter when writing code for both Node.js and the browser, and we are going to propose some tools and patterns that can help us in tackling this new and exciting challenge.

Sharing modules

The first wall we hit when we want to share some code between the browser and the server is the mismatch between the module system used by Node.js and the heterogeneous landscape of the module systems used in the browser. Another problem is that in the browser, we don't have a require() function or the filesystem from which we can resolve modules. So, if we want to write large portions of code that can work on both platforms and we want to continue to use the CommonJS module system, we need to take an extra step—we need a tool to help us in bundling all the dependencies together at build time and abstracting the require() mechanism on the browser.

Universal Module Definition

In Node.js, we know perfectly well that the CommonJS modules are the default mechanism for establishing dependencies between components. The situation in browser-space is unfortunately way more fragmented:

  • We might have an environment with no module system at all, which means that globals are the main mechanism to access other modules
  • We might have an environment based on an Asynchronous Module Definition (AMD) loader, for example, RequireJS (http://requirejs.org)
  • We might have an environment abstracting the CommonJS module system

Luckily, there is a pattern called Universal Module Definition (UMD) that can help us abstract our code from the module system used in the environment.

Creating an UMD module

UMD is not quite standardized yet, so there might be many variations that depend on the needs of the component and the module systems it has to support. However, there is one form that is probably the most popular and also allows us to support the most common module systems, such as AMD, CommonJS, and browser globals.

Let's see a simple example of how it looks. In a new project, let's create a new module called umdModule.js:

(function(root, factory) {                           //[1] 
  if(typeof define === 'function' && define.amd) {   //[2] 
    define(['mustache'], factory); 
  } else if(typeof module === 'object' &&            //[3] 
      typeof module.exports === 'object') { 
    var mustache = require('mustache'); 
    module.exports = factory(mustache); 
  } else {                                           //[4] 
    root.UmdModule = factory(root.Mustache); 
  } 
}(this, function(mustache) {                         //[5] 
  var template = '<h1>Hello <i>{{name}}</i></h1>'; 
  mustache.parse(template); 
 
  return { 
    sayHello:function(toWhom) { 
      return mustache.render(template, {name: toWhom}); 
    } 
  }; 
})); 

The preceding example defines a simple module with one external dependency: mustache (http://mustache.github.io), which is a simple template engine. The final product of the preceding UMD module is an object with one method called sayHello() that will render a mustache template and return it to the caller. The goal of UMD is integrating the module with other module systems available on the environment. This is how it works:

  1. All the code is wrapped in an anonymous self-executing function, very similar to the Revealing Module pattern we have seen in Chapter 2, Node.js Essential Patterns. The function accepts a root that is the global namespace object available on the system (for example, window on the browser). This is needed mainly for registering the dependency as a global variable, as we will see in a moment. The second argument is factory() of the module, a function returning an instance of the module and accepting its dependencies as input (Dependency Injection).
  2. The first thing we do is check whether AMD is available on the system. We do this by verifying the existence of the define function and its amd flag. If found, it means that we have an AMD loader on the system, so we proceed with registering our module using define and requiring the dependency mustache to be injected into factory().
  3. We then check whether we are in a Node.js-flavored CommonJS environment by checking the existence of the module and module.exports objects. If that's the case, we load the dependencies of the module using require() and provide them to the factory(). The return value of the factory is then assigned to module.exports.
  4. Lastly, if we have neither AMD nor CommonJS, we proceed with assigning the module to a global variable, using the root object, which in a browser environment will usually be the window object. In addition, you can see how the dependency, Mustache, is expected to be in the global scope.
  5. As a final step, the wrapper function is self-invoked, providing the this object as root (in the browser, it will be the window object) and providing our module factory as a second argument. You can see how the factory accepts its dependencies as arguments.

It's also worth underlining that in the module, we haven't used any ES2015 feature. This is to guarantee that the code will run fine even in the browser without any modification.

Now, let's see how we can use this UMD module in both Node.js and the browser.

First of all, we create a new testServer.js file:

const umdModule = require('./umdModule'); 
console.log(umdModule.sayHello('Server!')); 

If we execute this script, it will output the following:

<h1>Hello <i>Server!</i></h1>

If we want to use our freshly baked module on the client as well, we can create a testBrowser.html page with the following content:

<html> 
  <head> 
    <script src="node_modules/mustache/mustache.js"></script> 
    <script src="umdModule.js"></script> 
  </head> 
  <body> 
    <div id="main"></div> 
    <script> 
       document.getElementById('main').innerHTML = 
         UmdModule.sayHello('Browser!'); 
    </script> 
  </body> 
</html> 

This will produce a page with a big fancy Hello Browser! as the title of the page.

What has happened here is that we included our dependencies (mustache and our umdModule) as regular scripts in the head of the page and we then created a small inline script that uses UmdModule (available as a global variable in the browser) to generate some HTML code that was then placed inside the main block.

Tip

In the code examples available for this book on the Packt Publishing website, you can find other examples showing how the UMD module we just created can also be used in combination with an AMD loader and a CommonJS system.

Considerations on the UMD pattern

The UMD pattern is an effective and simple technique used for creating a module compatible with the most popular module systems out there. However, we have seen that it requires a lot of boilerplate, which can be difficult to test in each environment and is inevitably error-prone. This means that writing the UMD boilerplate manually can make sense for wrapping a single module which has already been developed and tested. It is not a practice to use when we are writing a new module from scratch; it is unfeasible and impractical, so in these situations, it is better to leave the task to tools that can help us automate the process. One of those tools is Webpack, which we will use in this chapter.

We should also mention that AMD, CommonJS, and browser globals are not the only module systems out there. The pattern we have presented will cover most of the use cases, but it requires adaptations to support any other module system. For example, the ES2015 module specification is something that we are going to discuss in the next section of the chapter as it offers a number of advantages over other solutions and it is already part of the new ECMAScript standard (even if at the time of writing it is not natively supported in Node.js).

Tip

You can find a broad list of formalized UMD patterns at https://github.com/umdjs/umd.

ES2015 modules

One of the features introduced by the ES2015 specification is a built-in module system. This is the first time we will encounter it in this book because, unfortunately, at the time of writing, ES2015 modules are still not supported in the current version of Node.js.

We will not describe this feature in detail here but it is important to know about it because it will most likely become the go-to module syntax for years to come. Apart from being standard, ES2015 modules introduce a nicer syntax and a number of advantages over the other module systems we just discussed.

The goal for ES2015 modules was to take the best out of CommonJS and AMD modules:

  • Like CommonJS, this specification provides a compact syntax, a preference for single exports, and support for cyclic dependencies
  • Like AMD, it offers direct support for asynchronous loading and configurable module loading

Moreover, thanks to the declarative syntax, it is possible to use static analyzers to perform tasks such as static checking and optimizations. For instance, it is possible to analyze the dependency tree of a script and create a bundled file for the browser where all the unused functions of the imported modules are stripped, thus providing a more compact file on the client and reducing load times.

Note

To learn more about the syntax of the ES2015 modules you can have a look at the ES2015 specification: http://www.ecma-international.org/ecma-262/6.0/#sec-scripts-and-modules.

Today, you can also use the new module syntax in Node.js, adopting a transpiler such as Babel. Actually, many developers are advocating it while presenting their own solutions for building Universal JavaScript apps. It's generally a good idea to be future-proof, especially because this feature is already standardized and will eventually be part of the Node.js core. For the sake of simplicity, we will stick to the CommonJS syntax also all over this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.81.201