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 as default by the platform. The ideal situation would be to continue writing 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 problem has already been solved by many projects, among which Webpack (https://webpack.github.io) is one of the most popular and broadly adopted.
Webpack 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 in the browser (including an abstraction of the require()
function). This bundle can then be easily included into a web page and executed inside a browser. Webpack recursively scans our sources and looks for references of the require()
function, resolving and then including the referenced modules into the bundle.
Webpack is not the only tool we have for creating browser bundles from Node.js modules. Other popular alternatives are Browserify
(http://browserify.org), RollupJs
(http://rollupjs.org) and Webmake
(https://npmjs.org/package/webmake). In addition, require.js
allows us to create modules for both the client and Node.js but it uses AMD in place of CommonJS (http://requirejs.org/docs/node.html).
To quickly demonstrate how this magic works, let's see how umdModule
, we created in the previous section looks, if we use Webpack. First, we need to install Webpack itself; we can do so with a simple command:
npm install webpack -g
The -g
option will tell npm
to install Webpack 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 if we had to implement it in Node.js (file sayHello.js
):
var mustache = require('mustache'); var template = '<h1>Hello <i>{{name}}</i></h1>'; mustache.parse(template); module.exports.sayHello = function(toWhom) { return mustache.render(template, {name: toWhom}); };
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:
window.addEventListener('load', function(){
var sayHello = require('./sayHello').sayHello;
var hello = sayHello(Browser!');
var body = document.getElementsByTagName("body")[0];
body.innerHTML = hello;
});
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:
npm install mustache
Now comes the magical step. In a terminal, let's run the following command:
webpack main.js bundle.js
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:
<html> <head> <title>Webpack magic</title> <script src="bundle.js"></script> </head> <body> </body> </html>
This is enough for running our code in the browser. Try to open the page and see it with your eyes. Boom!
During development, we don't want to manually run Webpack 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 the --watch
option when running the Webpack command. This option will keep Webpack running continuously and it will take care to re-compile our bundle every time one of the related source files changes.
The magic of Webpack doesn't stop here. This is a (incomplete) list of features that make sharing code with the browser a simpler and seamless experience:
http
, assert
, or events
, and many more, in the browser!require()
, from minification to the compilation and bundling of other assets such as templates and stylesheets.The power and flexibility of Webpack are so captivating that many developers started to use it even to manage client-side only code. 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:
npm install jquery
And then, we can load it into our code with a simple line of code:
const $ = require('jquery');
You will be surprised at how many client-side libraries already support CommonJS and Webpack.
As we said in the previous paragraph, one of the main advantages of Webpack is the ability to use loaders and plugins to transform the source code before bundling it.
Throughout this book we have been using many of the new handy features offered by the ES2015 standard, and we would love to keep using it even when working on a universal JavaScript application. In this section, we are going to see how to leverage the loader feature of Webpack to re-write the previous example using the ES2015 syntax within our source modules. With the proper configuration, Webpack will take care to transpile the resulting code for the browser to ES5 to guarantee the maximum compatibility with all the currently available browsers.
First of all, let's move our modules to a new src
folder. This will make it easier for us to organize our code and separate the transpiled code from the original source code. This separation will also make it easier for us to configure Webpack properly and simplify the way we invoke Webpack from the command line.
Now we are ready to rewrite our modules. The ES2015 of our src/sayHello.js
will look like this:
const mustache = require('mustache'); const template = '<h1>Hello <i>{{name}}</i></h1>'; mustache.parse(template); module.exports.sayHello = toWhom => { return mustache.render(template, {name: toWhom}); };
Notice that we are using const
, let
, and the arrow function syntax.
We can now update our src/main.js
file to ES2015. Our src/main.js
file instead can be re-written as follows:
window.addEventListener('load', () => { const sayHello = require('./sayHello').sayHello; const hello = sayHello('Browser!'); const body = document.getElementsByTagName("body")[0]; body.innerHTML = hello; });
Now we are ready to define the webpack.config.js
file:
const path = require('path'); module.exports = { entry: path.join(__dirname, "src", "main.js"), output: { path: path.join(__dirname, "dist"), filename: "bundle.js" }, module: { loaders: [ { test: path.join(__dirname, "src"), loader: 'babel-loader', query: { presets: ['es2015'] } } ] } };
This file is a module that exports a configuration object that will be read by Webpack when we invoke it from the command line without any argument.
In the configuration object, we are defining the entry point as our src/main.js
file and the destination for our bundle file as dist/bundle.js
.
This part was quite self-explanatory, so let's now have a look at the loaders array. This optional array allows us to specify a set of loaders that can alter the content of our source files while Webpack constructs our bundle file. The idea is that every loader represents a specific transformation (in this case, ES2015 to ES5 using babel-loader
) and it is applied only if the current source file matches the specific test
expression defined for the loader. In this example, we are telling Webpack to use babel-loader
on all the files coming from our src
folder and to apply the es2015
preset as Babel option.
Now we are almost ready; the only missing step before running Webpack is to install Babel and the ES2015 preset with the following command:
npm install babel-core babel-loader babel-preset-es2015
Now, to generate your bundle, you can simply run:
webpack
Remember to reference the new dist/bundle.js
in your magic.html
file. You should be able to open it in the browser and see that everything is still working properly.
If you are curious, you can read the content of the freshly generated bundle file and you will discover that all the ES2015 features we used in the source files have been converted to the equivalent code valid in ES5, which every browser on the market can execute just fine.