getify / LABjs

Loading And Blocking JavaScript: On-demand parallel loader for JavaScript with execution order dependencies
http://labjs.com
2.28k stars 313 forks source link

Consider preventing the LAB script from loading twice #39

Closed scriby closed 13 years ago

scriby commented 13 years ago

I ran across a somewhat hard to debug problem caused by loading the LAB script twice. I had LAB included in the main page, which loaded some scripts.

Then, later on in the page lifecycle, I used jQuery.load to fetch another page, which also included LAB. When that version of LAB ran, it wiped out the previous $LAB global variable, which reset the registry of which scripts had been loaded. Then, the application reloaded other scripts, such as jQuery, which caused some other things to break...

Perhaps the LAB script could check if it has already been loaded before rebinding the $LAB variable. I'm thinking that it could set a special value $LAB.detect = 'j98hfahdsfasfkdvn' which it tests to see if the current $LAB variable is the lab loader, and if so, not overwrite it.

If you're ok with the change, but don't want to take the time to make it, I would be happy to fork and implement it.

Thanks!

Chris

getify commented 13 years ago

v2.0 includes two API functions for managing multiple LABjs instances. Unfortunately, neither of those were present in LAB 1.x, so if you happen to load 1.x onto a page over the top of a 2.x+ instance, it will pave over it mercilessly without recourse. :(

But, if you load 2.x+ on top of an existing LAB instance, you can call noConflict() on the most recent one, which will roll back LABjs to the previous one (the one it was when it loaded) and return you the new instance in case you want it. This works basically quite the same as jQuery's noConflict(), so I felt that was such a good pattern I just mirrored it.

You can also use the sandbox() API function on an existing copy of LABjs, which creates a new instance of LABjs that is not tied to the global window.$LAB. Whereas noConflict() is a reactive method that you call once a new LABjs has been loaded onto a page, sandbox() is a proactive method that you call defensively, when you first load LABjs. Basically what it does is give you a separate copy of LABjs from the global copy, which means all your internal state will be kept safe in that copy, as long as you keep a reference to that copy around.

Either or both of those methods may be something that can help with your situation. BUT, I think it's possible there's a simpler approach that doesn't need either one.

If you simply capture a reference to the global window.$LAB before using it, and then everywhere that you use LABjs, always use that captured reference instead of relying on the global, then you will be sure to always keep your own copy of LABjs. When/if a second copy of LABjs is loaded and overwrites the global one, you still have reference to the copy, which means your copy will continue to function as you expect. The key is to make sure that you always reference $LAB using your reference, instead of the global reference.

That might look like this:

<script src="LAB.js"></script>
<script>
(function($L){
    // all my code inside here now uses $L instead of $LAB or window.$LAB
    // so I'm safe if someone overwrites the main window.$LAB instance
})($LAB);

Lemme know if any of that helps address your issues.

scriby commented 13 years ago

Thanks for the response. Your solutions all make sense, but isn't quite what I want for my particular case.

Our application is set up to allow pages to either be loaded as a normal web page, or as a dialog within another page. In the case of a normal page load, we want to load LAB.js and load scripts normally. In the case of a dialog within another page, we want LAB to load only the scripts the new page references that aren't already loaded, which AllowDuplicates=false already handles very well.

It's true that we could test for the existence of the $LAB variable, and document.write a script tag to load it if it didn't exist to also solve the problem. But, it would just be simpler for us if the library handled the "don't load myself twice on a page" part of it. If it's not a change you want introduced into lab, I understand and we'll work around it another way.

Thanks,

Chris

getify commented 13 years ago

The "don't load LABjs twice" part is actually contrary to the use-case that caused me to add noConflict() and sandbox(). I had many people with use-cases around needing to maintain separate copies of LABjs. So, I'm not quite sure how I could cleanly serve both the use-cases: "do allow LABjs to gracefully load multiple times' and "don't allow LABjs to load multiple times".

In my original interpretation of your question, it seemed like you were in a non-trusting, non-cooperative scenario. By that, I mean, it seemed like you were in a case where some page or code you don't control might come along and load a LABjs instance over the top of yours without your consent. In response to that, the above 3 solutions I talked about are ways for you to protect yourself from such a case. And in that vein, you would actually want for that other code to not use the same copy of LABjs as you were using, because you'd want for the two to not be able to interfere with each other.

Consider for example the case where your self-contained widget might get loaded into a "hostile" (that is, not one you know or control) page. That page may or may not use LABjs itself. But your widget should be able to use its own private copy of LABjs, regardless of what the page does or does not do with respect to LABjs. That's why noConflict() and sandbox() are useful, to allow silo'd copies of LABjs to work independently of each other.

On the flip side, if you're loading into an environment where there is explicit trust and control, and you can have the environments cooperate, then you don't need those defensive tactics at all. In that case, you want them to "cooperate" on their shared copy of LABjs, so as to take advantage of AllowDuplicates, etc.

To rephrase the cooperation issue... you need to have all pieces of your code that need LABjs to do a "conditional load" of LABjs... that is, each piece of code that uses LABjs needs to check and see if LAB is already loaded, and only load it if it's needed. That way, whichever piece of code runs first on a page, LABjs will be loaded, and all other pieces that run later will simply skip over loading LABjs.

This type of cooperative conditional loading is generally quite easy. And it DOES NOT require document.write(). :)

