document.createElement(“ script”)同步


80

是否可以.js同步调用文件,然后立即使用它?

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

这被简化了。在我的实现中,createElement东西在一个函数中。我考虑过要在函数中添加一些内容,以检查在返回控件之前是否实例化了某个变量。但是,仍然存在在我无法控制的其他站点包含js时该怎么办的问题。

有什么想法吗?

编辑:

我已经接受了最佳答案,因为它可以很好地说明正在发生的事情。但是,如果有人对如何改进它有任何建议,我欢迎他们。这是我想做的事的一个例子。

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

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

我只是想避免过多地了解内部知识,而只能说:“我希望使用此模块,现在我将使用其中的一些代码。”


我还没有弄清楚如何在不创建数组(用于计数)的情况下引用相同的值。否则,我认为这是不言自明的(当加载eval()所有内容时,将按给定的顺序存储每个文件,否则只需存储响应即可)。
Kang Rofingi 2015年

Answers:


132

您可以<script>使用“ onload”处理程序创建您的元素,当浏览器加载并评估脚本时将调用该元素。

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

您无法同步执行。

编辑-已经指出,从形式上讲,IE不会在<script>要加载/评估的标签上触发“加载”事件。因此,我想接下来要做的就是使用XMLHttpRequest来获取脚本,然后eval()自己获取。(或者,我想将文本填充到<script>您添加的标签中;的执行环境eval()受本地作用域的影响,因此它不一定会执行您想要的操作。)

编辑-从2013年初开始,我强烈建议您考虑使用像Requirejs这样更强大的脚本加载工具。有很多特殊情况需要担心。在非常简单的情况下,可以使用yepnope,它已内置在Modernizr中


3
不幸的是,它不是跨浏览器。
gblazex

69
真??加载脚本时,谁不触发“加载”事件? 等等-不要告诉我。
尖尖的

1
@Pointy我通过使用XMLHttpRequest然后解决了这个问题eval()。但是,调试它是一场噩梦b / c错误消息报告该行eval()出现,而不是实际错误
puk 2011年

3
但是requirejs怎么做到的呢?它们如何包括许多脚本并以正确的顺序触发它们?
mmm

4
当然,您正在寻找document.write()。不漂亮,但可以。
Jiri Vetyska 2014年

26

这不是很漂亮,但是可以工作:

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

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

要么

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

该脚本必须包含在单独的<script>标记中或之前window.onload()

这将不起作用:

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

创建节点的方法与Pointy一样,但只能在FF中完成。您无法保证该脚本何时可以在其他浏览器中就绪。

作为XML Purist,我真的很讨厌这一点。但这确实可以预期。您可以轻松地包装这些丑陋的document.write()s,因此您不必查看它们。您甚至可以进行测试并创建一个节点,然后追加节点,然后再依赖document.write()


您确定您的第一个代码段适用于所有浏览器吗?
Bogdan Gusiev 2013年

@BogdanGusiev我不确定100%。我在IE 8(当时的最新版本)Firefox和Chrome中进行了测试。很有可能这不适用于充当内容类型的XHTML文档类型application/xhtml+xml
乔什·约翰逊

1
不幸的是,脚本标记不能在JS文件中使用。
Clem13年

@Clem你可以做一个document.write("<SCR" + "IPT>" + "...")
约翰·威兹

对于<head>其中加载了其他几个依赖项(或私有文件)的脚本,这是一个不错的选择。
alecov '16

18

这已经很晚了,但是对于以后想要参考的人来说,您可以使用以下方法:

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

我前段时间写了一篇简短的博客文章,网址为http://crlog.info/2011/10/06/dynamically-requireinclude-a-javascript-file-into-a-page-and-be-notified-when-its -已加载/


这真的有效吗?看到我的问题:stackoverflow.com/questions/17978255/…–
mmm

1
这看起来很有趣。一个问题...为什么需要两次执行回调方法?(在onreadystatechange中使用的script.onload = callback和callback())
Clem

1
onreadysteatechange适用于IE,只会在IE上触发,因为onload不会为IE触发
Guilherme Ferreira 2015年

7

异步编程稍微复杂一些,因为发出请求的结果封装在一个函数中,而不是遵循请求语句。但是用户体验到的实时行为会大大改善,因为他们不会看到服务器速度缓慢或网络速度缓慢导致浏览器崩溃。同步编程是不礼貌的不应在人们使用的应用程序中使用。

道格拉斯·克罗克福德 YUI博客

好吧,请系紧你的座位,因为这将是一个颠簸的旅程。越来越多的人问有关通过javascript动态加载脚本的问题,这似乎是一个热门话题。

如此流行的主要原因是:

  • 客户端模块
  • 依赖管理更容易
  • 错误处理
  • 性能优势

关于模块化:很明显,管理客户端依赖项应该直接在客户端上进行。如果需要某个对象,模块或库,我们只是要求它并动态加载它。

错误处理:如果资源失败,我们仍然有机会仅阻止依赖于受影响脚本的部分,甚至可能会稍稍延迟一下。

性能已成为网站之间的竞争优势,现在已成为搜索排名的因素。动态脚本可以做的是模仿异步行为,而不是浏览器如何处理脚本的默认阻止方式。脚本会阻止其他资源,脚本会阻止HTML文档的进一步解析,脚本会阻止UI。现在有了动态脚本标签及其跨浏览器的替代方案,您可以执行真正的异步请求,并仅在可用时执行相关代码。您的脚本将与其他资源并行加载,并且呈现效果完美无缺。

有些人坚持使用同步脚本的原因是因为他们已经习惯了。他们认为这是默认方法,这是更简单的方法,甚至有些人甚至认为这是唯一方法。

