JQuery and RequireJS, Supporting Legacy Code
Intuit is considered one of the more "mature" and "older" companies in the Silicon Valley due to the fact that we have been around for 30+ years. As a result, Intuit has shipped a variety of different products over the years. One of our core products is QuickBooks and we have both Desktop and Online variations. For those of you unfamiliar with QuickBooks, it is accounting software for small businesses. And there's nothing more sexy than accounting software.
Mmmmmmm, just look at those beautiful pie charts and numbers.
Context setting because....
...it's 2016 and you may see the title and think "JQuery and Requirejs?! It's 2016 you fuckin' scrub." Yes I know that Webpack, Browserify, and all that shizzle exist. And yes I know that, for some reason, JQuery is considered the bane of Javascript and people loveeeeee their vanilla Javascript and Babel. DOM manipulations? Ha! I'm riding on that Angularjs bandwagon ALL DAY (oh wait, it's React now...)
But anyways, at Intuit, I work on a webapp platform called QuickBooks Financing. One of the problems I encounter involve supporting both our legacy Desktop customers as well as our customers who use our modern cloud products. It is our responsibility to ensure our webapp can support both our Desktop and Online customers. Because it is very difficult to manage a small business, small business owners rarely have the time to upgrade or update their computers. All they care about is running their business. As a result, a lot of our customers still use our older Desktop software on Windows XP. You know what that means! IE8 support baby~.
TL;DR: We gotta support IE8
So, for this post I'm going to discuss an issue I've encountered while integrating JQuery and RequireJS. Most likely you may have encountered a similar situation before if you use both libraries. As a matter of fact, this issue may still be relevant no matter which module loading system you use. This isn't super well documented online (or at least I haven't found them all in one place), so let's get to it!
JQuery and AMD 101
Ideally, with today's modern Javascript design patterns, you should want all of your Javascript code to be modular and have all their dependencies managed well, regardless of whether it's through the AMD or CommonJS spec. You'll also want to apply this concept with all of the common libraries you're used to reference globally, including JQuery. But, there are some problems when applying this concept with JQuery in partular. There's nothing wrong with the JQuery library that prevents you from referencing it as a module. However, the issue lies in any additional third-party libraries you may use that integrate with it.
JQuery is unique in that there are a lot of Javascript libraries that depend on it. Traditionally, JQuery will initialize and attach itself to the browser's window
object. This makes it globally available for any library to access, extend, or modify. When importing JQuery plugins, they will try to attach themselves to the jQuery
object that has already been loaded. So if you now have an app that uses RequireJS, when you want to load JQuery to your page, you are left with two choices: you can either load it through AMD, or as a global script import outside of RequireJS.
When you want to import JQuery through AMD, you can simply import it by referencing it as a module dependency like any other library. JQuery is AMD compatible, so this works exactly as expected. In addition, some of the libraries you import may have a hard JQuery module dependency, which is fine if they also support the AMD spec. HOWEVER, the biggest problem you'll run into is when your libraries are not updated to support the AMD spec or if they define the JQuery dependency incorrectly. Unfortunately, if you're in the same situation as me and have to use some plugins that help support IE8/IE9, they may not be actively maintained. That's just the nature of a lot of plugins created to solve IE8 qualms, the developers that manage them get bored/tired/sick of supporting IE8 and stop maintaining them altogether. As a result, these plugins do not know how to access the JQuery module, will instead try to find a global jQuery
object, and freak out.
To resolve this issue, you may try loading JQuery and all of of your libraries/plugins through simple <script import="" />
statements instead. This will solve the issue of where your plugins do not support AMD. BUT, if you have other libraries where you actually try to apply AMD, if these modules have a hard dependency on JQuery, they will not recognize that you've already defined a global jQuery
object. Thus, RequireJS will reinitialize JQuery and undo all the importing and referencing that you did earlier with your script
tags.
You can check out the RequireJS documentation here for more info.
So how do we solve this?
As specified in the aforementioned notes, both approaches will run into two different issues, but both approaches have workarounds. The two workarounds are defined as followed:
- Modify the RequireJS config to shim all the non-AMD compatible modulesĀ and load all JQuery plugins in the beginning of page load. (ideal approach)
- Import JQuery and all of its plugins outside of AMD, then reassign the JQuery module to the global
window.jQuery
object.
Approach 1
For this approach, you simply have to modify the RequireJS config and add a new module like below:
// JQuery custom module
define([
"jquery",
"libs/jquery.screwdefaultbuttonsV2",
"autonumeric",
"formatter",
"bootstrap"
], function ($) {
return $;
});
// RequireJS Config
require.config({
baseUrl: "/static/js",
deps: ["base"],
map: {
// '*' means all modules that define the specified module will get the corresponding module
// Some modules, for some reason, say require("jQuery") instead of require("jquery")
"*": { "jQuery": "jquery"}
},
// map names to nicer paths, should be mainly used for bower_components and the common directory
paths: {
"bootstrap": "../bower_components/bootstrap-sass/assets/javascripts/bootstrap",
"jquery": "../bower_components/jquery/dist/jquery",
"jquery-custom": "./libs/jquery-custom",
},
shim: {
// Specifies dependencies for jQuery plugins that do not call define() or are AMD-spec compliant
"bootstrap": {
"deps": ["jquery"]
},
"libs/jquery.screwdefaultbuttonsV2": {
"deps": ["jquery"]
}
}
});
Notice that in the config we have defined a map, shim, and a custom JQuery module. The shim will figure out how to wrap the non-AMD compatible libraries and define its dependencies. The custom JQuery module will define all of the JQuery plugins to load.
The map is used because there is an interesting use case where some JQuery plugins support the AMD spec, but do not define JQuery correctly!!!! It is a standard to define your JQuery dependency as jquery
, but some will define it as jQuery
. See here.
Approach 2
For this approach, you can continue to import jQuery and all of its plugins like so:
<script src="/static/bower_components/jquery/dist/jquery.js"></script>
<script src="/static/bower_components/autoNumeric/autoNumeric.js"></script>
<script src="/static/bower_components/formatter/dist/jquery.formatter.js"></script>
<script src="/static/bower_components/bootstrap-sass/assets/javascripts/bootstrap.js"></script>
Then you can simply delete the jQuery reference in the RequireJS config paths
, and add this to the main app entry point file (in the example you can see that deps
is set to base
, which is the base.js
file in our project).
define("jquery", [], function() {
return window.jQuery;
});
This will reassign any refeernce to the "jquery" module with the global window.jQuery
object!
Why approach 2 is probably better
Approach 1 is the ideal approach because it is following the AMD spec 100%. If an app is using AMD, everything should be done under that spec. However, when implementing Approach 1, my team ran into an issue when compiling the scripts for production with almondjs. As mentioned earlier, some JQuery libraries are compatible with the AMD-spec, but they reference the wrong name, i.e. reference jQuery
instead of jquery
. Thus, it is necessary to define a map
in the RequireJS config. For some reason, the global jQuery
object is defined with a capital Q, but if you want to reference jquery through AMD, the convention is to define it with a lowercase q.
WTF, no wonder this is a mess.
Anyway, even though you defined a map
in the config, because almondjs is used to compile the code, almondjs does not seem to respect the map config options, as specified here and here.
As a result, when compiling the code with almondjs, some of the JQuery plugins will not work.
Intuit also has some proprietary libraries that the webapp is using that are not compatible with any Javascript module spec. Managing the RequireJS config will create too much overhead. To prevent blocking development within the team, I decided to move forward with Approach 2 until a workaround has been made. Further research and investigation will have to be done and I haven't had the time to see it through. So if you have encountered something similar to this in the past, please share your thoughts! But until then, my team's gotta live with this.