RequireJS 2.0 Delayed Module Evaluation and Google Maps
The RequireJS behavior changed from version 1.0 to 2.0. Now module dependencies are only loaded after a explicit require()
call or if it is on the dependency tree of some of the require
‘d modules. That means that the module factory (define
callback) won’t execute unless it is needed. That is a huge win for many reasons, less work for the JS engine and code has the same behavior before and after build. It also makes it possible to create alias to complex modules without triggering a download.
On my current project I have ~15 JS widgets that can be placed on any page and the amount of JS required by each widget is small so it makes sense to bundle all the JS into a single file for production (<30KB minified + gzipped excluding jQuery) instead of loading things on demand (a single request have better perf results than multiple requests in many cases).
I will show a very simple technique I used on the project to create an alias to the Google Maps API since I think it can be useful to other people as well in different contexts.
Loading Google Maps with RequireJS
Google Maps v3 API loads multiple JS files, so listening to the first JS file to complete isn’t enough to make sure the whole lib is available, luckily it accepts a callback
parameter. Because of that we can use the async plugin to wait until all the files are loaded before executing the callback.
define(['async!http://maps.google.com/maps/api/js?v=3&sensor=false'], function(){ // google.maps is ready });
But in case you need to use Google maps inside multiple modules it will become cumbersome to type this long path all the times. So we can make use of the new delayed module evaluation behavior and simply define a new module inside our top-level file (usually called main.js
) to be used as an alias:
// convert Google Maps into an AMD module define('gmaps', ['async!http://maps.google.com/maps/api/js?v=3&sensor=false'], function(){ // return the gmaps namespace for brevity return window.google.maps; });
Now you can require it from inside other modules by simply typing:
define(['gmaps'], function(gmaps){ // shorter namespace, no need to type "google.maps" all the time var map = new gmaps.Map(myDiv, mapOpts); });
Before v2.0 the technique above wouldn’t work. You would need to add a nested require to avoid loading the Google Maps API when not needed, also defining a module would automatically trigger the download of all the dependencies. The current behavior was a huge improvement and made AMD modules even more flexible. #winning
Notes
The Google Maps API will only be loaded when needed (when any module that requires the gmaps
module is needed). It is important to notice that the async plugin will NOT work with almond.js (even tho it doesn’t use RequireJS internal methods for script loading, almond can’t handle asynchronous plugins).
We should usually avoid named modules to make code more portable - that way we can move scripts between folders/projects, rename them and create aliases - but in some cases it can be an extremely useful feature. In this case since the amount of code is minimal I preferred to use a named define
instead of a separate file to keep it near to my requirejs.config
call (since it’s just an alias), but it would also work as a separate file if you think that would be a better approach for your app structure. - My main.js
usually contains only the config options and the minimal logic possible to start the app, use what makes more sense in your case.
It is awesome to be able to pack all the JS files into a single file and still have the option to defer some of the loading if needed, check my comment on Ben Nadel blog post about Lazy Loading AMD modules for production to get a better idea of when/how to do it.
AMD works well and is flexible. RequireJS learning curve is a little bit steep (too many config options and different from what most JS devs are used to) but nowadays I can’t see myself going back to naive script concatenation, large source files and awkward namespaces (eg. foo.utils.bar.doSomething()
).
If you don’t need to lazy-load scripts you could use Almond.js instead of RequireJS for production since it is way smaller (Almond is under 1KB) but beware that not all features are available (the async
plugin for instance doesn’t work).
Embrace AMD and be happy.
Further reading
Edit (2012/08/08): made it clear that almond can’t handle the async plugin.