Skip to content

Instantly share code, notes, and snippets.

@retoheusser
Last active June 23, 2017 08:29
Show Gist options
  • Save retoheusser/ec670bce3ebcdc08969abaaa7d6d8829 to your computer and use it in GitHub Desktop.
Save retoheusser/ec670bce3ebcdc08969abaaa7d6d8829 to your computer and use it in GitHub Desktop.
Multiple versions of Angular on the same Website

Multiple versions of Angular on the same Website

When developing JavaScript applications that need to be embedded into other websites that are controlled by other code than yours, you should be careful about what you expose globally and should prevent polluting the global namespace (the window object) as much as possible. This can rather easily be achieved with your own code. But what if you depend on third-party libraries such as jQuery, Lodash, Moment.js or Angular.js? All of these put themselves onto the window object regardless whether the original site is already using the same library, probably a different version. And you should just rely on exactly the versions you use within your code and not theirs nor should you overwrite their versions and potentially break their whole application.

I was facing all of these issues because my application needed to embedded into all kinds of environments which already use jQuery or lodash (very common) and even Angular. The Angular.js library even prevents loading itself multiple times on the same page and writes a warning onto the console. So I had to come up with a solution which tries to isolate all my dependencies as much as possible which I will document here.

Function scopes

In JavaScript the way to go to isolate data without being read from outside is using an IIFE: Immediately invoked function expression

(function () {
  //code here won't pollute the global namespace except for stuff that explicitly intends this.
})();

##Project structure You often encounter websites loading multiple script files, e.g. vendor.js for third-party libs and app.js for their own code on top of the libs. I use Grunt to with the grunt-contrib-concat module (or grunt-ng-annotate is also capable of that if you're using it anyway like me) to concatenate all of these libs and then my own code. For some modules the order is important, for example I load jQuery first, lodash, then Angular and some others before all of the rest:

ngAnnotate: {
  dist: {
    src: [
      "app/libs/bower/jquery/*.js",
      "app/libs/bower/lodash/*.js",
      "app/libs/bower/angular/*.js",
      "app/libs/bower/moment/*.js",
      
      "app/src/mymodule1/*.js",
      "app/src/mymodule2/*.js"
    ],
    dest: "dist/js/app.js"
  }
}

This puts all my necessary files into app.js which can the be processed further, minified, etc.

##Isolating the entire code After having all code in one file, I just wrap it in an IIFE. For this purpose, there's another Grunt plugin: grunt-iife which is configured as follows:

iife: {
  dist: {
    files: {
      "dist/js/app.js": "dist/js/app.js" // just overwrites it
    },
    options: {
      bindThis: true, // binds the "this" (usually the window object) keyword to the function
      useStrict: false
    }
  }
}

The output of app.js will be as follows:

(function() {
  // jQuery code
  // lodash code
  // angular code
  
  // my module 1
  // my module 2
}).bind(this)();

What does this? Nothing yet. All the libs will still bind their stuff to the global window object, that's why we need to put all of them in noConflict mode. Some of them have a built-in noConflict function, some don't.

##Using a no conflict wrapper I've added two more JS files to my project, noConflictWrapper-start.js and noConflictWrapper-end.js. The Grunt concatenation task must put one of it at the very first position and the other at the very last in our whole code (before it gets wrapped by the IIFE). So the grunt config needs to be updated as follows.

ngAnnotate: {
  dist: {
    src: [
      "app/src/noConflictWrapper-start.js",
      "app/libs/bower/jquery/*.js",
      "app/libs/bower/lodash/*.js",
      "app/libs/bower/angular/*.js",
      "app/libs/bower/moment/*.js",
      
      "app/src/mymodule1/*.js",
      "app/src/mymodule2/*.js",
      "app/src/noConflictWrapper-end.js"
    ],
    dest: "dist/js/app.js"
  }
}

##Using library-provided noConflict mode jQuery comes with a built-in noConflict function which can be called and resets the $ variable to its previous reference and returns itself to be referenced. After all our code has run, we can reset jQuery when we reach the end of the IIFE, so we can put the following line into `noConflictWrapper-end.js``:

var $ = this.$.noConflict(true); // this == window

You can test this by loading a version of jQuery in your document and using another jQuery version in your application. If you type $.fn.jquery into the console, you should always see the previously embedded version (not the one your application is using). Note: In your angular code, never use $ or jQuery but always angular.element which delegates the calls to jQuery if it is present.

##Implement a custom noConflict mode Angular doesn't provide such a method like jQuery and is not intended to be loaded multiple times. That's why we need to make use of some hacks. Also moment.js doesn't provide such a thing. Let's fill noConflictWrapper-start.js with code:

