Sharing code with the browser

One of the main selling point 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 some small, self-contained and generic fragments of code, developing 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. Most of the effort required when developing for both the platforms is making sure to reduce those differences to the 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.

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 the 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.

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:

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

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 probably is the most popular and allows us to support the most common module systems, which are AMD, CommonJS, and browser globals.

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

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 1, Node.js Design Fundamentals. 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 the 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 we 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. Also, you can see how the dependency, Mustache, is expected to be in the global scope as well.
  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.

When writing a Node.js application, the last thing we want to do is to manually add support for a module system different from the one offered, by default, by the platform. The ideal situation would be continuing to write our modules as we have always done, using require() and module.exports, and then use a tool to transform our code into a bundle that can easily run in the browser. Luckily, this is a problem that has already been solved by many projects, among which Browserify (http://browserify.org) is the most popular and broadly supported.

Browserify allows us to write modules using the Node.js module conventions and then, thanks to a compilation step, it creates a bundle (a single JavaScript file) that contains all the dependencies our modules need for working, including an abstraction of the require() function. This bundle can then be easily included into a web page and executed inside a browser. Browserify recursively scans our sources looking for references of the require() function, resolving, and then including the referenced modules into the bundle.

To quickly demonstrate how this magic works, let's see how the umdModule we created in the previous section looks like if we use Browserify. First, we need to install Browserify itself, we can do it with a simple command:

The -g option will tell npm to install Browserify globally, so that we can access it using a simple command from the console, as we will see in a moment.

Next, let's create a fresh project and let's try to build a module equivalent to the umdModule we created before. This is how it looks like if we had to implement it in Node.js (file sayHello.js):

Definitely simpler than applying a UMD pattern, isn't it? Now, let's create a file called main.js that is the entry point of our browser code:

In the preceding code, we require the sayHello module in exactly the same way as we would do in Node.js, so no more annoyances for managing dependencies or configuring paths, a simple require() does the job.

Next, let's make sure to have mustache installed in the project:

Now, comes the magical step. In a terminal, let's run the following command:

The previous command will compile the main module and bundle all the required dependencies into a single file called bundle.js, which is now ready to be used in the browser!

To quickly test this assumption, let's create an HTML page called magic.html that contains the following code:

This is enough for running our code in the browser. Try to open the page and see it with your eyes. Boom!

Tip

During development, we surely don't want to manually run browserify at every change we make to our sources. What we want instead is an automatic mechanism to regenerate the bundle when our sources change. To do that, we can use watchify (https://npmjs.org/package/watchify), a companion tool that we can install by running the following command:

npm install watchify -g

Watchify can be used in the exact same way as browserify, the two tools have a similar purpose and command line options. The difference between the two is that watchify, after compiling the bundle for the first time, will continue to watch the sources of the projects for any change and will then rebuild the bundle automatically by processing only the changed files for maximum speed.

The magic of Browserify doesn't stop here. This is a (incomplete) list of features that make sharing code with the browser a simple and seamless experience:

The power and flexibility of Browserify are so captivating that many developers started to use it even to manage only client-side code, in place of more popular module systems such as AMD. This is also made possible by the fact that many client-side libraries are starting to support CommonJS and npm by default, opening new and interesting scenarios. For example, we can install JQuery as follows:

And then load it into our code with a simple line of code:

You will be surprised at how many client-side libraries already support CommonJS and Browserify.

When developing for different platforms, the most common problem we have to face is sharing the common parts of a component, while providing different implementations for the details that are platform-specific. We will now explore some of the principles and the patterns to use when facing this challenge.

The most simple and intuitive technique for providing different implementations based on the host platform is to dynamically branch our code. This requires that we have a mechanism to recognize at runtime the host platform and then switch dynamically the implementation with an if-else statement. If we are using Browserify, this is as simple as checking the variable process.browser, which is automatically set to true by Browserify when bundling our modules:

Some more generic approaches involve checking globals that are available only on Node.js or only in the browser. For example, we can check the existence of the window global:

Using a runtime branching for switching between Node.js and browser implementation is definitely the most intuitive and simple pattern we can use for the purpose; however it has some inconveniences:

This last inconvenience is due to the fact that bundlers have no way of knowing the value of a runtime variable at build-time (unless the variable is a constant), so they include any module regardless of whether it's required from reachable or unreachable code.

Most of the time, we already know at build-time what code has to be included in the client bundle and what shouldn't. This means that we can take this decision upfront and instruct the bundler to replace the implementation of a module at build-time. This often results in a leaner bundle, as we are excluding unnecessary modules, and a more readable code because we don't have all the if-else statements required by a runtime branching.

In Browserify, this module swapping mechanism can be easily configured in a special section of the package.json. For example, consider the following three modules:

In Node.js, moduleA is using the default implementation of the alert module, which will log a message to the console, in the browser though we want a proper alert pop up to show. To do that, we can instruct Browserify to swap at build time, the implementation of the alert.js module with clientAlert.js. All we need to do is to add a section such as the following into the package.json of a project:

This will result in every reference to the alert.js module being replaced with a reference to the clientAlert.js module. The first module will not even be included in the bundle.

We realize how build-time branching is much more elegant and powerful than runtime branching. On one side, it allows us to create modules that are focused on only one platform, and on the other, it provides a simple mechanism to exclude Node.js-only modules from the final bundle.

Now that we know how to switch between Node.js and browser code, the remaining piece of the puzzle is how to integrate this within our design and how we can create our components in such a way that some of their parts are interchangeable. These challenges should not sound new to us at all, in fact, all throughout the book we have seen, analyzed, and used patterns to achieve this very purpose.

Let's remind some of them and describe how they apply to cross-platform development:

As we can see, the arsenal of patterns at our disposal is quite powerful, but the most powerful weapon is still the ability of the developer to choose the best approach and adapt it to the specific problem at hand. In the next section, we are going to put what we learned into action, leveraging some of the concepts and patterns we have seen so far.

As a perfect conclusion for this section and chapter, we are now going to work on an application more complex than usual to demonstrate how to perform code sharing between Node.js and the browser. We will take as example a personal contact manager application with very basic functionalities.

In particular, we are only interested in some basic CRUD operations such as listing, creating, and removing contacts. But the main feature of our application, the one that we are really interested in exploring, is the sharing of the models between the server and the client. This is actually one of the most sought after capabilities when developing an application that has to validate and process data both on the client and on the server, which is what most of the modern applications actually need to do.

To give you an idea, this is how our application should look like once it's completed:

Sharing business logic and data validation using Backbone Models

The plan is to use a familiar stack on the server with express and levelup, Backbone Views (http://backbonejs.org) on the client, and a set of Backbone Models shared between Node.js and the browser, to implement persistence and validation. Browserify is our choice for bundling the modules for the browser. If you don't know Backbone, don't worry, the concepts we are going to demonstrate here are generic enough and can be understood also without any knowledge of this framework.

Let's start from the focal center of our application, the Backbone models wewant to share with the browser. In our application, we have two models: Contact, a Backbone Model, and Contacts, a Backbone Collection. Let's see how the Contact module looks like (the models/Contact.js file):

Most of the preceding code is shared between the browser and the server, namely, the logic for setting the default attributes values and their validation. Both the defaults() and validate() methods are part of the Backbone framework and are overridden to provide the custom logic for our model. We also added an extra field to the object, called collectionName, that will be used by the server for persisting the model in the right sublevel (we will see this later) and by the client in order to calculate the URL of the REST API endpoint (the url field).

Now, comes the best part: when a Backbone model is saved, deleted, or fetched (using save(), remove(), and fetch() respectively), Backbone internally delegates the task to the sync() method of the model. Sounds familiar? This is actually a Template pattern and it's perfect for us to perform a build-time branching of our code. That's in fact where the models must have a different behavior based on the target environment:

In the code fragment given earlier, the sync attribute is a function loaded from the modelSync module, which represents our server-side implementation of the method. This is how it looks like (the models/modelSync.js file):

When the internals of the Backbone Model invoke the sync() method, three parameters are provided, as follows:

In the preceding code, we are showing what happens when we receive a 'create' request. As we can see, the saveModel() function is invoked, which saves the model into the database.

The sync() implementation we have just seen, is meant to be executed only on the server, where we want to persist the data. Ideally, it could also work on the browser, because LevelUP has adapters for IndexedDB and LocalStorage, but that's not what we want in this example.

What we want instead is to persist the data on the server, and to do this we have to invoke a web service when an operation is performed on the model. This means that the modelSync module is not good for us to use on the browser, so we need a different implementation. Luckily, Backbone already provides a default implementation for the sync() method that does exactly what we need. So that's what we are going to use on the client-side implementation of the modelSync module (file: models/clientSync.js):

That's it, the next step is to instruct Browserify to use the module we just created in place of modelSync when creating the client-side bundle. As we have seen, this can be done in the package.json file:

The preceding few lines create a build-time branching telling Browserify to replace any reference to the module "./models/modelSync.js" with a reference to "./models/clientSync.js". The module modelSync will not be included in the final bundle.

At this point, the Contact module should be isomorphic, which means that it can run transparently both on the client and on Node.js. To show how this looks like, let's see how the model is used in the server routes (file routes.js):

The createContact() handler builds a new contact (Contact) from the JSON data received in the body of the request (a POST to the '/contacts' URL). Then, we attach to the model a listener for the invalid event, which triggers when its attributes do not pass the validation tests we have defined. Finally, contact.save() will persist the data in the database.

As we will see, this is exactly what we do in the browser-side of the application as well. This happens in the Backbone View responsible for handling the data submitted in a form (file client/ContactsView.js):

As we can see, when the createContact() function is invoked (after the "new contact" form is submitted), we issue the exact same commands we used on the server:

As we wanted to demonstrate, our Contact model is isomorphic and enables us to share its business logic and validation between the browser and the server!

To run the full sample distributed with the book, don't forget to install all the modules with:

Then, run Browserify on the main module of the client-side application to generate the bundle used on the browser:

Then finally, fire up the server with:

Now, we can open our browser at the following URL to access the application http://localhost:8080.

We can now verify that the validation is actually performed identically on the browser as it is on the server. To check this on the browser, we can simply try to create a contact with a phone number that contains letters, which will fail the validation. Then, to test the server-side validation, we can try to invoke the REST API directly with curl:

The preceding command should return an error indicating that the data we are trying to save is invalid.

This concludes our exploration of the fundamental principles for sharing code between Node.js and the browser. As we have seen, the challenges are many and the effort to design isomorphic code can be substantial. In this context, it's worth mentioning that one big challenge related to this area is shared rendering, which is the ability to render a view on the server as well as dynamically on the client. This requires a much more complex design effort that easily affects the entire architecture of the application on both the server and the browser. Many frameworks tried to solve this ultimate challenge, which usually is the most complex in the area of cross-platform JavaScript development. Among those projects, we can find Derby (http://derbyjs.com), Meteor (https://www.meteor.com), React (http://facebook.github.io/react), and then Rendr (https://github.com/rendrjs/rendr) and Ezel (http://ezeljs.com), which are based on Backbone, similar to what we did in our example.