JavaScript in Magento 2

Update: The talk is now up on YouTube.

I’ve been doing Magento 2 for more or less ten months full time now, and I’ve spent a lot of time writing JavaScript. Since the new JavaScript framework is one of my favourite features, I’ve been wanting to write about it for a while, but struggled with motivation and knowing where to begin. I’m giving a presentation that is contemporaneous with this post, and so I’ve been forced into writing it down. This post is a companion to that talk, with a bit more context around the slides.

Links

If you don’t want to read and just want to look at the stuff I link to in this post and in the talk, have at it. There are some great resources.

The Actual Post

The overriding theme of the changes as they pertain to JavaScript in Magento 2 is that it’s grown up, big time. There’s more code and more engineering and less praying that stuff will work. There’s even a test framework, that’s tricky to get working, but pretty cool in the end. That’s a topic for another time.

But on top of that maturing process, there are three big changes:

  1. There is a really cool app framework that you can use to get data from Magento’s APIs. I figure this is important and worth learning, but I’m not going to mention it further in this post, because it’s a big big topic. The framework is based on KnockoutJS, though, and your time would be well spent learning about it.

  2. Inline Scripts have been banished. This allows us to implement Content Security Policy across our sites, which is the next generation of online security.

  3. Scripts no longer block the rendering of the page, so your users can start spending money without having to wait for your stupid jQuery code.

To illustrate this, we’re going to port the following code:

<script> $j(function ($) $('#element-id').addClass( '<?php echo $this->getClassName() ?>' ); }); </script>

from a fairly typical Magento 1 style JavaScript snippet to an implementation that will work in Magento 2, and adhere to the best practices as they exist today.

The actual result is a bit that replaces the above snippet in the template, and an external piece of JavaScript. So let’s get stuck in.

Inline Scripts

The most heinous part of our original piece of art is that its using an inline <script /> tag to get data from the server to the page. This is bad for lots of reasons like mixing behaviour and knowledge, but it’s especially bad because it blocks us from implementing Content Security Policy.

Content Security Policy is the next step in web security. Under the current model, the browser will run any code that gets into the page. This seems reasonable on the outside, but it’s not. Because it’s surprisingly, unnervingly easy to get code into a webpage. The act of getting code into a webpage for nefarious purposes is called Cross Site Scripting (XSS). Owasp have a pretty good set of references on XSS, and how to mitigate it.

CSP adds another layer to this by not automatically running anything that gets put in front of it. By sending a carefully constructed HTTP header, you can whitelist where different assets are allowed to come from, meaning that a browser will only run JavaScript from a small list of places for any given page.

You can read an introduction to Content Security Policy, or dive into the evergreen Mozilla Developer Network, and check out the browser support for CSP here. Spoiler: it’s pretty good.

But how does this tie into inline scripts? Well, if I can XSS some HTML to include a JavaScript file, then I can include an inline script with all that stuff. The browser has no way of telling my mean inline script apart from your intentional one.

And rather than take that risk (which would invalidate the entire point of CSP), the standards body chose to outlaw inline scripts. So detach yourself from them, because security won’t wait around for you.

I’ve written about why we like inline scripts so much previously. In my mind, it boils down to how easy it is to put server based dynamic data into the page like that. Magento has given us a way of doing this, where we can just dump our PHP, and not have to put our JavaScript in with it. Voila:

<script type="text/x-magento-init"> { "#element-id": { "js/set-class-name": { "className": "<?= $block->;getClassName() ?>" } } } } </script>

Now, this isn’t an inline script tag, because it’s got a custom type. Browsers don’t execute code with a custom MIMEtype. So we’re safe here. What happens, is that this little bit of JSON is picked up later on by Magento’s JavaScript. It looks at the first key, which should be a CSS selector. This tells us which element we want to run our JavaScript on. Then we get another object, and all the keys in here relate to a certain JavaScript component. The key is just a path to a file. We don’t put in the baseUrl (which includes /static/package/theme/locale). We don’t include the .js suffix, either. The value that corresponds to these keys, this is important. This is freeform JSON data, and is used as configuration when we initialise the component. This is how we get special data into our JavaScript from the PHP side of things. So you can see here, I’m creating a className variable and setting it to something from a block method.

