Categories


Archives


Recent Posts


Categories


Magento 2 and RequireJS

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

Now that we’ve covered the basics of including javascript and CSS files in Magento 2, we’re going to start exploring Magento’s adoption of modern front end tools and libraries.

Magento’s in a bit of a tricky position when it comes to adopting modern front end technologies. Magento is a software platform, and an ecommerce one at that. Unlike an agency, whose marketing project will be discarded 6 months after launch or whose prototyping work will be depreciated a year after the product launches, an ecommerce software platform needs to focus on stable, proven technologies that are going to span the test of time.

Today we’re going to focus on the library that underlies nearly every javascript feature built in Magento 2 — RequireJS.

Before we get to Magento’s RequireJS implementation, we’re going to take a whirlwind tour of what RequireJS does.

RequireJS

RequireJS is a javascript module system. It implements the Asynchronous Module Definition (AMD) standard for javascript modules. In the terms of AMD, a javascript module provides a way to

  1. Run a javascript program that doesn’t default to the global namespace
  2. Share javascript code and data between named modules and programs

That’s all RequireJS does. You may use a RequireJS module that implements some special bit of functionality, but its not RequireJS that provides that functionality. RequireJS is the pneumatic tube that ensures the functionality is delivered to you.

The RequireJS start page has a good example of how RequireJS works. We’re going to crib from it and add some extra explanations.

First, download the RequireJS source and save it to a folder named scripts.

Then, create the following file.

<!-- File: require-example.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>My Sample Project</title>
        <!-- data-main attribute tells require.js to load
             scripts/main.js after require.js loads. -->
        <script data-main="scripts/main" src="scripts/require.js"></script>
    </head>
    <body>
        <h1>My Sample Project</h1>
    </body>
</html>

As you can see, this page loads in the main RequireJS with the following

<!-- File: require-example.html -->    
<script data-main="scripts/main" src="scripts/require.js"></script>

In addition to the standard src attribute, there’s also the custom data-main attribute. This tells RequireJS that it should use the scripts/main module as the program’s main entry point. In our case that’s scripts/main, which corresponds to a file at scripts/main.js.

Create the following file.

//File: scripts/main.js
requirejs([], function() {
    alert("Hello World");
});

With the above created, load your HTML page in a browser. You should see the Hello World alert. Congratulations! You just created your first RequireJS program.

By itself, RequireJS hasn’t done much that jQuery’s document ready functions can’t accomplish

jQuery(function(){
    alert("Hello World");
});

Where RequireJS sets itself apart is in its module system. For example, if we wanted to use a hypothetical module named helper/world, we’d change our main.js file to match the following.

requirejs(['helper/world'], function(helper_world) {
    var message = helper_world.getMessage();
    alert(message);
});

That is, we specify the modules we want to load as an array, and pass that array as the first argument to the requirejs function call. Then, RequireJS passes the single object the helper/world module exports to our main function as the first helper_world parameter.

Of course, if you ran the above you’d get a javascript error. That’s because we need to define our helper/world module. To define a module, turn the module name into a file path, and add the following contents

//File: scripts/helper/world.js
define([], function(){
    var o = {};
    o.getMessage = function()
    {
        return 'Hello Module World';
    }
    return o;
});

A module definition is very similar to our main program definition. The main difference is the use of the define function instead of the requirejs function. The first parameter of define is a list of RequireJS modules you’d like to use in your module (in our case, this is an empty array — in the real world most modules will use other modules). The second parameter is the javascript function/closure that defines what your module will return.

RequireJS has no opinion on what a javascript module should return/export. It could return a plain string. It could return a simple javascript object with a single method defined (as we have above). It could also load in a javascript library like PrototypeJS and return a PrototypeJS object. The only thing RequireJS does is provide a system for sharing javascript code via modules — the rest is up to each individual project developer.

Before we get to Magento’s RequireJS implementation, there’s two additional RequireJS topics we’ll need to cover: Require JS file loading, and RequireJS module naming.

RequireJS File Loading

By default, RequireJS will convert a module name like helper/world into an HTTP(S) path like the following

http://example.com/scripts/helper/world.js
https://example.com/scripts/helper/world.js
//example.com/helper/scripts/world.js

That is, the module name is turned into a file path, with the last segment being a file name that ends in js. By default, RequireJS will use the folder where the require.js script is located as its base (/scripts in the above example).

However, RequireJS allows you to set a different base path for your scripts. Before the start of your RequireJS program, include the the following code.

require.config({
    baseUrl: '/my-javascript-code',
});

With the above in place, when RequireJS needs to load the helper/world module, it will load it from

http://example.com/my-javascript-code/helper/world.js
https://example.com/my-javascript-code/helper/world.js    
//example.com/my-javascript-code/helper/world.js

