kubetail-org / loadjs

A tiny async loader / dependency manager for modern browsers (899 bytes)
MIT License
2.58k stars 150 forks source link

Allow to specify the target "document" or "window" object #18

Open niondir opened 8 years ago

niondir commented 8 years ago

Hi, nice library. But I'm missing a way to define the target document. I would like to use this library to load scripts into iFrames.

amorey commented 8 years ago

Thanks! If you load the library within the iframe (and the iframe is from the same domain) then you should be able to make calls to loadjs running in the iframe from the main window. Making the target document optional will increase some complexity to the library so I'm not sure it's a better option.

Andres

niondir commented 8 years ago

Technically that's possible but from the application point of view it's way more complicated. My iFrame can not be empty but needs code (at least a script tag) to load this library first - which actually should be the job of this library, right? ;) When I load this library to the iFrame I can use the same method to load all other scripts and then I do not need the library anymore. You see what I mean?

In the end I'm looking for something to simply load dependencies of plugins in a way that I do not get version conflicts. From all the script loaders I liked this one the most, just that it lacks this one feature.

I would treat the target as an optional parameter, default to window.

amorey commented 8 years ago

That sounds like a useful use case but it would add some complexity to the library so it's worth thinking about other ways to approach the problem. One option is to add loadjs to the iframe dynamically. This is working for me locally:

<html>
  <head>
    <script id="loadjs">
      loadjs=function(){function n(n,e){n=n.push?n:[n];var t,o,r,f,u=[],a=n.length,l=a;for(t=function(n,t){t.length&&u.push(n),l--,l||e(u)};a--;)o=n[a],r=i[o],r?t(o,r):(f=c[o]=c[o]||[],f.push(t))}function e(n,e){if(n){var t=c[n];if(i[n]=e,t)for(;t.length;)t[0](n,e),t.splice(0,1)}}function t(n,e){var t=document,o=t.createElement("script");o.src=n,o.onload=o.onerror=o.onbeforeload=function(t){e(n,t.type[0],t.defaultPrevented)},t.head.appendChild(o)}function o(n,e){n=n.push?n:[n];var o,r=n.length,f=r,u=[];for(o=function(n,t,o){if("e"==t&&u.push(n),"b"==t){if(!o)return;u.push(n)}f--,f||e(u)};r--;)t(n[r],o)}function r(n,t,r,i){var c,a,l;if(t&&!t.call&&(c=t),a=c?r:t,l=c?i:r,c){if(c in u)throw new Error("LoadJS: Bundle already defined");u[c]=!0}o(n,function(n){n.length?(l||f)(n):(a||f)(),e(c,n)})}var f=function(){},u={},i={},c={};return r.ready=function(e,t,o){return n(e,function(n){n.length?(o||f)(n):(t||f)()}),r},r.done=function(n){e(n,[])},r}();
    </script>
  </head>
  <body>
    <script>
      var loadjsSrc = document.getElementById('loadjs').innerHTML;

      // create iframe
      var iframeEl = document.createElement('iframe');
      iframeEl.src = "javascript:false;";
      document.body.appendChild(iframeEl);

      var iframeWin = iframeEl.contentWindow;

      // add loadjs to iframe
      var scriptEl = iframeWin.document.createElement('script');
      scriptEl.innerHTML = loadjsSrc;
      iframeWin.document.body.appendChild(scriptEl);

      // load jquery 2.2.4 in iframe
      var iframeLoadJS = iframeWin.loadjs;
      iframeLoadJS(['//code.jquery.com/jquery-2.2.4.min.js'], 'jquery');

      // load jquery 2.2.3 in main window
      loadjs(['//code.jquery.com/jquery-2.2.3.min.js'], 'jquery');

      // define callbacks
      iframeLoadJS.ready('jquery', function() {
        console.log('iframe version: ' + iframeWin.$.fn.jquery);
      });

      loadjs.ready('jquery', function() {
        console.log('main window version: ' + $.fn.jquery);
      });
    </script>
  </body>
</html>
niondir commented 8 years ago

Okay I see the reason to keep the lib simple, it's also one of the main selling points.

What is hard with the above code is to use npm to manage the loadjs dependency but I can gat that working with webpack or maybe just create another library to archive this.

niondir commented 8 years ago

Completely untested (I will test manually and via unittests later) but just to provide a possible solution first: https://github.com/Niondir/loadjs/commit/08ca6cb1d94b18ed1b741831077bdb8809720f6d

Adds only 12 characters to the min.js

Do not support bundles when using target, because an iFrame represents already an isolated bundle. That also makes the use of the target in place of the bundleId very intuitive.

The target is passed to the success callback to be able to retrieve the loaded scripts.

amorey commented 8 years ago

Here's a version of LoadJS that supports different targets (branch:target): https://github.com/muicss/loadjs/tree/target

To simplify the API I modified the interface to accept a dictionary argument instead of sequential functions:

loadjs(['file1.js', 'file2.js'], 'mybundle', {
  success: function() {},
  fail: function() {},
  target: document
});

Please try it out and let me know what you think. It'd be useful to get some feedback on the new library before merging into master.

niondir commented 8 years ago

I had the same idea with the options object. Its a good idea in general. Just the target and bundle is a dangerous combination. If you specify a bundle, everything related it must be cached in the context of the given target. e.g. 2 iFrames loading the same bundle name should not fail but load it twice (once per iFrame)

Another thought about the options object is, that a fluid API would be nice for all the callbacks.

loadjs(['file1.js', 'file2.js'], 'mybundle', {
  target: document
})
.success(function() {})
.fail(function() {});

It's maybe just an aesthetic aspect. Futures would be perfect here but that would raise the requirements on the Browser version.

amorey commented 8 years ago

I had the same thought about Futures. It might not be too difficult to pass back a simple object with success/fail callback methods...

With regards to bundles and iframes, at first I was worried about scoping the bundles to the iframe but I think it's actually convenient to be able to use .ready() in the main window based on activity in the iframe. Also the bundle id is flexible enough that you can scope the string yourself (e.g. "iframe1-mybundle", "iframe2-mybundle").

niondir commented 8 years ago

Having .ready() work is nice. Just another thought is to change it to .ready('some-bundle', target), where target is optional and default to window. Not sure how much effort it would be to manage the bundles "per target".

But that can also be adopted in another change. For now any way of being able to set the target would help :)

Btw: some code that allow chaining of the ready function is in that branch: https://github.com/niondir/loadjs/tree/chaining-ready Including Tests.

amorey commented 8 years ago

This feature is implemented in the loadjs:iframe-target branch: https://github.com/muicss/loadjs/tree/iframe-target

However, I got a Permission Denied error running the tests in IE. Based on what I've read it seems like IE won't let you manipulate the iframe unless it gets loaded with the X-Frame-Options HTTP header: https://davidwalsh.name/iframe-permission-denied

How does your app get around this issue?

niondir commented 8 years ago

I was not digging into this. Currently I just load everything in the global namespace - loading to an iFrame went a little bit down on my todo. Regarding the issue I guess I would just set the header on the server. But it's good to know.

amorey commented 8 years ago

If you need to load the iframe from the server anyway then it seems like adding loadjs to the iframe server side might simplify things. Let me know if you end up using iframes in your app.

niondir commented 8 years ago

There seem to be some caveats with iFrames that let me go with global imports right now, so it might need some time for the topic to become important enough again, but I will let you know.

By the way, the related project is https://github.com/Niondir/iot-dashboard where plugins can load dependencies from other URL's.