jQuery.getScript alternative in native JavaScript
Asked Answered
L

9

64

I'm trying to load JS scripts dynamically, but using jQuery is not an option.

I checked jQuery source to see how getScript was implemented so that I could use that approach to load scripts using native JS. However, getScript only calls jQuery.get()

and I haven't been able to find where the get method is implemented.

So my question is,

What's a reliable way to implement my own getScript method using native JavaScript?

Thanks!

Lobworm answered 30/5, 2013 at 15:6 Comment(2)
possible duplicate of javascript ajax request without frameworkScorch
This is the closest I could find to a dupe: What JavaScript event fires when async resources are finished loading?Anastasio
A
36

You can fetch scripts like this:

(function(document, tag) {
    var scriptTag = document.createElement(tag), // create a script tag
        firstScriptTag = document.getElementsByTagName(tag)[0]; // find the first script tag in the document
    scriptTag.src = 'your-script.js'; // set the source of the script to your script
    firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
}(document, 'script'));
Anastasio answered 30/5, 2013 at 15:8 Comment(6)
@Baszz huh? It creates a script element, sets the source, and then appends it to the DOM with insertBefore(). Where do you see $.get?Anastasio
You guys are right...thought it was about how the getScript() was implemented.Limicolous
How would you go about adding a callback that fires when the script is done loading and parsing with this method?Thymic
@Thymic as this question is already answered you will have to ask a new one, but I'm happy to answer it if/when you do.Anastasio
Nevermind, figured out. See here for those looking for the same: https://mcmap.net/q/299906/-jquery-getscript-alternative-in-native-javascriptThymic
This doesn't make the request "accept" header to be identical to this of jquery getscriptRutherfurd
T
100

Here's a jQuery getScript alternative with callback functionality:

function getScript(source, callback) {
    var script = document.createElement('script');
    var prior = document.getElementsByTagName('script')[0];
    script.async = 1;

    script.onload = script.onreadystatechange = function( _, isAbort ) {
        if(isAbort || !script.readyState || /loaded|complete/.test(script.readyState) ) {
            script.onload = script.onreadystatechange = null;
            script = undefined;

            if(!isAbort && callback) setTimeout(callback, 0);
        }
    };

    script.src = source;
    prior.parentNode.insertBefore(script, prior);
}
Thymic answered 17/1, 2015 at 17:40 Comment(2)
@RubenMartinezJr. set a timeout for a couple seconds after the call and clear it on the success callback; if it fires you can probably assume it's failed (unless your source is extremely slow).Thymic
Thank you for this. But note, this script is not a true replacement for jQuery's getScript. The first (minor?) issue is that it doesn't return any values in the callback. The second issue is that (it seems) jQuery's getScript skips an execution frame, as far as I can tell. Thus, the callback line could at least be rewritten as if (!isAbort && callback) setTimeout(callback, 0).Construct
A
36

You can fetch scripts like this:

(function(document, tag) {
    var scriptTag = document.createElement(tag), // create a script tag
        firstScriptTag = document.getElementsByTagName(tag)[0]; // find the first script tag in the document
    scriptTag.src = 'your-script.js'; // set the source of the script to your script
    firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); // append the script to the DOM
}(document, 'script'));
Anastasio answered 30/5, 2013 at 15:8 Comment(6)
@Baszz huh? It creates a script element, sets the source, and then appends it to the DOM with insertBefore(). Where do you see $.get?Anastasio
You guys are right...thought it was about how the getScript() was implemented.Limicolous
How would you go about adding a callback that fires when the script is done loading and parsing with this method?Thymic
@Thymic as this question is already answered you will have to ask a new one, but I'm happy to answer it if/when you do.Anastasio
Nevermind, figured out. See here for those looking for the same: https://mcmap.net/q/299906/-jquery-getscript-alternative-in-native-javascriptThymic
This doesn't make the request "accept" header to be identical to this of jquery getscriptRutherfurd
S
15

use this