This feature allows you to store your javascript files wherever you want in a RequireJS based system.

RequireJS: Module Naming

So far in our examples, a RequireJS module name has been tied to the location of that module’s source on disk. In other words, the helper/world module is always going to be located at the path helper/world.js.

RequireJS allows you to change this via configuration. If, for example, you wanted your helper/world module to be named hello, you’d run the following configuration code somewhere before the start of your program

require.config({   
    paths: {        
        "hello": "helper/world"
    },
});

The paths configuration key is where we can rename/alias modules. The key of a paths object is the name you want (hello), and the value is the module’s actual name (helper/world).

With the above configuration in place, the following program

requirejs(['hello'], function(hello) {
    alert("Hello World");
});

Would load the hello module from the helper/world.js path.

There are many other configuration directives that control how and where RequireJS will load javascript modules. While outside the scope of this article, the entire “Load Javascript Files” section of the RequireJS API Documentation is worth reading.

If you take away one thing from this brief RequireJS introduction tutorial, it should be that the point of RequireJS is for day-to-day javascript development should not need to concern itself with how a module load over HTTP. As a javascript developer, you should be able to say “I want to use the methods in module X to do something”, and just be able to use module X. The only time you should need to worry about file loading is when you’re adding a new module to the system, adding some non-RequireJS/AMD compatible code to the system, or looking for a module’s source files so you can figure out what it does.

Magento 2 and RequireJS

This brings us to Magento’s RequireJS implementation. Magento pulls in the main RequireJS library for you, includes some additional configuration, and provides a mechanism that will let you add your own additional RequireJS configurations.

The first thing you’ll want to make note of is Magento 2’s use of the aforementioned baseUrl feature of RequireJS. If you view the source of a Magento page, you’ll see something like the following

<script type="text/javascript">
    require.config(
        {"baseUrl":"http://magento.example.com/static/adminhtml/Magento/backend/en_US"}
    );
</script>

With the above configuration, this means when Magento 2 encounters a RequireJS module named helper/world, it will load that module source from a URL that looks something like this

http://magento.example.com/static/adminhtml/Magento/backend/en_US/helper/world.js

If you’ve worked through the previous articles in this series, you may recognize that URL as the “loading a front end static asset from a Magento module” URL. This means you could place a RequireJS module definition file in your module at

app/code/Package/Module/view/base/web/my_module.js

and it would be automatically available as a RequireJS module named

Package_Module/my_module

loaded via the following URL

http://magento.example.com/static/adminhtml/Magento/backend/en_US/Package_Module/my_module.js

It also means you could immediately start writing requirejs programs in your phtml templates by using code like the following

<script type="text/javascript">
    requirejs('Package_Module/my_module', function(my_module){
        //...program here...
    });
</script>

Or by adding stand-alone javascript files that do the same.

Configuring RequireJS via Modules

Earlier in our tutorial, we covered two RequireJS configuration directives — baseUrl and path. There are plenty of other RequireJS configuration directives, and as you get into advanced use of the framework (or you’re dealing with Magento core code that’s gotten into advanced use) you’ll find you need to use them.

Every Magento module has the ability to add RequireJS configuration directives via a special view file named requirejs-config.js.

app/code/Package/Module/view/base/requirejs-config.js    
app/code/Package/Module/view/frontend/requirejs-config.js    
app/code/Package/Module/view/adminhtml/requirejs-config.js    

This is a special javascript file that Magento will automatically load on every page load using the area hierarchy. We’re going to give it a try. First, we’ll need to create a module named Pulsestorm_RequireJsTutorial. You can create the base module files using the pestle command line framework with the following command

$ pestle.phar generate_module Pulsestorm RequireJsTutorial 0.0.1

and then enable the module in Magento by running the following two commands

$ php bin/magento module:enable Pulsestorm_RequireJsTutorial
$ php bin/magento setup:upgrade    

If you’re interested in creating a module by hand, or curious what the above pestle command is actually doing, checkout our Introduction to Magento 2 — No More MVC article.

Regardless of how you’ve created it, once you have a module created and enabled, add the following file

//File: app/code/Pulsestorm/RequireJsTutorial/view/base/requirejs-config.js
alert("Hello");

Clear your cache, and load any page in your Magento system. You should see your alert function call. Congratulations — you just added a requirejs-config.js file to your Magento module.

The Purpose of requirejs-config.js

While you can use requirejs-config.js to run any arbitrary javascript, its main job is to

  1. Allow end-user-programmers to add require.config options to Magento’s RequireJS system
  2. Allow end-user-programmers to perform any other setup/configuration their javascript needs

To understand how RequireJS does this, we need to look at where Magento actually pulls in these requirejs-config.js files. If you take a look at any source page in your Magento installation, you should see a tag that looks like this