但是,当需要就应用程序设计做出决定时,我们唯一需要关心的就是最终用户体验。在这一领域,异步是无法击败的。用户会立即得到答复(或说诺言),诺言总比没有好。黑屏会吓到人们。开发人员不应懒于提高感知性能

最后,关于肮脏的一面。为了使它能跨浏览器工作,您应该做什么:

  1. 学习异步思考
  2. 将代码组织为模块化
  3. 组织代码以很好地处理错误和极端情况
  4. 逐步增强
  5. 总是照顾适当数量的反馈

谢谢,加拉姆 我想我应该更清楚了。我确实希望这最终是异步的。我只想要一种对程序员来说合乎逻辑的访问方法。我想避免这样的事情:Import(“ package.mod1”,function(){//用mod1做事}); Import(“ package.mod2”,function(){//用mod2做东西}); 我看了看您的脚本和labjs,虽然不错,但对于我的需求来说似乎更复杂。我认为可能有一种更简单的方法,并希望避免引入额外的依赖关系。
乔什·约翰逊

1
你错过了我的帖子的重点。一切都与用户有关。这应该是您的首要任务。其他一切都是次要的。
gblazex

2
加拉姆,很好。用户体验非常重要。明确地说,我不愿意牺牲用户体验或质量,可维护的代码。我将研究闭包和Labjs,看看它们可以为我做些什么。但是暂时,我可能需要坚持使用<script>标签。不幸的是,我不是一个人做。我与一个中等规模的开发人员团队一起工作,因此可维护的代码是当务之急。如果每个人都无法弄清楚如何有效地使用lib,则用户exp会直接跳出窗口。回调很直观。因为您导入了包而没有回调。
乔什·约翰逊

再次,为了清楚起见,“同步”是用来理解我的观点的错误选择。我不希望浏览器在加载时冻结。
乔什·约翰逊

1
如果需要同步加载怎么办?如果您实际上需要阻止以保留用户体验。如果您使用的是基于JavaScript的A / B或MVT测试系统。您要如何异步加载内容并替换默认值,而又不会获得会破坏用户体验的闪烁效果?我愿意提出建议。我有500多位同事想知道解决方案。如果您没有,则不要带有“同步编程是不礼貌的,不应在人们使用的应用程序中使用”之类的表达。
2013年

6

上面的答案为我指明了正确的方向。这是我工作的通用版本:

  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
  }      

什么时候postLoadFunction()叫?
乔什·约翰逊

1
@JoshJohnsonscript.addEventListener('load', postLoadFunction);表示在脚本加载时调用postLoadFunction。
埃里克

4

我对此问题的现有答案(以及该问题在其他stackoverflow线程上的变体)存在以下问题:

  • 加载的代码均不可调试
  • 许多解决方案都要求回调函数知道加载何时完成而不是真正阻止,这意味着立即调用加载(即加载)代码会导致执行错误。

或者,更准确地说:

  • 所有加载的代码都不是可调试的(除了HTML脚本标记块之外,当且仅当解决方案向dom中添加了脚本元素时才可调试,并且永远都不能作为单独的可见脚本。) =>给定我必须加载多少个脚本(并进行调试),这是不可接受的。
  • 使用'onreadystatechange'或'onload'事件的解决方案无法阻止,这是一个大问题,因为代码最初是使用'require([filename,'dojo / domReady']);'同步加载动态脚本的。我正在剥离道场。

我的最终解决方案是在返回之前先加载脚本,并在调试器中正确访问所有脚本(至少适用于Chrome),如下所示:

警告:以下代码应仅在“开发”模式下使用。 (对于“发布”模式,我建议在不进行动态脚本加载的情况下进行预打包和压缩,或者至少不进行评估)。

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

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');
  })
  */
}


1

我习惯在我的网站上有多个彼此依赖的.js文件。为了加载它们并确保以正确的顺序评估依赖关系,我编写了一个函数来加载所有文件,然后,一旦所有文件被接收,就将它们加载eval()。主要缺点是,因为这不适用于CDN。对于此类库(例如jQuery),最好静态地包含它们。请注意,在HTML中动态插入脚本节点将不能保证脚本以正确的顺序进行评估,至少不能在Chrome中进行评估(这是编写此函数的主要原因)。

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

我还没有弄清楚如何在不创建数组(用于计数)的情况下引用相同的值。否则,我认为这是不言自明的(当加载eval()所有内容时,将按给定的顺序存储每个文件,否则只需存储响应即可)。

用法示例:

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

0

具有讽刺意味的是,我有您想要的东西,但是想要更接近您所拥有的东西。

我正在动态地和异步地加载东西,但是有这样的load回调(使用dojo和xmlhtpprequest)

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

有关详细说明,请参见此处

问题是代码行中的某个地方被规避了,如果您的代码有任何问题,该console.error(errorMessage);语句将指示该行在哪里eval(),而不是实际错误。这是一个很大的问题,我实际上正在尝试将其转换回<script>语句(请参阅此处


有趣的事实:我也已经回到<script>标签并使用约定(以及一些构建包)以一种有意义的方式打包我的js。
乔什·约翰逊

@JoshJohnson我不是那么幸运,我需要做一个广度的第一次加载,其中环内的脚本被异步加载而环之间的脚本被同步加载
puk 2011年

我很幸运,能够解决一些问题。我不羡慕你的位置。
乔什·约翰逊

0

这适用于支持async / awaitfetch的现代“常绿”浏览器。

此示例经过简化,没有错误处理,以显示工作的基本原理。

// 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'
] )
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.