var js_script = document.createElement('script');
js_script.type = "text/javascript";
js_script.src = "http://www.example.com/script.js";
js_script.async = true;
document.getElementsByTagName('head')[0].appendChild(js_script);
Singley answered 30/5, 2013 at 15:9 Comment(1)
I didn't vote, but I'd venture to guess that declaring a variable without var, unnecessarily setting the type and appending to the head irked someone enough to lose some rep over it.Anastasio
P
14

This polishes up previous ES6 solutions and will work in all modern browsers

Load and Get Script as a Promise

const getScript = url => new Promise((resolve, reject) => {
  const script = document.createElement('script')
  script.src = url
  script.async = true

  script.onerror = reject

  script.onload = script.onreadystatechange = function() {
    const loadState = this.readyState

    if (loadState && loadState !== 'loaded' && loadState !== 'complete') return

    script.onload = script.onreadystatechange = null

    resolve()
  }

  document.head.appendChild(script)
})

Usage

getScript('https://dummyjs.com/js')
.then(() => {
  console.log('Loaded', dummy.text())
})
.catch(() => {
  console.error('Could not load script')
})

Also works for JSONP endpoints

const callbackName = `_${Date.now()}`
getScript('http://example.com/jsonp?callback=' + callbackName)
.then(() => {
  const data = window[callbackName];

  console.log('Loaded', data)
})

Also, please be careful with some of the AJAX solutions listed as they are bound to the CORS policy in modern browsers https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

Plateau answered 20/5, 2020 at 0:55 Comment(2)
Another big plus of this solution is that by using a Promise it allows for the async/await syntaxGurolinick
Thanks, great solution! In older browsers this polyfill can also be used vanillajstoolkit.com/polyfills/promiseGerius
W
13

Firstly, Thanks for @Mahn's answer. I rewrote his solution in ES6 and promise, in case someone need it, I will just paste my code here:

const loadScript = (source, beforeEl, async = true, defer = true) => {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script');
    const prior = beforeEl || document.getElementsByTagName('script')[0];

    script.async = async;
    script.defer = defer;

    function onloadHander(_, isAbort) {
      if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
        script.onload = null;
        script.onreadystatechange = null;
        script = undefined;

        if (isAbort) { reject(); } else { resolve(); }
      }
    }

    script.onload = onloadHander;
    script.onreadystatechange = onloadHander;

    script.src = source;
    prior.parentNode.insertBefore(script, prior);
  });
}

Usage:

const scriptUrl = 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad&render=explicit';
loadScript(scriptUrl).then(() => {
  console.log('script loaded');
}, () => {
  console.log('fail to load script');
});

and code is eslinted.

Winterwinterbottom answered 11/4, 2018 at 16:44 Comment(3)
This is cool, but it also has the problem of not skipping an execution frame like jQuery seems to do (see comment on Mahn's post). For example, I am loading the Pollfish Javascript file (from their CDN) with this, which creates some HTML. That HTML is still not loaded when your console.log('script loaded') is run. It needs setTimeOut(func, 0).Construct
I ended up using this option, and it worked well. Though I did have to add a little extra work at the start to record what source was already requested, and keep track of their resulting promises in an array, that way any JS that tries to, by chance, load the same source script, will get the same promise the browser is already working on (or potentially already finished loading).Subsume
Warning: script.async = 'async'; or script.async = 1;Bipinnate
S
5

There are some good solutions here but many are outdated. There is a good one by @Mahn but as stated in a comment it is not exactly a replacement for $.getScript() as the callback does not receive data. I had already written my own function for a replacement for $.get() and landed here when I need it to work for a script. I was able to use @Mahn's solution and modify it a bit along with my current $.get() replacement and come up with something that works well and is simple to implement.

function pullScript(url, callback){
    pull(url, function loadReturn(data, status, xhr){
        //If call returned with a good status
        if(status == 200){
            var script = document.createElement('script');
            //Instead of setting .src set .innerHTML
            script.innerHTML = data;
            document.querySelector('head').appendChild(script);
        }
        if(typeof callback != 'undefined'){
            //If callback was given skip an execution frame and run callback passing relevant arguments
            setTimeout(function runCallback(){callback(data, status, xhr)}, 0);
        }
    });
}

function pull(url, callback, method = 'GET', async = true) {
    //Make sure we have a good method to run
    method = method.toUpperCase();
    if(!(method === 'GET'   ||   method === 'POST'   ||  method === 'HEAD')){
        throw new Error('method must either be GET, POST, or HEAD');
    }
    //Setup our request
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == XMLHttpRequest.DONE) {   // XMLHttpRequest.DONE == 4
            //Once the request has completed fire the callback with relevant arguments
            //you should handle in your callback if it was successful or not
            callback(xhr.responseText, xhr.status, xhr);
        }
    };
    //Open and send request
    xhr.open(method, url, async);
    xhr.send();
}