<script  type="text/javascript"  src="http://magento.example.com/static/_requirejs/adminhtml/Magento/backend/en_US/requirejs-config.js"></script>

This is a special javascript file that Magento generates during setup:di:compile (in production mode) or on the fly (developer and default mode). If you’re unfamiliar with how Magento’s various modes affect front end asset serving, checkout our Magento 2: Serving Frontend Files article. We’re going to assume you’re running in developer mode for the remainder of this article.

If you take a look at the source of this file in a browser, you’ll see your alert statement in a code block that looks something like this

(function() {
    alert("Hello World");
    require.config(config);
})();

While it’s not 100% obvious, by generating this javascript code block from requirejs-config.js, Magento 2 is letting us add extra RequireJS initializations to the system.

This may make more sense with a concrete example. Let’s replace our requirejs-config.js with the following

var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

alert("Done");    

What we’ve done here is define a javascript variable named config, and changed our alert value. If you go back and reload requirejs-config.js it might be clearer now what Magento is doing.

(function() {
var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

alert("Done");
require.config(config);
})();

For every individual requirejs-config.js, Magento will create a chunk of code that looks like this

(function() {
    //CONTENTS HERE
    require.config(config);
})();

but with //CONTENTS HERE replaced by the contents of requirejs-config.js.

var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

alert("Done");
require.config(config);

This means if we define a config variable in our requirejs-config.js file, Magento will ultimately pass it to require.config. This will let any Magento module developer use RequireJS features like shim, paths, baseUrl, map, or one of the many others from RequireJS’s configuration directives.

Understanding Lazy Loading

Another important thing to understand about RequireJS is that modules are lazy loaded. RequireJS will not load any javascript module source file until someone users that javascript module as a dependency.

In other words, if we used the configuration

var config = {
    paths:{
        "my_module":"Package_Module/my_module"
    }
};

Magento will not load the Package_Module/my_module.js file by default. Magento will only load that file once you’ve used it as a module

requirejs(['my_module'], function(my_module){

});

requirejs(['Package_Module/my_module'], function(my_module){

});    

define(['Package_Module/my_module'], function(my_module){

});        

Remember, the point of RequireJS is that day-to-day javascript developers shouldn’t need to worry about how their programs make HTTP requests for their source files. Lazy loading is an implementation detail that, in ideal circumstances, saves the end user the bandwidth of needing to download source files that a particular page might not need.

However, in less than ideal circumstance, this lazy loading behavior can make it tricky to work with older javascript frameworks and libraries. We’ll discuss one example of this below when we talk about a few jQuery gotchas.

Global jQuery Object

Even if you decide RequireJS is not for you, and you want to stick to plain old jQuery in Magento 2, you’ll still need to be aware of how RequireJS interacts with libraries that predate the AMD standard.

In Magento 2, jQuery is loaded as a RequireJS module. This means if you attempts to use code like the following

<script type="text/javascript">
    jQuery(function(){
        //your code here
    });
</script>

Your browser will complain that jQuery is undefined. That’s because the global jQuery object won’t be initialized until you use jQuery as a RequireJS module. If you’re used to writing code like the above, you’ll need to

  1. Replace it with code that kicks off execution of a RequireJS program
  2. Configure that program to use the jquery module as a dependency

In other words, something like this

requirejs(['jquery'], function(jQuery){
    jQuery(function(){
        //your code here
    });
});

To review — you kick off execution of a RequireJS program by calling the requirejs function and passing it a list of module dependencies, and an anonymous javascript function that will act as your programs main entry point.

The list of module dependencies is the first argument to requirejs — i.e. the following code says “My Program is dependent on the jquery module”

