Working With RequireJS in Oracle JET
The latest article in a series of tutorials on Oracle JET, this time focused on what RequireJS can bring to the table.
Join the DZone community and get the full member experience.
Join For FreeThis is another article in a series of articles on Oracle JET:
- Installing Oracle JET for JavaScript Web Development
- Installing Oracle JET for Hybrid-Mobile Application Development
- Understanding the Development Process With Oracle JET for Web and Mobile
In continuing my article series on sharing what I'm learning about Oracle JET, in this article I wanted to share what I've learned about RequireJS.
RequireJS is one of a number of popular open source tools that Oracle has chosen to make use of with Oracle JET rather than building yet another JavaScript framework from the ground up.
So I was curious to learn what RequireJS is, what problems in the JavaScript world was it designed to solve, and how is Oracle JET specifically making use of it?
Back to Programming Fundamentals — Modularization
The concept of "decomposition", that is the task of taking a large complex programming problem and breaking it into small solvable solutions is a common concept among programmers. Among my older experienced IT friends, we often jokingly lament programming is “the mistake of breaking one large problem into an IT career which you can never escape”.
This act of decomposition nicely segways to the concept of modularization and modular programming. As Wikipedia states:
"With modular programming, concerns are separate such that modules perform logically discrete functions, interacting through well-defined interfaces."
…and Wikipedia goes on to further establish the benefits:
".. modules may then be reused .. also facilitates the "breaking down" of projects into several smaller projects .. more easily assembled by large teams, since no team members are creating the whole system .. they can focus just on the assigned smaller task."
Overall, modularization is one of the most powerful tools in programming. But as Uncle Ben says, "with great power comes great responsibility". Arguably the decomposition of software solutions introduces some disadvantages, where reusable modules introduce dependencies, and as programmers we are then responsible for managing these.
If we consider JavaScript it supports modularization through artefacts including functions, classes, and the separation of these artefacts into individual JavaScript files. Because functions can call other functions across these artefacts, this introduces the same concerns around dependency management.
Of course dependency management is a common issue across programming languages but JavaScript introduces some unique concerns which I think are demonstrated well by the following code snippet. See if you can discover what might be at issue in this HTML and JavaScript excerpt? First here's the HTML:
// +index.html
<html>
<body>
..page content..
<script src="js/customers.js"></script>
<script src="js/main.js"></script>
<script src="js/orders.js"></script>
</body>
</html>
...and then the JavaScript:
// +js/main.js
createCustomer();
// +js/customers.js
function createCustomer() {
raiseOrder();
}
// +js/orders.js
function raiseOrder() {
// do something
}
As you determined, the loading of the scripts in the main HTML page doesn't reflect the order that the JavaScript functions call each other which will result in a runtime JavaScript error in the console:
Within the JavaScript world there is no implicit dependency management mechanism so it's left to us the developers to manage this. As you'll also note it's left to the HTML page to define the order of the JavaScript modules to load too, which is a bit odd for separate JavaScript files that may have been written independently of the HTML code, why does the HTML hold the responsibility of managing the dependencies of the JavaScript?
Overall this challenge isn’t surmountable for small projects where web developers build the HTML and JavaScript in conjunction. But I can imagine for larger projects where there are separate HTML, CSS, and JavaScript developers who don't know the full codebase, where there are thousands of lines of code or even large 3rd party libraries, it's easy to trip up in managing the dependencies. I guess the worst case outcome is because of this, JavaScript developers may start to de-decompose (is there such a word? Maybe "recompose" would be better?) their code base back into one large file to avoid these difficulties, and by accident sidestepping the major benefits of modularization in the first place.
RequireJS
This is where RequireJS steps in. As the logo for RequireJS says, it's "A JavaScript module loader". After researching what it does, as I like to quip, it "Does what it says on the tin".
The primary purpose of RequireJS is to give JavaScript developers the ability to define and manage JavaScript dependencies, and by inference promoting modular JavaScript code.
In opening the tin so to speak, and looking into RequireJS's meaty features, there's support for asynchronous module definition (AMD) which provides asynchronous and deferred loading of JavaScript scripts in a non blocking fashion to optimize page loads if a script isn't available, say via a CDN, to fall back to another location; the ability to handle scripts with multiple versions, and optimizations such as the minification of our JavaScript during build time. The API docs are quite extensive and will reward readers with time to spend reading it all.
All of these features are available to Oracle JET applications, though arguably we need to become familiar with the basic use of RequireJS in a small HTML application before moving to more advanced use cases in JET.
Setting Up RequireJS
Having downloaded RequireJS and made it available to a simple HTML application, within our HTML page we can load RequireJS as follows:
<html>
<head>
<script data-main="js/main" src="js/require.js"></script>
</head>
<body> .. </body>
</html>
As the goal of RequireJS is to manage all of our JavaScript files, we should arguably remove any other scripts from this page.
Alternatively, if we don't want RequireJS to block the page rendering as it loads, we can shift it to the end of the HTML body:
<html>
<body>
<h1>Page content</h1>
<script data-main="js/main" src="js/require.js"></script>
</body>
</html>
RequireJS searches for the data-main attribute where we define the first script to load, which will act as our application JavaScript bootstrap. In the example above this is js/main.js. RequireJS assumes all modules end in ".js" so adding the extension is not required.
Alternatively, we can load RequireJS as follows, though it's important to get the order correct otherwise the main.js script will fail, as you guessed it, it has dependency on RequireJS:
<script src="js/require.js"></script>
<script src="js/main.js"></script>
RequireJS Modules
In defining the js/main.js file, we place our JavaScript logic in this file, and RequireJS manages this as a module available to our application. The following code shows a primitive example of defining a module using RequireJS and helps us understand the basic concepts:
require(["js/customers"], function(cust) {
cust.createCustomers();
});
RequireJS modules always start with either a call to the require()
or define()
functions. The basic and similar signature of these functions is they first take an array of scripts that the current module is dependent on. In the above example it explicitly lists the js/customers.js script as a dependency for this module (again, remember that we don't need to include the .js extension).
For the second parameter, an anonymous function is called once the dependencies are loaded. As each dependency is loaded, it is passed as an object to the anonymous function to use, where we can define an appropriate handler to reference the object, in the order they were defined in the array of scripts. In the above example we’ve chosen to use "cust" to represent "js/customers".
From there we can include our application logic. If we want to call code within the dependencies for the module, we simply use the handle and then call the relevant function in that dependency, such as cust.createCustomers(), which equates to a call to createCustomers() in the js/customers script.
In understanding how modules are loaded, it's worth exploring a slightly more complicated example as follows:
require(["a","b","c"], function(a,b) {
a.doSomething();
b.doSomethingElse();
});
In the above example it defines a dependency on 3 modules a+b+c comma delimited, which will be loaded asynchronously. Of interest we don't have to define handles for the dependencies in the anonymous function's parameters if the dependencies aren't used. From this example, a+b are needed, but not c.
Of course the obvious question is why would we list c as a dependency if it's not actually used? This may be because the page that is consuming this overall module has code that is dependent on other JavaScript modules, such as UI components based on JavaScript, that aren't accessed in this specific example.
require() vs. define()
In working further with RequireJS modules, we will encounter the use of the require()
and separately the define()
function as shown in the following example:
// +js/main.js
require(["js/customers"], function(cust) {
cust.createCustomers();
});
// +js/customers.js
define(["js/orders"], function(ord)
return function createCustomer() {
ord.raiseOrder();
}
}
// +js/orders.js
define(function() {
return function raiseOrder() {
// doSomething
}
}
The main difference between these functions is that the require()
function is used to load functionality that must execute immediately. Typically this would be any bootstrap code we need to get our page and JavaScript logic up and running, and typically you only use require()
once in your application.
The define()
function differs from the require()
function in that it is used for defining a reusable module, one module per file, and each will typically return an object so it can be referenced and reused elsewhere. In the above example js/customer.js and js/orders.js represent our reusable modules wrapped in a define()
statement.
Similar to the require()
function, thedefine()
function lists an array of dependencies and then passes each to the anonymous function to be mapped to an object handle name of our choosing. In investigating the above example we can see that the require()
module has a dependency on the js/customers.js module accessed through the "cust" handle. In turn js/customers.js has a dependency on the js/orders.js module access through the "ord" handle. And finally, js/orders.js itself has no dependencies.
The RequireJS API documentation goes into a fair amount of detail on lots of different ways we may define a RequireJS module and is worth the read.
Managing File Dependency Order
Consider the following define() example:
define(["js/somepath/customers","js/anotherpath/again/orders",], function(cust, ord) {
return function doStuff() {
cust.doSomething();
ord.doSomethingElse();
}
});
Imagine your application is full of similar RequireJS modules, making use of other modules all with a mess of paths such as "js/somepath" and "js/anotherpath/again". The problem with explicitly listing the paths is if we choose to move the JavaScript files between directories, we may need to fix this in a considerable amount of our JavaScript define()
statements. Is there an easier way?
The solution is to call the require.config function to manage the modules instead.
In the initial bootstrap JavaScript file js/main.js we can add the following logic:
// +js/main.js
require.config({
baseUrl: "js",
paths: {
"cust": "anotherpath/again/customers",
"orders": "somepath/orders" }
});
// Bootstrap
require(["cust"], function(cust) {
// custom code
});
The require object, which can also be accessed via a 'requirejs' alias if there is a namespace collision, provides a number of parameters including the baseUrl which defines relative to the project base directory where the JavaScript files can be found, and the paths array which allows us to identify by name, all the modules we wish to load including their paths.
Taking the example above, you can see the entry "cust": "anotherpath/again/customers", which resolves to "js/another/again/customers.js" thanks to the baseUrl identifying the base directory of the JavaScript files, and RequireJS assumes all files end with .js.
From there the main require()
module (and the other loaded define()
modules) can list their dependencies as defined in the require.config()
paths array rather than explicitly defining the module locations. In the example above, under the comment "Bootstrap," we can see a require()
listing the dependency on "cust" which we defined in the paths of the require.config.
Back to Oracle JET
There's certainly plenty of good concepts to learn about RequireJS, but with these fundamentals out of the way it's a good time to return to Oracle JET and see how it utilizes RequireJS.
From our previous articles we learned how to generate an application shell based on one or more templates such as navbar:
yo oraclejet MyWebProject --template=navbar
Once the application's scaffold is created, we can locate the application's index.html page under the src directory, and within this file we discover that RequireJS is loaded asynchronously within the <body> using js/main.js as the bootstrap:
In opening the js/main.js file we discover that RequireJS is extensively used to manage 3rd party libraries already:
…among the different options, we discover requirejs.config is called to define custom JS modules, which will be found in the "js" subdirectory via the baseUrl parameter, and a number of 3rd party modules such as Knockout are loaded via the paths parameter. A quick look at the file system shows that it reflects the config settings:
If we continue to look down the js/main.js file, we can see quite an extended dependency list for the require function including modules like ojs/ojcore, knockout and more:
Among the different modules listed the JET specific modules include the following mandatory modules:
ojs/ojcore - This defines the base JET object and also reserves the "oj" namespace for JET.
ojs/ojknockout - For JET ojComponents defines additional bindings & services.
Beyond this there are a wide array of optional JET modules including:
ojs/ojmodel - A REST enabled API mapping to models (objects) and collections.
ojs/ojrouter - Router logic for single page applications (SPAs).
ojs/ojmodule - A single page application (SPA) region manager.
…and many more, including JET UI components like buttons and toolbars.
On this last point we can see the require()
function defining a dependency on ojs/ojnavigationlist as an example, which is a JET UI component showing horizontal or vertical navigation controls which the navbar template makes use of. To see a sneak peek refer to the Oracle JET Cookbook.
Overall I hope to cover these other JET modules in later articles when I can study them in more detail.
From there the anonymous function includes various calls to initialize the modules loaded into the application:
Among the dependencies of the require()
function we can also see "appController", which is loaded from application's src/js/appController.js as another module. It's well worth having a further look at this as its common in the Oracle JET applications generated from the Yeoman templates. Here's a sneak peek at the module (I've removed some code to keep this brief):
As we can see, the appController module returns a new ControllerViewModel object which is intended as a global object to handle global settings for our JET application. This module is not specific to RequireJS applications, but rather is a useful pattern you will see in the Oracle JET templated applications. It allows the initialization logic for our application to be separated from the initialization logic for the various frameworks we've loaded and are handling in the js/main.js file. As example in the cut-down version of this code above, we can see the single page application router-logic configured with a number of pages in our app to provide to the user for navigation.
In returning to the main.js file we see the object ControllerViewModel passed to Knockout and applied to the index.html page as a binding:
As mentioned I hope to discuss Knockout and more in future articles, but having a small idea of how the template generated JET applications together helps to understand how it all works.
Conclusion
In concluding my investigation into RequireJS, I'm pleasantly surprised by how easy it is to use. Certainly there's a lot of functionality available for optimizing how it works, but, in just understanding the basic use case for managing the loading and dependencies of JavaScript files, it presents a simple solution to a complex problem. I can see why it has been bundled with Oracle JET.
Opinions expressed by DZone contributors are their own.
Comments