Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to circumvent RequireJS to load module with global?

I'm trying to load a JS file from a bookmarklet. The JS file has this JS that wraps the module:

(function (root, factory) {
    if (typeof module === 'object' && module.exports) {
        // Node/CommonJS
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(factory);
    } else {
        // Browser globals
        root.moduleGlobal = factory();
    }
}(this, function factory() {
    // module script is in here

    return moduleGlobal;
}));

Because of this, if the webpage uses RequireJS, the script will not export a global when it loads. To get around this I temporarily set define to null, load the script, then reset define to its original value:

function loadScript(url, cb) {
    var s = document.createElement('script');
    s.src = url;
    s.defer = true;
    var avoidRequireJS = typeof define === 'function' && define.amd;
    if (avoidRequireJS) {
        var defineTmp = define;
        define = null;
    }
    s.onload = function() {
        if (avoidRequireJS) define = defineTmp;
        cb();
    };
    document.body.appendChild(s);
}

This works, but it seems to me like it could be problematic to change a global variable when other parts of the application could depend on it. Is there a better way to go about this?

like image 608
Web_Designer Avatar asked May 18 '17 03:05

Web_Designer


2 Answers

You may fetch the script using XMLHttpRequest, jQuery.ajax or the new Fetch API.

This will allow you to manipulate the script and reassign define before executing it. Two options:

  1. Have the module export a global by wrapping the script with:

    (function(define){ ... })(null);
    
  2. Handle the module exports yourself by wrapping the script with:

    (function(define, module){ ... })((function() {
        function define(factory) {
            var exports = factory();
        }
        define.amd = true;
        return define;
    })());
    

You can then load it using a new <script> element or eval 😲.

Note that when using XHR, you may have to address CORS issues.

like image 77
Dheeraj Vepakomma Avatar answered Oct 28 '22 14:10

Dheeraj Vepakomma


If you can use the AJAX method above, that will be best. But as stated, you will need to deal with CORS issues, which is not always trivial - even impossible if you do not control the origin server.

Here is a technique which uses an iframe to load the script in an isolated context, allowing the script to export its global object. We then grab the global object and copy it to the parent. This technique does not suffer from CORS restrictions.

(fiddle: https://jsfiddle.net/qu0pxesd/)

function loadScript (url, exportName) {
  var iframe = document.createElement('iframe');
  Object.assign(iframe.style, {
    position: 'fixed',
    top: '-9999em',
    width: '0px'
  });
  var script = document.createElement('script');
  script.onload = function () {
    window[exportName] = iframe.contentWindow[exportName];
    document.body.removeChild(iframe);
  }
  script.src = url;
  document.body.appendChild(iframe);
  iframe.contentWindow.document.open();
  iframe.contentWindow.document.appendChild(script);
  iframe.contentWindow.document.close();
}
loadScript('https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js', 'jQuery');

I ran a quick test to see if a memory leak would happen from deleting the iframe, and it appears to be memory safe. Here's the snapshot of loading a script 100 times, resulting in 100 different iframes and 100 different instances of jQuery loading.

enter image description here

The parent window's jQuery variable is continuously overwritten, meaning only the last one prevails and all previous references are cleaned up. This is not entirely scientific and you will need to do your own testing, but this should be safe enough to get you started.

Update: The above code requires that you know the name of the exported object, which is not always known. Some modules may export multiple variables too. For example, jQuery exports both $ and jQuery. The following fiddle illustrates a technique for solving this issue by copying any global objects which did not exist before the script was loaded:

https://jsfiddle.net/qu0pxesd/3/

like image 37
Ryan Wheale Avatar answered Oct 28 '22 14:10

Ryan Wheale