Module loading and dependency management

Một phần của tài liệu spa design and architecture understanding single page web applications scott 2015 11 26 Lập trình Java (Trang 99 - 106)

In most browsers, the SCRIPT tag used in your module’s source file creates a blocking condition. The application pauses while scripts are loading. So the more module files you have, the more lag your users will experience while waiting for the application to load. A deluge of HTTP requests can also tax your network.

To help alleviate this issue, you can concatenate the modules into as few files as possible and optimize the final files. Chapter 9 covers tools for these two techniques.

But in spite of the gains from using these two techniques, you still have the SCRIPT tag’s synchronous nature to contend with. To tackle this problem, you can also look at libraries that load your modules asynchronously.

3.4.1 Script loaders

Being able to bypass the blocking condition of the SCRIPT tag gives a tremendous boost to your application’s load time. HTML introduced native nonblocking support for loading and executing JavaScript code through its defer and async attributes for the SCRIPT tag. The defer attribute specifies that a script is executed only after the page has finished parsing. The async attribute, on the other hand, asynchronously executes the script as soon as it’s available. When using the SCRIPT tag, it’s up to you to make sure scripts are correctly ordered so that dependencies are available when needed. An alternative approach is to use AMD script-loading libraries.

AMD script loaders handle the low-level, boilerplate code to manage the asynchro- nous download process. They also allow you to specify dependencies that must be present for a module to function. If the dependent modules aren’t there, the

Not required

var moduleName = ( function() { var someVar;

function someFunction(){

} return {

someFunction : someFunction };

} ) ();

Figure 3.15 The outer parentheses aren’t required.

75 Module loading and dependency management

framework will go get them for you and make sure they’re downloaded and ready before proceeding. You have a multitude of script loaders to choose from—LABjs, HeadJS, curl.js, and RequireJS, to name a few. Each one is slightly different but serves to tackle loading and management issues. It’s also worth mentioning that, though not ratified as of this writing, the ability to asynchronously load scripts is a proposal for CommonJS-style script loaders.

Nothing is ever perfect, though. Asynchronously loading scripts speeds things up but introduces another problem: unpredictable resource availability. When scripts are loaded asynchronously, there’s no way to precisely know which one will be first. It’s entirely possible for a file to download and start executing before all necessary depen- dencies are in place. Creating the fastest load time possible is for naught if your scripts fail because their dependencies aren’t yet loaded. Good news, though. Most script- loading libraries take care of this issue as well.

Script loaders defer script execution until the file and any required dependencies needed by the module are loaded. Most cache the module as well, so it’s loaded only once, no matter how many times it’s requested.

To illustrate some basic loading and management concepts, I have to choose a library to use. We’ll use RequireJS because it’s currently a heavily used script-loading library. RequireJS will also give you a chance to become acquainted with a popular module format that differs slightly from the traditional module pattern you’ve already seen. It’s called the Asynchronous Module Definition (AMD)API. The next sections define AMD and walk you through script-loading basics with RequireJS.

3.4.2 Asynchronous Module Definition

