(https://github.com/amdjs/amdjs-api/wiki/AMD, http://requirejs.org/docs/whyamd.html). One of the most popular AMD loader solutions is the require.js and in this article we will look how to use it.
Single-file car dealer shop application
As the oversimplified sample application we are going to build a small car/bike dealer application. The dealer sells two type of vehicles, cars and bikes. As an owner of the dealer's shop we are able to buy a new item and put in on the stock or we can sell it and earn some money. All transactions are reflected in the bank account, when buying things we need to pay for them if we have enough money, when selling our earnings are deposited.
Here is a screenshot of the executed script.
Breaking modules down into individual script files
The first improvement that we can make is to break the modules into individual js files, so item module would go to the items/item.js, the same for items/car.js, items/bike.js, app/shop.js and app/bank.js. The main.js file has the code running the shop that is taken from the end of the app.js file. We don't need the app.js anymore.
The folder structure now looks like this:
We need to make a change to the index.html file:
If we test it again, it should work the same as it was working before with just a single script file.
But what would happen if we changed the order of the script tags and moved main.js one line up above the bank.js?
We broke the dependency order, because the main.js references the bank.js that wasn't loaded yet, the uncaught reference error is thrown. Of course in simple applications these dependencies are quite easy to manage, but imagine applications that consist of tens if not hundreds of different dependencies and modules.
Wrapping modules into require.js AMD type modules
With AMD we can avoid all of that. The first thing that we need to do is to wrap our modules in the define function callbacks and the main file using these modules in the require wrapper.
This is the item.js after wrapping in the define function, the content is literary copied and pasted inside of the callback function that is the first and only parameter to the define and the item object is returned from it. This structure is required to let other modules using it.
Because the bike.js and the car.js depend on the item.js, in the define method we additionally add that 'item' dependency. Note that an array with a string representing a name of the dependency is passed as the first parameter to the define method and then again as a parameter to the callback function that now became the second parameter of the define. Here is the bike.js example, the car.js is done in the similar way:
The bank.js would be wrapped in the same way as the item.js as it doesn't depend on any other module. But the shop.js is the module that has more dependencies as it depends on the car, the bike and the underscore library.
Now let's have a look at the main.js. This is our main application file that launches the application. Instead of wrapping in the define function, we wrap it into the require. We also add a config definition to specify the base folder and paths to different modules. Because the underscore library is not wrapped in the AMD compatible wrapper, we need to use a workaround to import it correctly (shim). The require function is similar in structure to define, as the first parameter we pass an array of dependencies and the second parameter is a callback that takes module names as the parameters. We do not return anything from the require callback.
The last thing to get our application working again is to modify the index.html file, there is no need to load all these different js script files anymore. We don't need to remember about order in which we need to load scripts as well - all this is managed for us automatically. The only script tag links to the require.js itself but it also has data-main attribute that points the require to the main script, note that there is no need to put the "js" extension at the end.
The files structure looks like this now:
And the application should execute in the same way as before we wrapped it into the AMD modules with requuire.js.
Optimising with r.js
Our application is broken into modules so it is easier for us developers to maintain and extend it. But having many different separate module files isn't a good thing for a user, browsers will need to make multiple network request to load all the modules. This isn't very problematic with a small application, but it may become an overhead with large scale applications depending on hundreds of different modules. The solution is to merge our modules into a single file. This is good for us as during the development we would still use separate files, that then would be merged during the build process (locally or on the continuous integration server) and from the end user's perspective there will be only one js file loaded with a single request from a browser.
For combining modules into one file we will use the r.js "adapter" that also includes the optimiser - all that is developed by the same team that works on the require.js project. Apart from just combining, the optimiser can also minify the script with UglifyJS. Full documentation of using the require optimizer is available here: http://requirejs.org/docs/optimization.html.
The r.js script can be run in a browser but I prefer to use it with Node.js as a part of the build process. To prepare our application for building, let's create a new build configuration file build.js. It is not required as we could pass build properties as options when executing the build process in the command line but it is certainly easier to manage it this way. In the build.js we specify the location of the run-time configuration file (that we will just create in a moment), name of the main application file (main.js in our case), location for the output (processed) file that will be in the "build" folder and finally I set the optimisation parameter to none for now as we want to see first how the combined modules look like without "minificating" it. Let's call this build file "build-dev" as it will not be optimised for production - not minified/uglified and all code comments will be kept there.
I also moved the require.config from the main.js to a separate file config.js (this file is called the runtime config file).
So we are ready to run the script. In the terminal, in the root of our project folder, run this command:
If everything is in place correctly you should be able to see a result like below:
But what is most important, the build file is placed in the build folder, so let's have a look at it. As you can see this is just pure merge of our modules, the underscore lib comes first and the other modules follow it.
So let's link our index.html directly to this file and see how it works:
Of course you should see an error like this:
We didn't add the require.js library as one of the dependencies! Let's update the build-dev.js file by adding a path to the require.js script file (property is named "requireLib" as "require" word is already reserved):
Now when you run the build script again, you will notice that require.js is added at the top. If you refresh your browser, this time the page should launch correctly.
Once we learned how to combine multiple module files and how to get the output script running, let's have a look how to optimise it with the r.js "adapter". Let's create another build script - build.js and copy the entire content of the build-dev.js excluding the line optimize: false and change the out property to export the file to dealerapp.js.
When we run our script we should see an additional line about uglifying dealerapp.js.
The main script file became now a 'one liner', from 101 KB dealerapp-dev.js we managed to get 33 KB in our production code and still our application works perfectly.
The majority of the content of the combined file is now the require.js script. It contains functionality that we don't need, we just want to be able to load and run our AMD modules. Luckily there is another AMD loader that is a lot smaller and contains only necessary functionality - almond.js https://github.com/jrburke/almond, a library from James Burke - the same guy that stay behind the require.js library. Download it and place in the libs folder in place of the require.js file.
We need to slightly modify our build files, the build-dev.js and build.js, first remove references to the require.js and then instead of having 'main' in the name property we add a path to almond.js (without 'js') and the reference to 'main' will go to the 'include' property.
We follow the same approach for the build.js file:
So let's run the r.js optimisation tool in terminal:
If we run the application in a browser, it runs in exactly the same way as before with the require.js. Let's compare file sizes after optimising with the require.js loader and the almond.js loader:
dealerapp-dev.js 99 KB
dealerapp.js 32 KB
dealerapp-dev.js 32 KB
dealerapp.js 18 KB