Now we have a replacement for $.get() and $.getScript() that work just as simply:

pullScript(file1, function(data, status, xhr){
    console.log(data);
    console.log(status);
    console.log(xhr);
});

pullScript(file2);

pull(file3, function loadReturn(data, status){
    if(status == 200){
        document.querySelector('#content').innerHTML = data;
    }
}
Sipe answered 8/6, 2019 at 19:49 Comment(1)
Note that as this uses AJAX (fetch/XMLHttpRequest) for the request you'll bound to the CORS policy in modern browsers developer.mozilla.org/en-US/docs/Web/HTTP/CORSPlateau
C
4

Mozilla Developer Network provides an example that works asynchronously and does not use 'onreadystatechange' (from @ShaneX's answer) that is not really present in a HTMLScriptTag:

function loadError(oError) {
  throw new URIError("The script " + oError.target.src + " didn't load correctly.");
}

function prefixScript(url, onloadFunction) {
  var newScript = document.createElement("script");
  newScript.onerror = loadError;
  if (onloadFunction) { newScript.onload = onloadFunction; }
  document.currentScript.parentNode.insertBefore(newScript, document.currentScript);
  newScript.src = url;
}

Sample usage:

prefixScript("myScript1.js");
prefixScript("myScript2.js", function () { alert("The script \"myScript2.js\" has been correctly loaded."); });

But @Agamemnus' comment should be considered: The script might not be fully loaded when onloadFunction is called. A timer could be used setTimeout(func, 0) to let the event loop finalize the added script to the document. The event loop finally calls the function behind the timer and the script should be ready to use at this point.

However, maybe one should consider returning a Promise instead of providing two functions for exception & success handling, that would be the ES6 way. This would also render the need for a timer unnecessary, because Promises are handled by the event loop - becuase by the time the Promise is handled, the script was already finalized by the event loop.

Implementing Mozilla's method including Promises, the final code looks like this:

function loadScript(url)
{
  return new Promise(function(resolve, reject)
  {
    let newScript = document.createElement("script");
    newScript.onerror = reject;
    newScript.onload = resolve;
    document.currentScript.parentNode.insertBefore(newScript, document.currentScript);
    newScript.src = url;
  });
}

loadScript("test.js").then(() => { FunctionFromExportedScript(); }).catch(() => { console.log("rejected!"); });
Chervil answered 1/4, 2019 at 9:24 Comment(0)
C
0

window.addEventListener('DOMContentLoaded',
       function() {
           var head = document.getElementsByTagName('HEAD')[0];
           var script = document.createElement('script');
           script.src = "/Content/index.js";
           head.appendChild(script);
       });
Cut answered 7/7, 2020 at 10:45 Comment(0)
R
0

Here's a version that preserves the accept and x-requested-with headers, like jquery getScript:

function pullScript(url, callback){
    pull(url, function loadReturn(data, status, xhr){
        if(status === 200){
        var script = document.createElement('script');
        script.innerHTML = data; // Instead of setting .src set .innerHTML
        document.querySelector('head').appendChild(script);
        }
        if (typeof callback != 'undefined'){
        // If callback was given skip an execution frame and run callback passing relevant arguments
        setTimeout(function runCallback(){callback(data, status, xhr)}, 0);
        }
    });
}

function pull(url, callback) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === XMLHttpRequest.DONE) {
        callback(xhr.responseText, xhr.status, xhr);
        }
    };
    xhr.open('GET', url, true);
    xhr.setRequestHeader('accept', '*/*;q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript');
    xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
    xhr.send();
}

pullScript(URL);
Rutherfurd answered 7/11, 2022 at 22:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.