This is how Magento are expecting us to load data into our templates. How this gets turned into actual running code is the other side of this coin.

Asynchronous Scripts

Which brings us to the other tentpole benefit of Magento 2’s new JavaScript architecture: It doesn’t load in such a way that the rendering of the page is blocked.

In Magento 1, all external JavaScript is included within the <head /> part of the document. When the browser encounters a <script /> tag, it pauses rendering of the page to download and execute the script. It pauses, because it doesn’t know whether or not the script it’s downloading will change what it needs to do to render. In truth, this is all basically to support document.write. Anyway, most of your scripts can afford to wait until after the DOM renders. In fact, if you’re wrapping your script in a DOMReady event listener, you’re enforcing that behaviour. Furthermore, you have a decent incentive to wait until the DOM is rendered to download your script, since the browser just sits there with a dumb blank screen until it’s finished, and you want to get a usable page in front of your customers faster.

Google highlight this as one of their biggest performance indicators inside pagespeed. So if you think I’m having you on, you should trust them.

There are many ways you can achieve this, But Magento have chosen to use a library called RequireJS. It’s pretty mature and very well known among certain groups. To use it, you need to write your JavaScript in a custom format called AMD. That stands for Asynchronous Module Definition.

It’s pretty well documented, but the short story is that you get a function to define modules. You say what your module does, and what it needs to do it, and put it in a file. RequireJS handles the rest. Let’s take a look:

define([ 'jquery' ], function setClassName ( $ ) { function main (el, config) { $(el).addClass(config.className); }; return main; });

To be picked up by Magento, this script would need to be placed in your theme, under the directory web/js/set-class-name.js. If your script is inside a module, you’ll need to change the JSON you’re using to include it to include your module name at the beginning. For example, if the JavaScript was in the web/js directory of the module ACME\Integration, then the path in the JSON snippet above would be ACME_Integration/js/set-class-name.

Note that you can use requirejs-config.js to set a human readable name that maps to that URL. This is generally considered best practice, since it makes it easier for somebody to override your component with something else in a customisation. To read more about how to configure this, you’ll want to read Magento’s documentation, and then RequireJS' documentation, and then look at this example.

In AMD, the function you pass as an argument is run, and its return value becomes the module. In this case (and in lots of cases), we are returning a function as the module value.

This function is special, though. Since it takes two arguments, first an element, and then a configuration object, it’s what Magento considers a JavaScript component.

Magento has written some code to pick up those <script /> tags with custom types, parse the JSON out of them, and then use RequireJS to load the modules asked for in the JSON. Once it loads, it gets the elements matched by the selector, and calls the JavaScript component function, passing in each element (one at a time), and the configuration object from the initialisation JSON.

The code that does this is itself an AMD module. At some level, there is a module at the top of the tree, that Magento is instructing to be loaded directly. All RequireJS modules live inside their own file, and each is loaded on demand by RequireJS as it is needed. This is more efficient than the Magento 1 approach, since the only JavaScript that gets loaded is that required by a template actually rendered inside the page. This renders immaterial our tendency to be lazy by putting JavaScript in the <default /> handle of the XML layout.

Many people have highlighted an issue with this, though. In Magento 1, you can switch a configuration flag to combine all those millions of scripts we put in through the XML. I dislike this feature for a number of reasons, but you don’t need this in Magento 2. First of all, while it’s loading these scripts, your page is visible. For the few seconds that your navigation might not work, your customer is probably still getting their bearings, trying to work out why your page loaded so damn quickly. Tehe other reason you don’t need to worry is that you can make it super fast by turning on HTTP2. If the performance impact of many requests bothers you, I implore you to enable HTTP2 on your stores. It’s well supported, and designed to handle exactly this kind of use case.