If you have been following JavaScript development, you might have seen a lot of posts on modules. It's a hot topic in JavaScript and it's easy to jump on a bandwagon. The truth is that the problem that needs to be solved is very fundamental to the success of your code.
JavaScript has an inherent lack of a module system...
If that's the first place your mind went, then you are in good company. Derick Bailey explains the concept of a module like this:
A module is a wrapper or enclosure around a group of related things in code - whether those things are objects, functions, variables, or whatever they might be. Modules create encapsulation and give us the ability package many smaller things in to something larger, presenting it in a unified manner.Think about it this way: In .NET or Java, you use classes or what have you to build your project. Then you have a compiler that comes along and bundles everything up for you into an assembly based on which classes you have declared you will be needing. Now you have a module that you can use in other projects, and it can easily be referenced or imported for use. This concept of grouping your code together and packaging it is completely missing from JavaScript.
Lets take a look at how this problem typically plays out. Assume that we have a very simple application that has color palette widget. Moving the selector in the color palette changes the page color.
For a more interesting example of the new Color Picker widgets that I didn't end up using, go here.
Let's look at the JavaScript that is behind the scenes here.
// open a document ready function $(function () { // function that just changes the background color var setBackground = function (color) { $(document.body).css("background-color", color); } // function to handle the pallete color selection var setColor = function (e) { // the color object contains all the hex, rgba, and hsl // conversions and utilities var color = e.sender.color().toBytes(); // set the color setBackground(e.value); }; // select and create the color pallete var colors = $("#colors").kendoFlatColorPicker({ change: setColor, value: "#fff" }).getKendoFlatColorPicker(); });
When I look at the above code, It appears to me that it could be broken out into two different objects.
In JavaScript, you have several choices for creating "modules", but the most fundamental way is to use something called an Immediately Invoked Function Expression (IIFE), that will return you an object. Lets take just the utility function that changes the background color.
// variables declared outside of a function will go on the global // scope, so create an "APP" namespace var APP = window.APP || {}; /************************************************* UTILS MODULE **************************************************/ // use a function that executes right away and assign // whatever it returns to the utils variable APP.utils = (function() { // return an object return { // declare the function to change the background color setBackground: function(color) { $(document.body).css("background-color", color); } }; }());
I realize that you would most likely never have a function to perform a single operation on a single object, but it helps to simplify the overarching idea here, so just bear with me.
Now we have encapsulated that function, and maybe any other utility functions that we have in an object called utils, and we can call it like so:
// set the background color to blue APP.utils.setBackground("#336699");
Next we can encapsulate the color palette widget as a module. The palette module needs to access the utils object which is available off of the same APP "namespace".
// variables declared outside of a function will go on the global // scope, so create an "APP" namespace var APP = window.APP || {}; /************************************************* UTILS MODULE **************************************************/ // use a function that executes right away and assign // whatever it returns to the utils variable APP.utils = (function() { // return an object return { // declare the function to change the background color setBackground: function(color) { $(document.body).css("background-color", color); } } }()); /*************************************************** Color Palette Module ****************************************************/ // create the color palette module off the app namespace // all that we need to return out of this function is an instance // of the color palette widget APP.palette = (function() { // this function is private and not available to utils // function to handle the pallete color selection var setColor = function (e) { // the color object contains all the hex, rgba, and hsl // conversions and utilities var color = e.sender.color().toBytes(); // set the color APP.utils.setBackground(e.value); }; // select and create the color pallete var colors = $("#colors").kendoFlatColorPicker({ change: setColor, value: "#fff" }).getKendoFlatColorPicker(); // just return the entire widget instance return colors; }());
Now the code is fully modular. It's quite a bit more verbose, but we can extract each one of these modules into it's own file. That gives us two files:
!<-- include jquery, kendo ui and the app scripts --> <script src="/Scripts/jquery-1.9.1.min.js"></script> <script src="/Scripts/kendo/2013.1.319/kendo.web.min.js"></script> <script src="/Scripts/app/utils.js"></script> <script src="/Scripts/app/palette.js"></script>
We don't need ANY inline script since the functions we wrapped our modules in execute as soon as they are included in the page and return the necessary references that other modules depend on. There are still some fairly major drawbacks here.
The utils.js file ALWAYS has to be referenced first because it creates the APP variable that all of the modules live on.
There are 2 files already, and this is a dead simple page. Imagine how many files you might have in a full on enterprise application.
You could use a build tool like ASP.NET MVC Bundling, but as your application grows you are still going to have modules that depend on other modules. You can try to keep that all straight in your head, but as your app grows, you are going to find that it starts to feel like you got Gizmo wet and took him to a pizza buffet after midnight.
The core idea behind RequireJS is to allow you to specify in a JavaScript file which files it depends on so that you can be sure that when your code executes, the dependencies are satisfied.
RequireJS is just a JavaScript file. It's a prime example of solving deficiencies in JavaScript by writing JavaScript.
You can download it from the RequireJS site, or you can use your package manager of choice. I'm working in ASP.NET today, so I'll grab it off of NuGet.
PM> Install-Package RequireJS
It's going to drop 2 files in the Scripts directory. We're only concerned with require.js at the moment. The other file, r.js, is a utility which concatenates and minifies your files. We'll talk about it shortly.
Usually you will see RequireJS projects structured so that your application lives in a folder called "app". Your JavaScript files will go into a project called "mylibs", and your third party scripts will go into a folder called "libs". That's not gospel, it's just a suggestion. Structure your code the way that you feel the most comfortable with.
Before I actually reference the RequireJS script, I'm going to need to add a "main" file that acts as an entry point. I'll just call it "main.js" and drop it in the "app" folder at the same level as the "mylibs" directory. That main.js file will call the "require" function, which is a function that is specified by the require.js file. In it, I'm going to declare that I want to load in my two files. It then takes those file paths and injects them into a function as variables and executes that function. RequireJS assumes that you are working with JavaScript files so you don't include .js in your names.
require([ "mylibs/utils", "mylibs/palette" ], function(utils, palette) { // the app is loaded... });
Now I can include RequireJS in my HTML page and specify that I want it to use the main.js file as it's main entry point by specifying the data-main
attribute on the script tag.
<script src="/Scripts/require.js" data-main="/Scripts/app/main.js"></script>
The application now runs and RequireJS loads in the two files that I specified and the app works. If you open your browser tools, you can see the two JavaScript files being loaded in.
Here is where things start to get very interesting. The palette.js file depends on the utils.js file. Without it, the palette.js code is broken. I'm going to modify these files just slightly to let RequreJS know that. I do that with a define
function at the top of those files. The define function is a function created by RequireJS, and it takes two parameters:
A RequireJS Palette module then looks like this:
define([ "mylibs/utils" ], function (utils) { // this function is private and not available to sliders or utils // function to handle the pallete color selection var setColor = function (e) { // the color object contains all the hex, rgba, and hsl // conversions and utilities var color = e.sender.color().toBytes(); // set the color utils.setBackground(e.value); }; // select and create the color palette var colors = $("#colors").kendoFlatColorPicker({ change: setColor, value: "#fff" }).getKendoFlatColorPicker(); // just return the entire widget instance return colors; });
And the utils module doesn't depend on anything...
define([], function () { return { // declare the function to change the background color setBackground: function (color) { $(document.body).css("background-color", color); } }; });
Notice that those IIFE's went away? So did the APP variable/namespace. RequireJS handles your modules and holds them for you. You can get them at anytime by executing require("Your Module Name"). I can also remove utils.js from the main.js file completely and just load palette.js. RequireJS will load in palette.js, see that it depends on utils.js and load that file in first.
require([ "mylibs/palette" ], function() { // the app is running... });
I still have Kendo UI and jQuery hanging about in my page and not included in my RequireJS configuration. There isn't anything really "wrong" with this, but you can include these third party libraries in RequireJS so you can bundle and minify everything down into one file.
You can configure these third party libraries in the main.js file by creating a "path", which is just a named variable that is pointing to the library location. If you are using a CDN for these libraries (which you should if you can), then you can specify a fallback to a local file if the CDN isn't available.
require.config({ paths: { // specify a path to jquery, the second declaration is the local fallback jquery: ["//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery", "../Scripts/jquery.1.9.1.min"] } }); require([ "mylibs/palette" ], function(jquery, palette) { // the app is running... });
You then pass jQuery into your main entry point. Many people will choose to add in an "app.js" file here so that they are not loading a bunch of their own files along with the third party ones in the main.js file. The "app.js" file then loads in all of your libraries.
require.config({ paths: { // specify a path to jquery, the second declaration is the local fallback jquery: [ "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min", "../Scripts/jquery.1.9.1.min"] } }); require([ 'app' ], function(jquery, app) { // this loads jquery and your app file });
You would of course want to specify jQuery as a dependency in the palette.js file. You ALWAYS want to specify all a file's dependencies. This is the only way that RequireJS will know which files to load and in what order. Just because I specified jQuery before palette.js, it does not mean RequireJS will load them in that order.
Some third party libraries will have dependencies on other third parties. Kendo UI depends on jQuery. While the different Kendo UI modules will specify their internal dependencies for RequireJS, they do not specify a dependency on jQuery since it's easy to do and requires just one line. Here I am adding in a dependency for Kendo UI with a local fallback, and using "shim" to let RequireJS know that I need jQuery loaded before Kendo UI.
require.config({ paths: { // specify a path to jquery, the second declaration is the local fallback jquery: [ "//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min", "../Scripts/jquery.1.9.1.min"], kendo: [ "//cdn.kendostatic.com/2013.1.319/js/kendo.web.min", "../kendo/2013.1.319/kendo.web.min"] }, // inform requirejs that kendo ui depends on jquery shim: { "kendo": { deps: ["jquery"] } } }); require([ 'app' ], function(jquery, kendo, app) { // this loads jquery and your app file });
Now you can remove the script tags for jQuery and Kendo UI from your page and let your modules specify if they need them. For instance, the palette file now looks like this:
define([ "jquery", "kendo", "mylibs/utils" ], function ($, kendo, utils) { // this function is private and not available to sliders or utils // function to handle the pallete color selection var setColor = function (e) { // the color object contains all the hex, rgba, and hsl // conversions and utilities var color = e.sender.color().toBytes(); // set the color utils.setBackground(e.value); }; // select and create the color pallete var colors = $("#colors").kendoFlatColorPicker({ change: setColor, value: "#fff" }).getKendoFlatColorPicker(); // just return the entire widget instance return colors; });
The application is fully configured an ready for deployment. It could be deployed as-is, but that's not very responsible. You should always minify and concatenate your scripts before you go to production. This is where the r.js file comes in.
Before we can use the r.js file, you need to have NodeJS installed. If you don't have Node, and it makes you nervous, just think of it as a command line utility with a TON of useful plugins that help you automate your workflow regardless of operating system and platform. In this case, I need NodeJS to run the r.js file which will perform a build on the JavaScript.
After you install NodeJS, you are ready to run the r.js build file, also known as the optimizer.
The RequireJS optimizer is good, but it's not a mind reader. You need to give it a file that it can read which will tell it about your project and which files you want it to minify. This file can be named whatever you want and can exist anywhere, but I usually call mine "build.js" and keep it in the top level directory. It doesn't need to be in a publicly accessible location.
({ baseUrl: "./Scripts/app", paths: { jquery: "empty:", kendo: "empty:" }, name: "main", out: "Scripts/main-built.js" })
You will notice that I provided paths for Kendo UI and jQuery in the form of "empty:", which tells RequireJS that these are CDN scripts and need only to be referenced, not included in the build.
Then you simply call the r.js file from the command line with Node, and pass in the location of the build.js file.
// run from the project directory node r.js -o build.js
That will spit out a "main-built.js" file which contains all of the project JavaScript concatenated and minified. You can now just modify the data-main file that RequireJS is loading to be the main-built.js file, instead of app.main.js.
Since I am working in ASP.NET, there are a few other changes I want to make to streamline my workflow.
I do not want to have to manually change my script tag whenever I go to release and then back for development. I'm going to sniff out my current build mode in the HomeController and then pass a variable to the Index.cshtml page so I know which script to include.
public ActionResult Index() { #if DEBUG ViewBag.useBuild = false; #else ViewBag.useBuild = true; #endif return View(); }
@if (ViewBag.useBuild) { <script src="@Url.Content("~/Scripts/require.js")" data-main="@Url.Content("~/Scripts/main-built.js")"></script> } else { <script src="@Url.Content("~/Scripts/require.js")" data-main="@Url.Content("~/Scripts/app/main.js")"></script> }
I also don't want to have to jump down to the command line every time I change my status to Release so that I can run the optimizer. You can specify a Pre Build event by going to Project / Project Settings / Build Events. I only want the optimizer build to occur when the project is in Release mode.
if $(ConfigurationName) == Release node $(ProjectDir)/r.js -o $(ProjectDir)/Scripts/build.js
Modularizing your JavaScript code really sets you up for success. It's really just the foundation on which you can build highly maintainable applications of great size. You can download Kendo UI and start building extremely modular applications today using RequireJS.
You can download the sample project from today from our ASP.NET MVC Examples repo. I would also highly recommend that you checkout these additional resources.
Burke Holland is a web developer living in Nashville, TN and was the Director of Developer Relations at Progress. He enjoys working with and meeting developers who are building mobile apps with jQuery / HTML5 and loves to hack on social API's. Burke worked for Progress as a Developer Advocate focusing on Kendo UI.