【问题标题】:document.createElement("script") synchronouslydocument.createElement("script") 同步
【发布时间】:2011-03-15 23:19:26
【问题描述】:

是否可以同步调用.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() 每个文件都按给定的顺序排列,否则只存储响应)。

标签: javascript dom synchronous


【解决方案1】:

您可以使用“onload”处理程序创建您的&lt;script&gt; 元素,该处理程序将在浏览器加载和评估脚本时调用。

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

你不能同步进行。

edit — 有人指出,按照形式,IE 不会在 &lt;script&gt; 正在加载/评估的标签上触发“加载”事件。因此,我想接下来要做的是使用 XMLHttpRequest 获取脚本,然后自己获取eval()。 (或者,我想,将文本填充到您添加的 &lt;script&gt; 标记中;eval() 的执行环境受本地范围的影响,因此它不一定会按照您的意愿执行。)

编辑从 2013 年初开始,我强烈建议您使用更强大的脚本加载工具,例如 Requirejs。有很多特殊情况需要担心。对于非常简单的情况,有yepnope,现在已内置在Modernizr 中。

【讨论】:

  • 不幸的是它不是跨浏览器。
  • 真的吗??加载脚本时谁不会触发“加载”事件? 等等 - 不要告诉我。
  • @Pointy 我通过使用 XMLHttpRequest 然后eval() 解决了这个问题。但是,调试它是一场噩梦 b/c 错误消息报告行 eval() 出现,而​​不是实际错误
  • 但是 requirejs 是如何做到这一点的呢?他们如何包含许多脚本并以正确的顺序触发它们?
  • 当然,document.write() 就是你要找的。不漂亮,但很管用。
【解决方案2】:

这不是很漂亮,但它有效:

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

脚本必须包含在单独的&lt;script&gt; 标记中或window.onload() 之前。

这不起作用:

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

创建节点也可以这样做,就像 Pointy 所做的那样,但仅限于 FF。您无法保证脚本何时在其他浏览器中准备就绪。

作为一个 XML 纯粹主义者,我真的很讨厌这个。但它确实可以预见地工作。您可以轻松地包装那些丑陋的document.write()s,这样您就不必看它们了。您甚至可以进行测试并创建一个节点并附加它,然后使用document.write()

【讨论】:

  • 你确定你的第一个代码 sn-p 可以在所有浏览器中运行吗?
  • @BogdanGusiev 我不是 100% 确定。我在 IE 8、(当时的当前版本)Firefox 和 Chrome 中进行了测试。这可能不适用于作为内容类型 application/xhtml+xml 的 XHTML 文档类型。
  • 很遗憾脚本标签不能在JS文件中使用。
  • @Clem 你可以做一个document.write("&lt;SCR" + "IPT&gt;" + "...")
【解决方案3】:

这已经很晚了,但为了让任何想这样做的人将来参考,您可以使用以下内容:

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

【讨论】:

  • 这真的有效吗?看我的问题:stackoverflow.com/questions/17978255/…
  • 这看起来很有趣。一个问题……为什么要执行两次回调方法? (在 onreadystatechange 中使用的 script.onload=callback 和 callback())
  • onreadysteatechange 适用于 IE,只会在 IE 上触发,因为 onload 不会触发 IE
【解决方案4】:

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

  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()
  • @JoshJohnson script.addEventListener('load', postLoadFunction); 表示在脚本加载时调用 postLoadFunction。
【解决方案5】:

异步编程稍微复杂,因为结果 发出请求的过程被封装在一个函数中,而不是跟随请求语句。 用户体验的实时行为可能显着 更好,因为他们不会看到缓慢的服务器或缓慢的网络导致 浏览器就像它已经崩溃一样。 同步编程是不尊重 并且不应被雇用在人们使用的应用程序中。

道格拉斯·克罗克福德 (YUI Blog)

好的,请系好座椅,因为这将是一段颠簸的旅程。越来越多的人询问如何通过javascript动态加载脚本,这似乎是一个热门话题。

它如此受欢迎的主要原因是:

  • 客户端模块化
  • 更轻松的依赖管理
  • 错误处理
  • 性能优势

关于模块化:很明显,管理客户端依赖关系应该在客户端处理。如果需要某个对象、模块或库,我们只需请求它并动态加载它。

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

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

有些人之所以坚持同步脚本,是因为他们习惯了。他们认为这是默认方式,是更简单的方式,有些人甚至认为这是唯一的方式。

但是,当需要就应用程序的设计做出决定时,我们唯一应该关心的是最终用户体验。在这方面,异步是不可战胜的。用户得到立即响应(或说承诺),承诺总比没有好。一个空白的屏幕吓坏了人们。开发人员不应该懒惰地提高感知性能