requirejs(['jquery'],    

The anonymous function that acts as your program’s main entry point is the second argument to the requirejs function

requirejs(['jquery'], function(jQuery){
    //...
});

RequireJS will call this function for you. For each dependency you configure RequireJS will load the module and pass in its returned (or, in RequireJS speak, exported) module.

Modern versions of jQuery will detect if they’re being included in a RequireJS/AMD environment and define a module that returns the global jQuery object.

// File: http://code.jquery.com/jquery-1.12.0.js
// Register as a named AMD module, since jQuery can be concatenated with other
// files that may use define, but not via a proper concatenation script that
// understands anonymous AMD modules. A named AMD is safest and most robust
// way to register. Lowercase jquery is used because AMD module names are
// derived from file names, and jQuery is normally delivered in a lowercase
// file name. Do this after creating the global so that if an AMD module wants
// to call noConflict to hide this version of jQuery, it will work.

// Note that for maximum portability, libraries that are not jQuery should
// declare themselves as anonymous modules, and avoid setting a global if an
// AMD loader is present. jQuery is a special case. For more information, see
// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon

if ( typeof define === "function" && define.amd ) {
    define( "jquery", [], function() {
        return jQuery;
    } );
}    

RequireJS and jQuery Plugins

There’s another gotcha to using jQuery and RequireJS together. The jQuery library, which predates RequireJS and the AMD standard by many years, developed its own plugin system. While not module based, this system plays relatively nice with javascript’s global by default environment — plugin developers create their plugins by modifying the single global jQuery object.

This presents a problem for RequireJS — as we mentioned above, the global jQuery object is not defined until the jquery module is used in a requirejs program, and until RequireJS calls that program’s main entry-function. This means the long standing way of including a jQuery plugin

<script src="http://magento.example.com/js/path/to/jquery/plugin/jquery.cookie.js">

will fail when the plugin attempts to use the global jQuery object and/or $ alias.

//File: http://magento.example.com/js/path/to/jquery/plugin/jquery.cookie.js
var config = $.cookie = function (key, value, options) {

If you want to include a jQuery plugin for use in Magento 2, you’ll need to do it via RequireJS. Fortunately, the process is relatively straight forward.

First, you’ll want to create an alias for the plugin path using the paths configuration property.

var config = {
    paths:{
        "jquery.cookie":"Package_Module/path/to/jquery.cookie.min"
    }
};

The above configuration creates a module named jquery.cookie that points to the jQuery cookie plugin source file in the Package_Module module.

At this point, you may think its OK to start using jQuery with your plugin by doing something like this

requirejs(['jquery','jquery.cookie'], function(jQuery, jQueryCookie){
    //my code here
});

After all, listing both jquery and jquery.cookie as dependencies should trigger a loading of both files.

You’d be right — but only some of the time. RequireJS loads its module source files asynchronously, and doesn’t promise anything about the order they’re loaded in. That means the above code may load the core jQuery library first. However, depending on other scripts running on the page and the network, it may also load the jQuery cookie plugin first. If this happens, the cookie plugin will load without a global jQuery object. Without a global jQuery object, the plugin initialization will fail.

This may seem like a poor design decision — but you need to remember that RequireJS, and the AMD standard, were designed to stop the use of global state. It’s not surprising that RequireJS doesn’t work seamlessly with a library like jQuery. Even though jQuery is responsible about its use of global state (one global jQuery object), it still uses global state, and RequireJS isn’t going to get in the business of deciding who does and doesn’t use global state responsibly.

Fortunately, there is a solution. The RequireJS shim configuration directive allows you to configure what I’ll call “load order” dependencies. i.e., you can say

Hey RequireJS — when you load the jquery.cookie module? Make sure you’ve completely loaded the jquery module first.

Configuration wise, this looks like

var config = {
    paths:{
        "jquery.cookie":"Package_Module/path/to/jquery.cookie.min"
    },
    shim:{
        'jquery.cookie':{
            'deps':['jquery']
        }
    }
};

We’ve defined a new top level configuration property named shim. This property is a javascript object of key value pairs. The key should be the name of your module (jquery.cookie above). The value is another javascript object that defines the shim configuration for this specific module.

There’s a number of different shim configuration options — the deps configuration option we’ve used above creates a source dependency (distinct from a normal define or requirejs module dependency) that ensures RequireJS will load each listed module (a single [jquery] above) entirely before the module that we’re “shimming” (jquery.cookie above).

The dep configuration option only scratches the surface of what shim is capable of — the shim documentation is worth reading if you’re interested in more details.

With the above configuration in place (and a jquery.cookie.min file in the Package_Module module), you should be able to safely create RequireJS programs that have the jquery cookie plugin added as a dependency.

Require vs. RequireJS

One last note before we wrap up. Throughout the RequireJS documentation, you’ll see reference to two functions

require()
requirejs();

What’s the difference between these two? There isn’t any, they’re the same function.

The AMD standard calls for a function that’s named require. However, RequireJS realized that there may already be code in use that defines a require function. In order to ensure their library could be used along side this code, they provide a requirejs alias to their main, AMD standard function.

Wrap Up

We’ve only scratched the surface of Magento’s use of javascript and modern frontend libraries in this article. However, all these systems are dependent on the RequireJS implementation. If you start there, you should be able to track back any javascript based feature to its inclusion via RequireJS, and through that figure out what’s going on. As always, knowing what a specific library does is always useful — but knowing how the framework your code lives in works is the key to becoming a more productive and rational programmer.

Originally published February 2, 2016
Series Navigation<< Magento 2: Adding Frontend Assets via Layout XMLMagento 2 and the Less CSS Preprocessor >>

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 2nd February 2016

email hidden; JavaScript is required