document.createElement („skrypt”) synchronicznie

81

Czy można wywołać .jsplik synchronicznie, a następnie natychmiast go użyć?

<script type="text/javascript">
    var head = document.getElementsByTagName('head').item(0);
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('src', 'http://mysite/my.js');
    head.appendChild(script);

    myFunction(); // Fails because it hasn't loaded from my.js yet.

    window.onload = function() {
        // Works most of the time but not all of the time.
        // Especially if my.js injects another script that contains myFunction().
        myFunction();
    };
</script>

To jest uproszczone. W mojej implementacji element createElement jest w funkcji. Pomyślałem o dodaniu do funkcji czegoś, co mogłoby sprawdzić, czy pewna zmienna została utworzona przed zwróceniem kontroli. Ale nadal pojawia się problem, co zrobić, dołączając js z innej witryny, nad którą nie mam kontroli.

Myśli?

Edytować:

Przyjąłem na razie najlepszą odpowiedź, ponieważ daje dobre wyjaśnienie tego, co się dzieje. Ale jeśli ktoś ma jakieś sugestie, jak to poprawić, jestem otwarty na niego. Oto przykład tego, co chciałbym zrobić.

// Include() is a custom function to import js.
Include('my1.js');
Include('my2.js');

myFunc1('blarg');
myFunc2('bleet');

Chciałbym po prostu uniknąć zbyt dużej znajomości wewnętrznych elementów i móc po prostu powiedzieć: „Chcę użyć tego modułu, a teraz użyję trochę kodu z niego”.

Josh Johnson
źródło
Nie wymyśliłem, jak tworzyć odniesienia do tej samej wartości bez tworzenia tablicy (dla licznika). W przeciwnym razie myślę, że jest to oczywiste (kiedy wszystko jest załadowane, eval()każdy plik w podanej kolejności, w przeciwnym razie po prostu zapisz odpowiedź).
Kang Rofingi

Odpowiedzi:

134

Możesz stworzyć swój <script>element za pomocą procedury obsługi "onload", która zostanie wywołana po załadowaniu i ocenie skryptu przez przeglądarkę.

var script = document.createElement('script');
script.onload = function() {
  alert("Script loaded and ready");
};
script.src = "http://whatever.com/the/script.js";
document.getElementsByTagName('head')[0].appendChild(script);

Nie możesz tego zrobić synchronicznie.

edit - zwrócono uwagę, że zgodnie z formą IE nie wywołuje zdarzenia „load” na <script>ładowanych / ocenianych tagach. Tak więc przypuszczam, że następną rzeczą do zrobienia byłoby pobranie skryptu z XMLHttpRequest, a następnie eval()samodzielne pobranie. (Lub, jak przypuszczam, umieść tekst w <script>dodawanym tagu; na środowisko wykonawcze eval()ma wpływ lokalny zasięg, więc niekoniecznie robi to, co chcesz).

edycja - Od początku 2013 r. zdecydowanie radzę przyjrzeć się bardziej niezawodnemu narzędziu do ładowania skryptów, takim jak Requirejs . Istnieje wiele specjalnych przypadków, o które należy się martwić. W naprawdę prostych sytuacjach istnieje Yepnope , który jest teraz wbudowany w Modernizr .

Spiczasty
źródło
3
niestety nie jest to cross-browser.
gblazex
69
Naprawdę?? Kto nie uruchamia zdarzenia „load” po załadowaniu skryptu? Czekaj - nie mów mi.
Pointy,
1
@Pointy Rozwiązałem ten problem używając XMLHttpRequest, a następnie eval(). Jednak debugowanie to koszmar, b / c komunikat o błędzie informuje, że eval()pojawia się linia , a nie rzeczywisty błąd
puk
3
Ale w jaki sposób requirejs to robi? W jaki sposób włączają wiele skryptów i uruchamiają je we właściwej kolejności?
mmm
4
Oczywiście document.write () jest tym, czego szukasz. Niezbyt ładne, ale działa.
Jiri Vetyska
26

To nie jest ładne, ale działa:

<script type="text/javascript">
  document.write('<script type="text/javascript" src="other.js"></script>');
</script>

<script type="text/javascript">
  functionFromOther();
</script>

Lub

<script type="text/javascript">
  document.write('<script type="text/javascript" src="other.js"></script>');
  window.onload = function() {
    functionFromOther();
  };
</script>

Skrypt należy umieścić w osobnym <script>tagu lub wcześniej window.onload().

To nie zadziała:

<script type="text/javascript">
  document.write('<script type="text/javascript" src="other.js"></script>');
  functionFromOther(); // Error
</script>

