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?
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:
Have the module export a global by wrapping the script with:
(function(define){ ... })(null);
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.
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.
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With