To conditionally load LABjs, all you have to do is this:

function LABjs_is_ready() {
   // put your code to use LABjs here
}

if (!$LAB) {
   var scriptElem = document.createElement("script"), scriptdone = false;
   scriptElem.onload = scriptElem.onreadystatechange = function () {
      if ((scriptElem.readyState && scriptElem.readyState !== "complete" && scriptElem.readyState !== "loaded") || scriptdone) return false;
      scriptElem.onload = scriptElem.onreadystatechange = null;
      scriptdone = true;
      LABjs_is_ready();
   };
   scriptElem.src = "/path/to/LAB.js";
   document.head.insertBefore(scriptElem, document.head.firstChild);
}
else LABjs_is_ready();

All you need to do is make sure that any page or code which loads LABjs does so conditionally, like this, and your cooperation should be fine to make sure that AllowDuplicates functionality works for different consumers of the $LAB api.

scriby commented 13 years ago

Thanks for the other workaround. We should be able to build a solution which uses that idea.

To address "So, I'm not quite sure how I could cleanly serve both the use-cases: 'do allow LABjs to gracefully load multiple times' and 'don't allow LABjs to load multiple times'."

Isn't the answer to this straightforward? To get a new instance of lab, you call $LAB.sandbox(). Manually loading LAB.js twice on one page would not overwrite the $LAB global. As I mentioned in my first post, you can have the first instance of LAB loaded on the page include some special value that it checks for to determine if the current $LAB variable is the same script.

So, you could set an instance variable "detect" within LAB to a special value, like "oshdf893y23". Then, when LAB loads, you can check if there's already a $LAB variable with that detect value, and if so, don't do anything. If that value doesn't exist, you continue loading like normal.

I think that solves both cases of getting a new copy of lab when you need it, and not getting one accidentally when you don't.

I appreciate your time considering this problem.

Chris

getify commented 13 years ago

Isn't the answer to this straightforward? To get a new instance of lab, you call $LAB.sandbox(). Manually loading LAB.js twice on one page would not overwrite the $LAB global.

But there's a strong precedent in javascript libraries (jQuery, for instance) to overwrite when loaded again, and to allow rolling back with noConflict(). I have to believe there's a reason that most every other script out there doesn't do what you're suggesting.

Speaking of noConflict(), seems like it could solve quite trivially your issue, now that I think about it. Consider:

<script src="LAB.js"></script>
<script>
(function(){
   var $L = $LAB.noConflict();
   if (!window.$LAB) window.$LAB = $L; // $LAB was rolled back too far, so restore it!
})();
</script>

In that way, you ask to load LABjs every time, and you always roll it back with noConflict(), and only if you've rolled it back too far (that is, there's no $LAB anymore), then you re-assign the global. Now, in all your code, always use the global $LAB, and you should be fine. This makes sure there's only ever one instance of $LAB running on a page.

scriby commented 13 years ago

I was ultimately hoping for a solution that didn't require boilerplate code each time LAB was included. We might just end up modifying our copy of LAB to behave like we want (hooray open source).

Thanks for spending time thinking about this! I'm going to go ahead and close the issue.

Chris