To samo można zrobić z utworzeniem węzła, tak jak zrobił to Pointy, ale tylko w FF. Nie masz żadnej gwarancji, że skrypt będzie gotowy w innych przeglądarkach.

Będąc purystą XML, naprawdę tego nienawidzę. Ale działa przewidywalnie. Możesz łatwo owinąć te brzydkie, document.write()więc nie musisz na nie patrzeć. Możesz nawet wykonać testy i utworzyć węzeł, dołączyć go, a następnie ponownie włączyć document.write().

Josh Johnson
źródło
Czy na pewno Twój pierwszy fragment kodu działa we wszystkich przeglądarkach?
Bogdan Gusiev
@BogdanGusiev Nie jestem pewien w 100%. Testowałem w IE 8 (ówczesne aktualne wersje) Firefox i Chrome. Jest szansa, że ​​to nie zadziała z typami dokumentów XHTML, które są obsługiwane jako typ zawartości application/xhtml+xml.
Josh Johnson
1
Niestety znaczników skryptów nie można używać w plikach JS.
Clem
@Clem Możesz zrobić document.write("<SCR" + "IPT>" + "...").
John Weisz
Jest to dobra alternatywa dla skryptów, w <head>których ładuje się kilka innych zależności (lub plików prywatnych).
alecov
18

Jest to dość późno, ale w przyszłości dla każdego, kto chciałby to zrobić, możesz użyć następujących opcji:

function require(file,callback){
    var head=document.getElementsByTagName("head")[0];
    var script=document.createElement('script');
    script.src=file;
    script.type='text/javascript';
    //real browsers
    script.onload=callback;
    //Internet explorer
    script.onreadystatechange = function() {
        if (this.readyState == 'complete') {
            callback();
        }
    }
    head.appendChild(script);
}

Zrobiłem na ten temat krótki post na blogu jakiś czas temu http://crlog.info/2011/10/06/dynamically-requireinclude-a-javascript-file-into-a-page-and-be-notified-when-its -załadowany/

zcourts
źródło
czy to naprawdę działa? zobacz moje pytanie: stackoverflow.com/questions/17978255/ ...
mmm
1
To wygląda interesująco. Jedno pytanie ... dlaczego konieczne jest dwukrotne wykonanie metody callback? (script.onload = callback i callback () używane w onreadystatechange)
Clem
1
onreadysteatechange jest dla IE i będzie uruchamiany tylko w IE, ponieważ onload nie będzie odpalał dla IE
Guilherme Ferreira
7

Programowanie asynchroniczne jest nieco bardziej skomplikowane, ponieważ konsekwencja wykonania żądania jest hermetyzowana w funkcji zamiast podążać za instrukcją żądania. Jednak zachowanie w czasie rzeczywistym, którego doświadcza użytkownik, może być znacznie lepsze, ponieważ nie będzie widział spowolnionego serwera lub powolnej sieci, co spowoduje, że przeglądarka będzie działać tak, jakby się zawiesiła. Programowanie synchroniczne jest lekceważące i nie powinno być stosowane w aplikacjach, z których korzystają ludzie.

Douglas Crockford ( blog YUI )

W porządku, zapnij siedzenia, bo to będzie wyboista jazda. Coraz więcej osób pyta o dynamiczne ładowanie skryptów przez javascript, wydaje się, że to gorący temat.

Główne powody, dla których stało się to tak popularne, to:

  • modułowość po stronie klienta
  • łatwiejsze zarządzanie zależnościami
  • obsługa błędów
  • zalety wydajności

O modułowości : oczywiste jest, że zarządzanie zależnościami po stronie klienta powinno być obsługiwane bezpośrednio po stronie klienta. Jeśli potrzebny jest jakiś obiekt, moduł lub biblioteka, po prostu o niego prosimy i ładujemy dynamicznie.

Obsługa błędów : jeśli zasób się nie powiedzie, nadal mamy szansę zablokować tylko te części, które zależą od skryptu, którego dotyczy problem, lub może nawet spróbować ponownie z pewnym opóźnieniem.

Wydajność stała się przewagą konkurencyjną między stronami internetowymi, a obecnie jest czynnikiem rankingowym wyszukiwania. To, co mogą zrobić skrypty dynamiczne, to naśladować zachowanie asynchroniczne w przeciwieństwie do domyślnego blokowania sposobu obsługi skryptów przez przeglądarki. Skrypty blokują inne zasoby, skrypty blokują dalszą analizę dokumentu HTML, blok skryptów interfejs użytkownika. Teraz, dzięki dynamicznym znacznikom skryptów i ich alternatywom dla różnych przeglądarek, możesz wykonywać rzeczywiste żądania asynchroniczne i wykonywać zależny kod tylko wtedy, gdy są dostępne. Twoje skrypty będą ładowane równolegle nawet z innymi zasobami, a renderowanie będzie bezbłędne.