最后是关于肮脏的一面。你应该怎么做才能让它跨浏览器工作:

  1. 学会异步思考
  2. 将您的代码组织成模块化
  3. 组织代码以很好地处理错误和边缘情况
  4. 逐步增强
  5. 始终注意适量的反馈

【讨论】:

  • 谢谢,加拉姆。我想我应该更清楚。我确实希望这最终是异步的。我只是想要一种对程序员来说合乎逻辑的访问方式。我想避免这样的事情: Import("package.mod1", function() { // do stuff with mod1 }); Import("package.mod2", function() { // 用 mod2 做事 });我查看了您的脚本和 labjs,虽然不错,但对于我的需求来说似乎更复杂。我认为可能有一种更简单的方法,并希望避免引入额外的依赖项。
  • 你错过了我帖子的重点。这一切都与用户有关。这应该是您的首要任务。其他一切都是次要的。
  • 加拉姆,非常好的观点。用户体验非常重要。需要明确的是,我不愿意牺牲用户体验或质量、可维护的代码。我将研究闭包和labjs,看看它们能为我做什么。但目前我可能需要坚持使用
  • 再次,为了清楚起见,“同步”是用来表达我的观点的错误选择。我不希望浏览器在加载时冻结。
  • 如果需要同步加载怎么办?如果您确实需要阻止以保留用户体验。如果您使用的是基于 JavaScript 的 A/B 或 MVT 测试系统。您希望如何异步加载内容并替换默认值而不会出现破坏用户体验的闪烁效果?我愿意接受建议。我有 500 多名同事想知道这个问题的解决方案。如果您没有,请不要使用诸如“同步编程是不尊重的,不应在人们使用的应用程序中使用”之类的表达。
【解决方案6】:
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');
  })
  */
}

【讨论】:

    【解决方案7】:

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

    • 加载的代码均不可调试
    • 许多解决方案需要回调来知道加载何时完成而不是真正阻塞,这意味着我会因立即调用加载(即加载)代码而出现执行错误。

    或者,更准确地说:

    • 加载的代码均不可调试(HTML 脚本标记块除外,当且仅当解决方案将脚本元素添加到 dom 时,并且永远不会作为单独的可查看脚本。) = > 考虑到我必须加载(和调试)多少脚本,这是不可接受的。
    • 使用 'onreadystatechange' 或 'onload' 事件的解决方案未能阻止,这是一个大问题,因为代码最初使用 'require([filename, 'dojo/domReady']);' 同步加载动态脚本我正在剥离道场。

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

    警告:以下代码可能只在“开发”模式下使用。 (对于“发布”模式,我建议在不加载动态脚本的情况下进行预打包和缩小,或者至少不进行 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);
        }
    };
    

    【讨论】:

      【解决方案8】:

      这看起来像是动态脚本加载的一个不错的概述: http://unixpapa.com/js/dyna.html

      【讨论】:

        【解决方案9】:

        我习惯于在我的网站上有多个相互依赖的 .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' 
        ]) ;
        

        【讨论】:

          【解决方案10】:
          
          // ...
          
          await import_script('https://cdnjs.cloudflare.com/ajax/libs/...js');
          
          async function import_script(url) {
          
                  const script = document.createElement('script');
                  script.type = 'text/javascript';
                  script.src = url;
          
          
                  document.head.appendChild(script);
          
                  console.log(`import ${url} ...`);
          
                  await new Promise((resolve) => script.onload = resolve);
              }
          

          【讨论】:

            【解决方案11】:

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

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

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

            更详细的解释见here

            问题是代码在某处被评估,如果您的代码有任何问题,console.error(errorMessage); 语句将指示eval() 所在的行,而不是实际错误。这是一个很大的问题,我实际上正试图将其转换回&lt;script&gt; 语句(请参阅here

            【讨论】:

            • 有趣的事实:我也回到了&lt;script&gt; 标签并使用约定(以及一些构建包)以一种有意义的方式打包我的 js。
            • @JoshJohnson 我没那么幸运 b/c 我需要首先加载包,其中环内的脚本异步加载,环之间的脚本同步加载
            • 我很幸运,能够解决一些问题。我不羡慕你的职位。
            【解决方案12】:

            这适用于支持 async/awaitfetch 的现代 'evergreen' 浏览器。

            这个例子被简化了,没有错误处理,展示了工作中的基本原理。

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

            【讨论】:

            • 我们不能说它像webpack ... 1. 对于每个脚本,它都发送一个new HTTP request,2. 这也不会检查它们之间的依赖关系,3. 不是所有的浏览器支持async/await 和 4. 性能方面,我们乏味然后正常。 最好将其附加到head
            猜你喜欢
            • 2011-08-13
            • 1970-01-01
            • 1970-01-01
            • 2011-05-27
            • 2010-12-24
            • 1970-01-01
            • 2014-11-19
            • 1970-01-01
            • 2020-03-08
            相关资源
            最近更新 更多