/**
 * This is executed at the very beginning within the outermost IIFE before any of our libs get defined.
 * We cache potentially defined libs from the host document in order to reassign them after our code has run.
 * (Those who don't have a noConflict() mode)
 */

var originals = {
  "angular": this.angular,
  "moment": this.moment
};

/**
 * Here we empty the window.angular object and assign it to a variable on the current function scope.
 * It needs to be empty because our own angular version is loaded right after that and it checks whether angular
 * was already defined and would refuse to load if that was the case.
 * We also need the angular object on the current function scope once it is defined such that our modules can
 * still use it after window.angular will have been reassigned its previous version at the end of our code.
 */
var angular = (this.angular = {});

As you see we define a variable named angular on the current function scope. This is necessary because after angular will have been defined this local variable will shadow the window.angular. Without this, it would be angular == window.angular.

Then in noConflictWrapper-end.js we're going to reset all libraries without the noConflict method to their previous reference:

var $ = this.$.noConflict(true);

/**
 * Reassign angular to its previous version of the host document.
 */
this.angular = originals.angular;

// moment noConflict method
var moment = this.moment;
this.moment = originals.moment;

If you try angular.version and moment.version on the console, you should now also the the previous versions or undefined if you didn't embed another version on the host document.

Let's sum up our file generated by Grunt:

(function() {
  var originals = {
    "angular": this.angular,
    "moment": this.moment
  };
  
  var angular = (this.angular = {});

  // jQuery lib
  // lodash lib
  // angular lib
  // moment lib
  
  // my module 1
  // my module 2
  
  // You can't use <body ng-app="myApp"> twice on the same page, you need to bootstrap your application here manually
  angular.bootstrap(someDOMElement, ["myApp"]);
  
  var $ = this.$.noConflict(true);
  this.angular = originals.angular;
  
  var moment = this.moment;
  this.moment = originals.moment;
}).bind(this)();

That was pretty easy, right? NOOOOOOOO we still have lodash (_) on the global window object and are overwriting their version of it or even their underscore.js library! Why? Because one of our third-party angular modules (e.g. Restangular) depends on lodash and won't work if it's not defined. Let's come up with a fix for that.

##Third party angular modules depending on libs we isolate (lodash) Let's add a noConflictWrapper-lodash.js to be executed right after the lodash library:

/**
 * This must come right after the definition of lodash and stores a copy of window._ in _ on the function scope.
 */

var _ = this._;

Add it to the Gruntfile:

ngAnnotate: {
  dist: {
    src: [
      "app/src/noConflictWrapper-start.js",
      "app/libs/bower/jquery/*.js",
      "app/libs/bower/lodash/*.js",
      "app/src/noConflictWrapper-lodash.js",
      "app/libs/bower/angular/*.js",
      "app/libs/bower/moment/*.js",
      
      "app/src/mymodule1/*.js",
      "app/src/mymodule2/*.js",
      "app/src/noConflictWrapper-end.js"
    ],
    dest: "dist/js/app.js"
  }
}

After this special no conflict wrapper, every module which uses a function of _ will use the _ object on the function scope, which shadows the original window._. It is important to understand that until the noConflictWrapper-end.js is reached, it is _ == window._ and then in the end it gets reset to its previous reference. But because of the asynchronous nature of JavaScript, the module will still make calls to _ after the reference has been reset to a previos version or undefined. But the function scoped _ will still hold the reference to the lodash we need to use and ensures its safe usage at any time.

Finally we also need to call lodash's built-in noConflict method and we want it to have as an injectable dependency for the use in our own code. So we define a lodash module for our application which does all the work:

(function() {
  "use strict";

  angular.module("myApp.lodash", [])
    .constant("_", window._)
    .config(lodashNoConflict);

  /**
   * _ was defined right after the definition on the function scope of the outermost IIFE and must shadow window._
   * because other third-pary angular modules (restangular) depend on lodash without dependency injection.
   * This function sets _ on the function scope to the lodash we use and window._ back to its
   * original value of the host page.
   */
  function lodashNoConflict() {
    _ = window._.noConflict();
  }
})();

After bootstrapping our application, it will define lodash as an application-wide constant which can be injected as dependency into all our modules. At this moment our lodash is window._. The lodashNoConflict function in the config block will be executed right after it and resets it to the previous reference while our constant still holds the one we need.

Now we have isolated all our third-party libs and don't interfere with the different versions of the host document anymore. I hope you'll now have as much fun as me implementing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment