document.createElement ("script") sincrónicamente

81

¿Es posible llamar a un .jsarchivo sincrónicamente y luego usarlo inmediatamente después?

<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>

Esto está simplificado. En mi implementación, el elemento createElement está en una función. Pensé en agregar algo a la función que pudiera verificar si una determinada variable fue instanciada antes de devolver el control. Pero aún queda el problema de qué hacer al incluir js de otro sitio sobre el que no tengo control.

Pensamientos

Editar:

He aceptado la mejor respuesta por ahora porque da una buena explicación de lo que está sucediendo. Pero si alguien tiene alguna sugerencia sobre cómo mejorar esto, estoy abierto a ellos. He aquí un ejemplo de lo que me gustaría hacer.

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

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

Solo quiero evitar tener que conocer demasiado los aspectos internos y poder decir: "Deseo usar este módulo, y ahora usaré algún código de él".

Josh Johnson
fuente
No he descubierto cómo hacer referencias al mismo valor sin crear una matriz (para contar). De lo contrario, creo que se explica por sí mismo (cuando todo está cargado, eval()cada archivo en el orden dado, de lo contrario, simplemente almacene la respuesta).
Kang Rofingi

Respuestas:

134

Puede crear su <script>elemento con un controlador "onload", que será llamado cuando el navegador haya cargado y evaluado el script.

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);

No puedes hacerlo sincrónicamente.

editar - se ha señalado que, fiel a la forma, IE no dispara un evento de "carga" en las <script>etiquetas que se cargan / evalúan. Por lo tanto, supongo que lo siguiente que debe hacer sería buscar el script con un XMLHttpRequest y luego eval()usted mismo. (O, supongo, introduzca el texto en una <script>etiqueta que agregue; el entorno de ejecución de eval()se ve afectado por el alcance local, por lo que no necesariamente hará lo que usted desea que haga).

editar : a principios de 2013 , recomiendo encarecidamente buscar una herramienta de carga de scripts más sólida como Requirejs . Hay muchos casos especiales de los que preocuparse. Para situaciones realmente simples, está yepnope , que ahora está integrado en Modernizr .

Puntiagudo
fuente
3
lamentablemente no es para varios navegadores.
gblazex
69
¿¿De Verdad?? ¿Quién no activa un evento de "carga" cuando se carga un script? Espera , no me lo digas.
puntiagudo
1
@Pointy Resolví este problema usando XMLHttpRequest y luego eval(). Sin embargo, depurarlo es una pesadilla porque el mensaje de error informa que eval()aparece la línea , no el error real
puk
3
¿Pero cómo requirejs hacer esto entonces? ¿Cómo incluyen muchos guiones y los ejecutan en el orden correcto?
mmm
4
Por supuesto, document.write () es lo que está buscando. No es bonita, pero funciona.
Jiri Vetyska
26

Esto no es bonito, pero funciona:

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

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

O

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

La secuencia de comandos debe incluirse en una <script>etiqueta separada o antes window.onload().

Esto no funcionará:

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

Se puede hacer lo mismo con la creación de un nodo, como hizo Pointy, pero solo en FF. No tiene ninguna garantía de cuándo estará listo el script en otros navegadores.

Siendo un purista de XML realmente odio esto. Pero funciona de manera predecible. Podrías envolver fácilmente esos feos document.write()para no tener que mirarlos. Incluso podría hacer pruebas y crear un nodo y agregarlo y luego recurrir a él document.write().

Josh Johnson
fuente
¿Está seguro de que su primer fragmento de código funciona en todos los navegadores?
Bogdan Gusiev
@BogdanGusiev No estoy 100% seguro. Probé en IE 8, (las versiones actuales de) Firefox y Chrome. Es probable que esto no funcione con los tipos de documento XHTML que se sirven como tipo de contenido application/xhtml+xml.
Josh Johnson
1
Desafortunadamente, las etiquetas de script no se pueden usar en archivos JS.
Clem
@Clem Podrías hacer un document.write("<SCR" + "IPT>" + "...").
John Weisz
Esta es una buena alternativa para los scripts dentro de los <head>cuales cargan varias otras dependencias (o archivos privados).
alecov
18

Esto es muy tarde, pero para referencia futura a cualquier persona que quiera hacer esto, puede usar lo siguiente:

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);
}

Hice una breve publicación en el blog hace algún tiempo http://crlog.info/2011/10/06/dynamically-requireinclude-a-javascript-file-into-a-page-and-be-notified-when-its -cargado/

zcourts
fuente
esto realmente funciona? vea mi pregunta: stackoverflow.com/questions/17978255/…
mmm
1
Esto parece interesante. Una pregunta ... ¿por qué es necesario ejecutar el método de devolución de llamada dos veces? (script.onload = callback y callback () usados ​​en onreadystatechange)
Clem
1
onreadysteatechange es para IE y solo se activará en IE, ya que la carga no se activará para IE
Guilherme Ferreira
7