AMD started as the draft of the module format for a larger project called CommonJS. CommonJS was an attempt to not only solve the issue of a missing standard module definition in the JavaScript language but also make this single format work for a vari- ety of environments, including the server. But among the CommonJS group, full con- sensus wasn’t reached on the module specification (as of this writing), and AMD was moved to its own group, the AMD JS group (https://github.com/amdjs).

AMD has largely been adopted for use within web browsers. This specification defines not only a standard module format but also how to asynchronously load it and its dependencies. The specification defines two structures: define and require. I’ll talk about both before diving into an example using RequireJS.

DEFINE

Use define to declare a module, as you did using the module pattern previously. Like the module pattern, the AMD module allows for imports, which can be accessed via matching function parameters. The body of the AMD module is also nested inside an outer containing function, just like the module pattern. Another similarity is that the AMD module’s body contains private functionality that can be exposed via its return statement.

At this point, you’re probably asking yourself why AMD exists, if you already had a perfectly good way to create modules using the module pattern. The main reason is

that AMD represents a formal specification for defining modules, absent having one in the language. But more specifically, it’s a specification for defining modules and their dependencies that are to be loaded asynchronously.

TIP Even though the specification allows for a module ID, it’s normally omit- ted. If the ID is left out, it’s generated internally by the script loader. In turn, the module is managed internally via the generated ID. Unnamed AMD mod- ules are more portable, allowing you to freely move the module to new direc- tories without code updates.

Notice that a namespace isn’t defined as with the module pattern. Another one of the perks of using AMD and script loaders is that namespaces are no longer needed.

Modules are managed by the library. You also don’t have to worry about the order of the dependencies. They’ll be loaded and available by the time the module’s function executes.

REQUIRE

The require syntax is used to asynchronously load/fetch a particular module. In this structure, you define the modules to be loaded in the same way that dependencies are declared using the define syntax. A callback function is executed when the required modules are fetched and ready.

The require structure is a directive, not a module declaration. So whereas module definitions are usually one per physical file, require should be used anytime you need it. It can be used by itself or from within a module.

AMD—define

define('someID',

['dependency1', 'dependency2'], function ( depArg1, depArg2 ) { // private variables, functions

return { // public functions };

});

AMD—require

require(

['module1', 'module2'],

function ( modArg1, modArg2 ) { // do something

});

Optional name/ID

for the module Optional array of strings representing the module’s dependencies

Outer containing function’s parameters are references to the dependencies Code used internally by the module Object literal with public functions is returned, serves as the module’s public API

Array of strings representing the modules to load/fetch

Callback function with parameters that are references to the modules

77 Module loading and dependency management

3.4.3 Using AMD modules with RequireJS

Quite often concepts are easier to understand after seeing them in action. So let’s take one of the earlier examples and convert the modules into AMD-style modules. You’ll use RequireJS as the module-loading library to implement AMD.

Before you begin the example, let’s go over some RequireJS concepts. Even though you’ll focus on module loader concepts, you still have to get some of the RequireJS basics under your belt to be able to complete the example:

■ data-main—Everything has to start somewhere. Just as a basic Java or C# appli- cation has a “main” method, RequireJS similarly has a point of origin. When you add a SCRIPT tag to your SPA to include the RequireJS library, you’ll add a data- main attribute to it. This is the starting point of a RequireJS-loaded application.

Normally your main JavaScript file will contain the RequireJS configuration and a require directive to execute the initial AMD module.

■ requirejs.config()—This function establishes the RequireJS configuration options. Its only input is an object literal that will contain all configuration properties and their values.

■ baseUrl—This is a path relative to your web application’s root directory. Any other paths in the configuration object will be relative, in turn, to this path.

■ path—You have to tell RequireJS where to find your modules. A path maps a module name (which you invent) to a path on the web server, relative to the baseURL (and the application’s root directory). It can be the full path to the module’s source file (minus the extension) or a partial path. If a partial direc- tory path is used, the rest of the path to the file must be included anywhere the object is listed as a dependency.

You have many more configuration options, but we’ll stick to the basics to keep the emphasis on learning AMD, not RequireJS specifically. If you want to learn more about RequireJS-specific options, the documentation can be found at http://requirejs.org.

Now that you’ve been introduced to a few basic concepts, let’s try converting an example’s modules to AMD modules. You’ll use the example from section 3.2.2, because it’s a small example and will be easy to compare against the original version.

First, you’ll need to download the RequireJS library from http://requirejs.org. Fig- ure 3.16 has the correct download option circled.

Figure 3.16 RequireJS has no dependencies to download. You need only the require.js file.

With the necessary files in place, the example’s directory now looks like the following listing.

|-- app

| |-- example

| | |-- modules

| | | |-- counter.js

| | | |-- displayUtil.js

|-- thirdParty

| |-- require.min.js

|-- index.html

|-- main.js

When you open your default URL in the web browser, you’ll immediately get the index.html page, because the server has been configured to use it as your welcome page. All you need to do here is add a single JavaScript include to point to the RequireJS library. Inside the SCRIPT tag, you’ll use the data-main attribute to let RequireJS know that your main file is called main.js and can be found at the web appli- cation’s root (see the following listing).

<!DOCTYPE html>

<html>

<head>

<link rel="stylesheet" href="css/default.css">

</head>

<body>

<!-- starting point defined in data-main -->

<script data-main="main.js"

src="thirdParty/require.min.js">

</script>

</body>

</html>

Next, you’ll need to add some basic configuration to main.js. This example is simple, so you need to configure only your baseUrl and path properties so RequireJS can find the modules. You’ll additionally use the require directive to call displayNewCount() from your displayUtil module twice. Calling the function more than once will let you know that the counter is incremented appropriately (see the following listing).

requirejs.config({

baseUrl : "app/example",

paths : { counter : "modules/counter", util : "modules/displayUtil"

} });

Listing 3.13 AMD example directory structure

Listing 3.14 index.html

Listing 3.15 main.js

Two example modules RequireJS

library

Main file contains the RequireJS configuration and our application’s kickoff point

Tells RequireJS where application’s main file is located

RequireJS library

baseUrl relative to the web application’s root

Path relative to the baseUrl (entry to the left of the colon is the module’s name)

79 Module loading and dependency management

require(

[ "util" ], function(utilModule) {

// increment first time utilModule.displayNewCount();

// increment second time utilModule.displayNewCount();

} );

Looking at the code for the require call from listing 3.15, notice that you told RequireJS you wanted to use the displayUtil module in the callback function by including its module name from the paths section. Each module-name string in the dependency list should have a matching function parameter. Inside the callback func- tion, you use this parameter to reference the module.

The following listing shows the module for the display utility. Because this is a module declaration, you’ll use the define syntax.

define(["counter"], function(counterModule) { function printCount(){

var count = counterModule.getCount();

if(count === 1) {

count = count + " time";

} else {

count = count + " times";

}

console.log("Count incremented: " + count);

}

function displayNewCount(){

counterModule.incrementCount();

this.printCount();

} return {

printCount : printCount,

displayNewCount : displayNewCount };

});

In the displayUtil module, your only dependency is the counter module. The body of the module is nearly a one-to-one match with the original, non-AMD version. Keep in mind that you don’t need to assign the module to a namespace, because modules are managed internally by RequireJS.

Finally, the next listing shows the counter module. Because it has no dependen- cies, you can leave out that part of the structure. With no dependencies, your outer function will have no parameters.

Listing 3.16 displayUtil.js Use the module’s

name to declare a resource as a dependency

Function parameter receives the object export of the dependency it matches

Inside our callback function, call displayUtil twice to increment counter

Counter module is a dependency

Code was modified to use the parameter reference to the counter module

define(function() { var count = 0;

function incrementCount(){

++count;

}

function getCount(){

return count;

} return {

incrementCount : incrementCount, getCount : getCount

};

});

With all of the files in place, you can start your server and navigate to the default URL. The application will start after the require directive is reached. Figure 3.17 shows the network console from the browser. Now you can see the module loader in action.

From the output in the network console, you can see that all required modules were downloaded automatically by the module loader. All you had to do was to add a depen- dency to your require or define declaration. Because each dependency was passed into the module via its corresponding function parameter, you had access to the depen- dent module’s public API. Additionally, because RequireJS manages the modules for you, you didn’t need any namespaces. This is a testimony to the power of using AMD modules and the module loaders that implement this module specification.

Listing 3.17 counter.js

Dependency list can be left out; module’s outer function has no parameters

Figure 3.17 The module loader correctly downloads and manages our AMD modules and dependencies. The require directive in main.js declares a dependency that instructs RequireJS to fetch the displayUtil module. The displayUtil module, in turn, has a dependency on the counter module that gets dynamically loaded by the module loader.

81 Summary

Một phần của tài liệu spa design and architecture understanding single page web applications scott 2015 11 26 Lập trình Java (Trang 99 - 106)

Tải bản đầy đủ (PDF)

(178 trang)