Powodem, dla którego niektórzy ludzie trzymają się skryptów synchronicznych, jest to, że są do tego przyzwyczajeni. Myślą, że jest to sposób domyślny, łatwiejszy, a niektórzy mogą nawet pomyśleć, że to jedyny sposób.

Ale jedyną rzeczą, o którą powinniśmy się troszczyć, kiedy trzeba podjąć decyzję dotyczącą projektu aplikacji, jest doświadczenie użytkownika końcowego . A w tej dziedzinie asynchroniczności nie da się pokonać. Użytkownik otrzymuje natychmiastowe odpowiedzi (lub obiecuje), a obietnica jest zawsze lepsza niż nic. Pusty ekran przeraża ludzi. Programiści nie powinni być leniwi, aby poprawić postrzeganą wydajność .

I na koniec kilka słów o brudnej stronie. Co należy zrobić, aby działał w różnych przeglądarkach:

  1. nauczyć się myśleć asynchronicznie
  2. zorganizuj swój kod tak, aby był modułowy
  3. organizuj kod, aby dobrze obsługiwać błędy i przypadki skrajne
  4. zwiększać się stopniowo
  5. zawsze dbaj o odpowiednią ilość informacji zwrotnych
gblazex
źródło
Dzięki, Galam. Myślę, że powinienem był wyrazić się bardziej jasno. Spodziewałem się, że w końcu będzie to asynchroniczne. Po prostu chcę uzyskać do niego dostęp, który miałby logiczny sens dla programisty. Chciałem uniknąć takich rzeczy jak: Import ("package.mod1", function () {// rób rzeczy z mod1}); Import ("package.mod2", function () {// rób rzeczy z mod2}); Przyjrzałem się twojemu skryptowi i labjsom i chociaż są ładne, wydają się być bardziej złożone dla moich potrzeb. Pomyślałem, że może być prostszy sposób i chciałem uniknąć wprowadzania dodatkowych zależności.
Josh Johnson,
1
Przegapiłeś sens mojego postu. Chodzi o użytkowników. To powinno być twoim priorytetem. Wszystko inne jest drugorzędne.
gblazex
2
Galam, bardzo dobra uwaga. Doświadczenie użytkownika jest bardzo ważne. Żeby było jasne, nie jestem skłonny poświęcić doświadczenia użytkownika LUB jakości kodu, który można konserwować. Zajrzę się zamknięciem i labjsami, aby zobaczyć, co mogą dla mnie zrobić. Ale na razie być może będę musiał trzymać się tagów <script>. Niestety nie pracuję nad tym sam. Pracuję ze średniej wielkości zespołem programistów, więc utrzymanie kodu ma wysoki priorytet. Jeśli każdy nie może dowiedzieć się, jak efektywnie korzystać z biblioteki, exp użytkownika wyskakuje przez okno. Wywołania zwrotne są intuicyjne. Oddzwonienie, ponieważ zaimportowałeś pakiet nie.
Josh Johnson,
Ponownie, dla jasności, „synchroniczny” był złym doborem słów użytych do przekazania mojej opinii. Nie chcę, aby przeglądarka zawiesiła się podczas ładowania.
Josh Johnson,
1
A jeśli potrzebujesz synchronicznego ładowania? Jeśli faktycznie musisz zablokować, aby zachować wrażenia użytkownika. Jeśli używasz systemu testów A / B lub MVT opartego na JavaScript. Jak chcesz asynchronicznie ładować zawartość i zastępować domyślną bez efektu migotania, który psuje doświadczenie użytkownika? Jestem otwarty na sugestie. Mam ponad 500 kolegów, którzy chcieliby poznać rozwiązanie tego problemu. Jeśli go nie masz, nie używaj wyrażeń takich jak „Programowanie synchroniczne jest lekceważące i nie powinno być stosowane w aplikacjach, z których korzystają ludzie”.
transilvlad
6

Odpowiedzi powyżej wskazały mi właściwy kierunek. Oto ogólna wersja tego, co mam działające:

  var script = document.createElement('script');
  script.src = 'http://' + location.hostname + '/module';
  script.addEventListener('load', postLoadFunction);
  document.head.appendChild(script);

  function postLoadFunction() {
     // add module dependent code here
  }      
James
źródło
Kiedy jest postLoadFunction()wezwany?
Josh Johnson
1
@JoshJohnson script.addEventListener('load', postLoadFunction);oznacza, że ​​funkcja postLoadFunction jest wywoływana podczas ładowania skryptu.
Eric
4

Miałem następujący problem (y) z istniejącymi odpowiedziami na to pytanie (i odmianami tego pytania w innych wątkach stackoverflow):

  • Żaden z załadowanego kodu nie był debugowalny
  • Wiele rozwiązań wymagało wywołań zwrotnych, aby wiedzieć, kiedy ładowanie zostało zakończone, zamiast prawdziwego blokowania, co oznacza, że ​​otrzymywałbym błędy wykonania od natychmiastowego wywołania załadowanego (tj. Ładowania) kodu.

Albo nieco dokładniej:

  • Żaden z załadowanego kodu nie był debugowalny (z wyjątkiem bloku tagów skryptu HTML, wtedy i tylko wtedy, gdy rozwiązanie dodało elementy skryptu do domeny i nigdy, przenigdy jako pojedyncze skrypty do przeglądania). => Biorąc pod uwagę, ile skryptów muszę załadować ( i debugowania), było to niedopuszczalne.
  • Rozwiązania wykorzystujące zdarzenia „onreadystatechange” lub „onload” nie blokowały się, co było dużym problemem, ponieważ kod pierwotnie ładował skrypty dynamiczne synchronicznie przy użyciu polecenia „require ([nazwa pliku,„ dojo / domReady ”]);” a ja rozbierałem dojo.

Moje ostateczne rozwiązanie, które ładuje skrypt przed powrotem, ORAZ ma wszystkie skrypty odpowiednio dostępne w debugerze (przynajmniej dla Chrome) jest następujące:

OSTRZEŻENIE: Poniższy kod PRAWDOPODOBNIE powinien być używany tylko w trybie programowania. (W trybie „wydania” zalecam wstępne pakowanie i minifikację BEZ dynamicznego ładowania skryptów lub przynajmniej bez eval).

//Code User TODO: you must create and set your own 'noEval' variable

require = function require(inFileName)
{
    var aRequest
        ,aScript
        ,aScriptSource
        ;

    //setup the full relative filename
    inFileName = 
        window.location.protocol + '//'
        + window.location.host + '/'
        + inFileName;

    //synchronously get the code
    aRequest = new XMLHttpRequest();
    aRequest.open('GET', inFileName, false);
    aRequest.send();

    //set the returned script text while adding special comment to auto include in debugger source listing:
    aScriptSource = aRequest.responseText + '\n////# sourceURL=' + inFileName + '\n';

    if(noEval)//<== **TODO: Provide + set condition variable yourself!!!!**
    {
        //create a dom element to hold the code
        aScript = document.createElement('script');
        aScript.type = 'text/javascript';

        //set the script tag text, including the debugger id at the end!!
        aScript.text = aScriptSource;

        //append the code to the dom
        document.getElementsByTagName('body')[0].appendChild(aScript);
    }
    else
    {
        eval(aScriptSource);
    }
};
jeremykentbgross
źródło
4
function include(file){
return new Promise(function(resolve, reject){
        var script = document.createElement('script');
        script.src = file;
        script.type ='text/javascript';
        script.defer = true;
        document.getElementsByTagName('head').item(0).appendChild(script);

        script.onload = function(){
        resolve()
        }
        script.onerror = function(){
          reject()
        }
      })

 /*I HAVE MODIFIED THIS TO  BE PROMISE-BASED 
   HOW TO USE THIS FUNCTION 

  include('js/somefile.js').then(function(){
  console.log('loaded');
  },function(){
  console.log('not loaded');
  })
  */
}
Daggie Blanqx - Douglas Mwangi
źródło
1

Jestem przyzwyczajony do posiadania wielu plików .js w mojej witrynie internetowej, które są od siebie zależne. Aby je załadować i upewnić się, że zależności są oceniane we właściwej kolejności, napisałem funkcję, która ładuje wszystkie pliki, a następnie, gdy wszystkie zostaną odebrane, eval()je. Główną wadą jest to, że ponieważ nie działa to z CDN. W przypadku takich bibliotek (np. JQuery) lepiej jest uwzględniać je statycznie. Zwróć uwagę, że dynamiczne wstawianie węzłów skryptów w HTML nie gwarantuje, że skrypty są oceniane we właściwej kolejności, przynajmniej nie w Chrome (to był główny powód napisania tej funkcji).

function xhrs(reqs) {
  var requests = [] , count = [] , callback ;

  callback = function (r,c,i) {
    return function () {
      if  ( this.readyState == 4 ) {
        if (this.status != 200 ) {
          r[i]['resp']="" ;
        } 
        else {
          r[i]['resp']= this.responseText ;
        }
        c[0] = c[0] - 1 ;
        if ( c[0] == 0 ) {
          for ( var j = 0 ; j < r.length ; j++ ) {
            eval(r[j]['resp']) ;
          }
        }
      }
    }
  } ;
  if ( Object.prototype.toString.call( reqs ) === '[object Array]' ) {
    requests.length = reqs.length ;
  }
  else {
    requests.length = 1 ;
    reqs = [].concat(reqs);
  }
  count[0] = requests.length ;
  for ( var i = 0 ; i < requests.length ; i++ ) {
    requests[i] = {} ;
    requests[i]['xhr'] = new XMLHttpRequest () ;
    requests[i]['xhr'].open('GET', reqs[i]) ;
    requests[i]['xhr'].onreadystatechange = callback(requests,count,i) ;
    requests[i]['xhr'].send(null);
  }
}

Nie wymyśliłem, jak tworzyć odwołania do tej samej wartości bez tworzenia tablicy (dla licznika). W przeciwnym razie myślę, że jest to oczywiste (kiedy wszystko jest załadowane, eval()każdy plik w podanej kolejności, w przeciwnym razie po prostu zapisz odpowiedź).

Przykład użycia:

xhrs( [
       root + '/global.js' ,
       window.location.href + 'config.js' ,
       root + '/js/lib/details.polyfill.min.js',
       root + '/js/scripts/address.js' ,
       root + '/js/scripts/tableofcontents.js' 
]) ;
user1251840
źródło
0

Jak na ironię, mam to, czego chcesz, ale chcesz czegoś bliższego temu, co miałeś.

loadŁaduję rzeczy dynamicznie i asynchronicznie, ale z takim wywołaniem zwrotnym (używając dojo i xmlhtpprequest)

  dojo.xhrGet({
    url: 'getCode.php',
    handleAs: "javascript",
    content : {
    module : 'my.js'
  },
  load: function() {
    myFunc1('blarg');
  },
  error: function(errorMessage) {
    console.error(errorMessage);
  }
});

Bardziej szczegółowe wyjaśnienie można znaleźć tutaj

Problem polega na tym, że gdzieś wzdłuż linii kod jest oceniany i jeśli coś jest nie tak z twoim kodem, console.error(errorMessage);instrukcja wskaże wiersz, w którym eval()jest, a nie rzeczywisty błąd. To jest TAKI duży problem, do którego faktycznie próbuję wrócić<script> instrukcje (patrz tutaj .

rzygać
źródło
Ciekawostka: także wróciłem do <script>tagów i używając konwencji (wraz z niektórymi pakietami kompilacji), aby po prostu spakować mój js w sposób, który ma sens.
Josh Johnson
@JoshJohnson Nie mam tyle szczęścia, b / c Muszę zrobić wszerz pierwsze ładowanie pakietów ze skryptami w pierścieniach ładowanymi asynchronicznie, a skrypty między pierścieniami ładowane synchronicznie
puk
Miałem szczęście i udało mi się coś wymyślić. Nie zazdroszczę twojej pozycji.
Josh Johnson,
0

Działa to w przypadku nowoczesnych „wiecznie zielonych” przeglądarek, które obsługują async / await i fetch .

Ten przykład jest uproszczony, bez obsługi błędów, aby pokazać podstawowe zasady w działaniu.

// This is a modern JS dependency fetcher - a "webpack" for the browser
const addDependentScripts = async function( scriptsToAdd ) {

  // Create an empty script element
  const s=document.createElement('script')

  // Fetch each script in turn, waiting until the source has arrived
  // before continuing to fetch the next.
  for ( var i = 0; i < scriptsToAdd.length; i++ ) {
    let r = await fetch( scriptsToAdd[i] )

    // Here we append the incoming javascript text to our script element.
    s.text += await r.text()
  }

  // Finally, add our new script element to the page. It's
  // during this operation that the new bundle of JS code 'goes live'.
  document.querySelector('body').appendChild(s)
}

// call our browser "webpack" bundler
addDependentScripts( [
  'https://code.jquery.com/jquery-3.5.1.slim.min.js',
  'https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js'
] )
Dave Bevan
źródło
nie możemy powiedzieć, że webpack... 1. dla każdego skryptu wysyła a new HTTP request, 2. To również nie sprawdzi zależności między nimi, 3. Nie wszystkie przeglądarki obsługują async/awaiti 4. Wydajność jest nudna, a potem normalna. Byłoby dobrze,head
gdybyśmy dołączyli