La programación asincrónica es un poco más complicada porque la consecuencia de realizar una solicitud se encapsula en una función en lugar de seguir la declaración de solicitud. Pero el comportamiento en tiempo real que experimenta el usuario puede ser significativamente mejor porque no verá un servidor lento o una red inactiva que haga que el navegador actúe como si se hubiera bloqueado. La programación sincrónica es una falta de respeto y no debe emplearse en aplicaciones que son utilizadas por personas.

Douglas Crockford ( Blog de YUI )

Muy bien, abroche sus asientos, porque va a ser un viaje lleno de baches. Cada vez más personas preguntan sobre la carga de scripts dinámicamente a través de javascript, parece ser un tema candente.

Las principales razones por las que esto se hizo tan popular son:

  • modularidad del lado del cliente
  • gestión de la dependencia más sencilla
  • manejo de errores
  • ventajas de rendimiento

Acerca de la modularidad : es obvio que la gestión de las dependencias del lado del cliente debe manejarse directamente en el lado del cliente. Si se necesita cierto objeto, módulo o biblioteca, simplemente lo solicitamos y lo cargamos dinámicamente.

Manejo de errores : si un recurso falla, todavía tenemos la oportunidad de bloquear solo las partes que dependen del script afectado, o incluso intentarlo de nuevo con cierta demora.

El rendimiento se ha convertido en una ventaja competitiva entre los sitios web, ahora es un factor de clasificación de búsqueda. Lo que pueden hacer los scripts dinámicos es imitar el comportamiento asincrónico en contraposición a la forma de bloqueo predeterminada de cómo los navegadores manejan los scripts. Los scripts bloquean otros recursos, los scripts bloquean el análisis posterior del documento HTML, los scripts bloquean la interfaz de usuario. Ahora, con las etiquetas de secuencia de comandos dinámicas y sus alternativas entre navegadores, puede realizar solicitudes asincrónicas reales y ejecutar código dependiente solo cuando estén disponibles. Sus scripts se cargarán en paralelo incluso con otros recursos y el renderizado será impecable.

La razón por la que algunas personas se apegan a las secuencias de comandos sincrónicas es porque están acostumbradas. Creen que es la forma predeterminada, es la forma más fácil y algunos incluso pueden pensar que es la única forma.

Pero lo único que debería preocuparnos cuando se deba decidir sobre el diseño de una aplicación es la experiencia del usuario final . Y en esta área lo asincrónico no tiene rival. El usuario obtiene respuestas inmediatas (o dice promesas), y una promesa siempre es mejor que nada. Una pantalla en blanco asusta a la gente. Los desarrolladores no deberían ser perezosos para mejorar el rendimiento percibido .

Y finalmente algunas palabras sobre el lado sucio. Qué debe hacer para que funcione en todos los navegadores:

  1. aprender a pensar asincrónicamente
  2. organiza tu código para que sea modular
  3. Organice su código para manejar bien los errores y casos extremos.
  4. mejorar progresivamente
  5. siempre cuida la cantidad correcta de comentarios
