- Introduction to Magento 2 — No More MVC
- Magento 2: Serving Frontend Files
- Magento 2: Adding Frontend Files to your Module
- Magento 2: Code Generation with Pestle
- Magento 2: Adding Frontend Assets via Layout XML
- Magento 2 and RequireJS
- Magento 2 and the Less CSS Preprocessor
- Magento 2: CRUD Models for Database Access
- Magento 2: Understanding Object Repositories
- Magento 2: Understanding Access Control List Rules
- Magento 2: Admin Menu Items
- Magento 2: Advanced Routing
- Magento 2: Admin MVC/MVVM Endpoints
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
- Run a javascript program that doesn’t default to the global namespace
- 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
- Allow end-user-programmers to add
require.config
options to Magento’s RequireJS system - 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
- Replace it with code that kicks off execution of a RequireJS program
- 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 thejquery
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.