gblazex
fuente
Gracias, galam. Supongo que debería haber sido más claro. Esperaba que esto fuera asincrónico al final. Solo quiero una forma de acceder a él que tenga sentido lógico para el programador. Quería evitar cosas como: Import ("package.mod1", function () {// hacer cosas con mod1}); Import ("paquete.mod2", function () {// hacer cosas con mod2}); Eché un vistazo a su script y labjs y, aunque es agradable, parece ser más complejo para mis necesidades. Pensé que podría haber una forma más sencilla y quería evitar incorporar dependencias adicionales.
Josh Johnson
1
Perdiste el punto de mi publicación. Se trata de los usuarios. Esta debería ser tu primera prioridad. Todo lo demás es secundario.
gblazex
2
Galam, muy buen punto. La experiencia del usuario es muy importante. Para ser claro, no estoy dispuesto a sacrificar la experiencia del usuario O la calidad del código mantenible. Voy a investigar el cierre y los labjs para ver qué pueden hacer por mí. Pero, por el momento, es posible que deba seguir con las etiquetas <script>. Desafortunadamente, no estoy trabajando en esto solo. Trabajo con un equipo de desarrolladores de tamaño medio, por lo que el código de mantenimiento es una prioridad alta. Si todos no pueden averiguar cómo usar la biblioteca de manera eficiente, entonces el usuario exp sale directamente por la ventana. Las devoluciones de llamada son intuitivas. Una devolución de llamada porque importó un paquete no lo es.
Josh Johnson
Una vez más, en aras de la claridad, "sincrónico" fue una mala elección de palabras utilizadas para expresar mi punto de vista. No quiero que el navegador se congele mientras se cargan las cosas.
Josh Johnson
1
¿Qué sucede si necesita una carga síncrona? Si realmente necesita bloquear para preservar la experiencia del usuario. Si está utilizando un sistema de prueba A / B o MVT basado en JavaScript. ¿Cómo desea cargar de forma asincrónica el contenido y reemplazar el predeterminado sin obtener un efecto de parpadeo que arruine la experiencia del usuario? Estoy abierto a sugerencias. Tengo más de 500 colegas a los que les gustaría conocer una solución a esto. Si no tiene uno, no incluya expresiones como "La programación síncrona es una falta de respeto y no debe emplearse en aplicaciones que son utilizadas por personas".
transilvlad
6

Las respuestas anteriores me señalaron en la dirección correcta. Aquí hay una versión genérica de lo que hice funcionar:

  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
fuente
¿Cuándo se postLoadFunction()llama?
Josh Johnson
1
@JoshJohnson script.addEventListener('load', postLoadFunction);significa que se llama a postLoadFunction al cargar el script.
Eric
4

Tuve los siguientes problemas con las respuestas existentes a esta pregunta (y variaciones de esta pregunta en otros subprocesos de stackoverflow):

  • Ninguno de los códigos cargados fue depurable
  • Muchas de las soluciones requerían devoluciones de llamada para saber cuándo finalizó la carga en lugar de bloquear realmente, lo que significa que obtendría errores de ejecución al llamar inmediatamente al código cargado (es decir, cargar).

O, un poco más exactamente:

  • Ninguno de los códigos cargados fue depurable (excepto desde el bloque de etiquetas de secuencia de comandos HTML, si y solo si la solución agregó un elemento de secuencia de comandos al dom, y nunca jamás como secuencias de comandos visibles individuales). => Dada la cantidad de secuencias de comandos que tengo que cargar ( y depurar), esto era inaceptable.
  • Las soluciones que usaban eventos 'onreadystatechange' u 'onload' no se podían bloquear, lo cual era un gran problema ya que el código originalmente cargaba scripts dinámicos sincrónicamente usando 'require ([filename,' dojo / domReady ']);' y me estaba quitando el dojo.

Mi solución final, que carga el script antes de regresar, Y tiene todos los scripts correctamente accesibles en el depurador (al menos para Chrome) es la siguiente:

ADVERTENCIA: El siguiente código PROBABLEMENTE debería usarse solo en modo 'desarrollo'. (Para el modo 'lanzamiento', recomiendo el preempaquetado y la minificación SIN carga dinámica de scripts, o al menos sin evaluación).

//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
fuente
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
fuente
1

Estoy acostumbrado a tener varios archivos .js en mi sitio web que dependen unos de otros. Para cargarlos y asegurarme de que las dependencias se evalúen en el orden correcto, he escrito una función que carga todos los archivos y luego, una vez que se reciben todos, eval()ellos. El principal inconveniente es que, dado que esto no funciona con CDN. Para tales bibliotecas (por ejemplo, jQuery) es mejor incluirlas estáticamente. Tenga en cuenta que insertar nodos de secuencia de comandos en el HTML de forma dinámica no garantizará que las secuencias de comandos se evalúen en el orden correcto, al menos no en Chrome (esta fue la razón principal para escribir esta función).

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);
  }
}

No he descubierto cómo hacer referencias al mismo valor sin crear una matriz (para contar). De lo contrario, creo que se explica por sí mismo (cuando todo está cargado, eval()cada archivo en el orden indicado, de lo contrario, simplemente almacene la respuesta).

Ejemplo de uso:

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' 
]) ;
usuario1251840
fuente
0

Irónicamente, tengo lo que quieres, pero quiero algo más cercano a lo que tenías.

Estoy cargando cosas de forma dinámica y asincrónica, pero con una loaddevolución de llamada como esa (usando dojo y xmlhtpprequest)

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

Para obtener una explicación más detallada, consulte aquí.

El problema es que en algún lugar de la línea se evalúa el código, y si hay algún problema con el código, la console.error(errorMessage);declaración indicará la línea donde eval()está, no el error real. Este es un problema TAN grande que en realidad estoy tratando de convertir nuevamente en <script>declaraciones (ver aquí .

puk
fuente
Dato curioso: yo también he vuelto a las <script>etiquetas y he usado la convención (junto con algunos paquetes de compilación) para empaquetar mis js de una manera que tenga sentido.
Josh Johnson
@JoshJohnson No tengo tanta suerte porque necesito hacer una primera carga amplia de paquetes con scripts dentro de anillos que se cargan de forma asincrónica y scripts entre anillos que se cargan sincrónicamente
puk
Tuve suerte y pude resolver algo. No envidio tu posición.
Josh Johnson
0

Esto funciona para los navegadores modernos 'perennes' que admiten async / await y fetch .

Este ejemplo está simplificado, sin manejo de errores, para mostrar los principios básicos en funcionamiento.

// 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
fuente
No podemos decirlo como webpack... 1. para cada script, está enviando un new HTTP request, 2. Esto tampoco comprobará las dependencias entre ellos, 3. No todos los navegadores son compatibles async/awaity 4. En cuanto al rendimiento, es tedioso y luego normal. Sería bueno agregar esto enhead
santosh