如何判断<script>标签是否加载失败

147
我正在动态地向页面的中添加

4
大型网站,自动加载通过ajax加载的内容的依赖项。if+轮询是让我不想在所有JS中都加入的烦人的操作。 - David
2
在使用脚本标签注入进行JSONP请求时,您可能还需要检查加载失败。 - schellsan
2
跳转至答案 https://dev59.com/2XRB5IYBdhLWcg3wuZQ1#47GhEYcBWogLw_1b65sz - Pacerier
16个回答

46

2021年更新: 现在所有的浏览器都支持在脚本标签上使用onerror="",例如:

2010年原始评论:

如果只关心 HTML5 浏览器,您可以使用 error 事件。

根据规范:

如果 src 属性的值是空字符串,或者无法解析,则用户代理必须在元素上排队一个名为 error 的简单事件,并中止这些步骤。

(...)

如果加载导致错误(例如 DNS 错误或 HTTP 404 错误),则执行脚本块必须仅包括在元素上触发名为 error 的简单事件。

这意味着您不必进行任何容易出错的轮询,并且可以结合 async 和 defer 属性来确保脚本不会阻塞页面渲染:

即使指定了 async 属性,也可以指定 defer 属性,以使仅支持 defer(而不是 async)的旧 Web 浏览器回退到 defer 行为,而不是同步阻止行为。

更多信息请参见http://www.w3.org/TR/html5/scripting-1.html#script


1
有没有关于JSONP实现的想法,例如jQuery是否支持此功能?我在这里阅读的所有内容都说无法检测到JSONP加载失败,但超时可以是一种解决方法,并且最近版本的JSONP已添加了超时。不过,似乎这个答案提供的比超时更好的东西 - 这是正确的吗? - hippietrail
1
示例可能吗? - rogerdpack
18
<script src="nonexistent.js" onerror="alert('error!')"></script> fiddle - Rudey
1
@Rudey Uncaught SyntaxError: Unexpected token '<'(在最新版的Chrome中) - daleyjem
@daleyjem 听起来你试图在 JavaScript 中放置 HTML 标签。<script src="nonexistent.js" onerror="alert('error!')"></script> 应该放在你的 HTML 文件中,而不是 JS 文件中。 - Rudey

35

脚本标签没有错误事件。您可以在成功时得知它并在超时后假定它未加载:

<script type="text/javascript" onload="loaded=1" src="....js"></script>

8
即使发生JavaScript错误,"onload"监听器仍将被触发。 - Luca Matteis
44
是的,如果文件加载并且文件本身存在错误,就会出现问题。但是,如果文件没有被提供服务, onload 事件将永远不会触发。 - Diodeus - James MacFarlane
1
但是超时时间必须由页面的编码人员提供,对吧?因此,编码人员可能会选择与浏览器不同的超时时间,假设脚本加载超时,然后稍后成功。或者不成功? - hippietrail
17
在 script 标签中有一个 onerror 事件。当资源未找到时会触发该事件。 - Nguyen Tran
2
正如其他人已经评论的那样,现代浏览器中确实存在error事件——因为错误地陈述“脚本标签没有错误事件”而被点负。请更新或删除这个过时/错误的答案。 - mindplay.dk
显示剩余4条评论

28

我的清洁解决方案(2017年)

function loaderScript(scriptUrl){
   return new Promise(function (res, rej) {
    let script = document.createElement('script');
    script.src = scriptUrl;
    script.type = 'text/javascript';
    script.onerror = rej;
    script.async = true;
    script.onload = res;
    script.addEventListener('error',rej);
    script.addEventListener('load',res);
    document.head.appendChild(script);
 })

}

正如马丁所指出的,使用方法如下:

const event = loaderScript("myscript.js")
  .then(() => { console.log("loaded"); })
  .catch(() => { console.log("error"); });

或者

try{
 await loaderScript("myscript.js")
 console.log("loaded"); 
}catch{
 console.log("error");
}

1
使用方法如下:loaderScript("myscript.js").then(() => { console.log("loaded"); }).catch(() => { console.log("error"); }); - Martin Wantke

21
Erwinus的脚本非常好用,但是代码不够清晰。我花了点时间整理并弄明白它在做什么。我进行了以下更改:
  • 使用有意义的变量名
  • 使用prototype
  • require()使用参数变量
  • 默认情况下不返回alert()消息
  • 修复了一些语法错误和范围问题
再次感谢Erwinus,这个功能本身非常棒。
function ScriptLoader() {
}

ScriptLoader.prototype = {

    timer: function (times, // number of times to try
                     delay, // delay per try
                     delayMore, // extra delay per try (additional to delay)
                     test, // called each try, timer stops if this returns true
                     failure, // called on failure
                     result // used internally, shouldn't be passed
            ) {
        var me = this;
        if (times == -1 || times > 0) {
            setTimeout(function () {
                result = (test()) ? 1 : 0;
                me.timer((result) ? 0 : (times > 0) ? --times : times, delay + ((delayMore) ? delayMore : 0), delayMore, test, failure, result);
            }, (result || delay < 0) ? 0.1 : delay);
        } else if (typeof failure == 'function') {
            setTimeout(failure, 1);
        }
    },

    addEvent: function (el, eventName, eventFunc) {
        if (typeof el != 'object') {
            return false;
        }

        if (el.addEventListener) {
            el.addEventListener(eventName, eventFunc, false);
            return true;
        }

        if (el.attachEvent) {
            el.attachEvent("on" + eventName, eventFunc);
            return true;
        }

        return false;
    },

    // add script to dom
    require: function (url, args) {
        var me = this;
        args = args || {};

        var scriptTag = document.createElement('script');
        var headTag = document.getElementsByTagName('head')[0];
        if (!headTag) {
            return false;
        }

        setTimeout(function () {
            var f = (typeof args.success == 'function') ? args.success : function () {
            };
            args.failure = (typeof args.failure == 'function') ? args.failure : function () {
            };
            var fail = function () {
                if (!scriptTag.__es) {
                    scriptTag.__es = true;
                    scriptTag.id = 'failed';
                    args.failure(scriptTag);
                }
            };
            scriptTag.onload = function () {
                scriptTag.id = 'loaded';
                f(scriptTag);
            };
            scriptTag.type = 'text/javascript';
            scriptTag.async = (typeof args.async == 'boolean') ? args.async : false;
            scriptTag.charset = 'utf-8';
            me.__es = false;
            me.addEvent(scriptTag, 'error', fail); // when supported
            // when error event is not supported fall back to timer
            me.timer(15, 1000, 0, function () {
                return (scriptTag.id == 'loaded');
            }, function () {
                if (scriptTag.id != 'loaded') {
                    fail();
                }
            });
            scriptTag.src = url;
            setTimeout(function () {
                try {
                    headTag.appendChild(scriptTag);
                } catch (e) {
                    fail();
                }
            }, 1);
        }, (typeof args.delay == 'number') ? args.delay : 1);
        return true;
    }
};

$(document).ready(function () {
    var loader = new ScriptLoader();
    loader.require('resources/templates.js', {
        async: true, success: function () {
            alert('loaded');
        }, failure: function () {
            alert('NOT loaded');
        }
    });
});

4
我被迫使用jQuery的 $.getScript 函数,因为在MSIE8-中,该函数无法处理缓存的脚本,非常遗憾。 - Zathrus Writer
@ZathrusWriter 那可能是个更好的想法,谢谢你让我们知道!你可以在这段代码中添加一个随机整数到URL中,这样就可以去除缓存。 - Aram Kocharyan
2
是的,Aram,那肯定可以解决问题,但这也会使浏览器的缓存失效,所以在这种情况下的开销可能并不值得。 ;) - Zathrus Writer
@ZathrusWriter,那么jQuery实现的工作原理是什么? - Pacerier
@Pacerier 抱歉,我不是jQuery核心开发者,所以我无法回答这个问题。 - Zathrus Writer

17

我知道这是一个老的线程,但是我有一个不错的解决方案(我觉得)。它是从我的一个课程中复制而来,该课程处理所有AJAX相关的事务。

当脚本无法加载时,它会设置一个错误处理程序,但当错误处理程序不受支持时,它会回退到一个计时器,在15秒内检查错误。

function jsLoader()
{
    var o = this;

    // simple unstopable repeat timer, when t=-1 means endless, when function f() returns true it can be stopped
    o.timer = function(t, i, d, f, fend, b)
    {
        if( t == -1 || t > 0 )
        {
            setTimeout(function() {
                b=(f()) ? 1 : 0;
                o.timer((b) ? 0 : (t>0) ? --t : t, i+((d) ? d : 0), d, f, fend,b );
            }, (b || i < 0) ? 0.1 : i);
        }
        else if(typeof fend == 'function')
        {
            setTimeout(fend, 1);
        }
    };

    o.addEvent = function(el, eventName, eventFunc)
    {
        if(typeof el != 'object')
        {
            return false;
        }

        if(el.addEventListener)
        {
            el.addEventListener (eventName, eventFunc, false);
            return true;
        }

        if(el.attachEvent)
        {
            el.attachEvent("on" + eventName, eventFunc);
            return true;
        }

        return false;
    };

    // add script to dom
    o.require = function(s, delay, baSync, fCallback, fErr)
    {
        var oo = document.createElement('script'),
        oHead = document.getElementsByTagName('head')[0];
        if(!oHead)
        {
            return false;
        }

        setTimeout( function() {
            var f = (typeof fCallback == 'function') ? fCallback : function(){};
            fErr = (typeof fErr == 'function') ? fErr : function(){
                alert('require: Cannot load resource -'+s);
            },
            fe = function(){
                if(!oo.__es)
                {
                    oo.__es = true;
                    oo.id = 'failed';
                    fErr(oo);
                }
            };
            oo.onload = function() {
                oo.id = 'loaded';
                f(oo);
            };
            oo.type = 'text/javascript';
            oo.async = (typeof baSync == 'boolean') ? baSync : false;
            oo.charset = 'utf-8';
            o.__es = false;
            o.addEvent( oo, 'error', fe ); // when supported

            // when error event is not supported fall back to timer
            o.timer(15, 1000, 0, function() {
                return (oo.id == 'loaded');
            }, function(){ 
                if(oo.id != 'loaded'){
                    fe();
                }
            });
            oo.src = s;
            setTimeout(function() {
                try{
                    oHead.appendChild(oo);
                }catch(e){
                    fe();
                }
            },1); 
        }, (typeof delay == 'number') ? delay : 1);  
        return true;
    };

}

$(document).ready( function()
{
    var ol = new jsLoader();
    ol.require('myscript.js', 800, true, function(){
        alert('loaded');
    }, function() {
        alert('NOT loaded');
    });
});

4
jQuery最初的来源是什么? - Naftali
1
而且,它是一个跨浏览器的解决方案;-) - Codebeat
1
@Erwinus 我没有检查头文件,只是进行了一个快速的跨浏览器检查,*$.getScript* 总是能够正常运行,所以我坚持使用它...你可以自己尝试一下,我在这里使用的是W7和XAMPP。 - Zathrus Writer
21
无论这是否奏效,阅读起来都让人极度痛苦——更不用说调试了。这是一种相当糟糕的格式和真正可怕的编码风格。仅变量命名就是一团混乱。 - Vala
7
如果你不认为可读性代码的价值(而你可以通过任何更改不涉及实际执行的代码来实现可读性),那么我为每一个必须与你及你的代码共事的人感到遗憾。 - Vala
显示剩余10条评论

10

要检查 nonexistant.js 中的 JavaScript 是否没有返回错误,您需要在 http://fail.org/nonexistant.js 中添加一个变量,比如 var isExecuted = true; ,然后在脚本标签加载时检查它是否存在。

但是如果您只想检查 nonexistant.js 是否返回了 404(表示文件存在),您可以尝试使用一个 isLoaded 变量...

var isExecuted = false;
var isLoaded = false;
script_tag.onload = script_tag.onreadystatechange = function() {
    if(!this.readyState ||
        this.readyState == "loaded" || this.readyState == "complete") {
        // script successfully loaded
        isLoaded = true;

        if(isExecuted) // no error
    }
}

这将同时涵盖两种情况。


1
它能工作,但并不完全符合我的要求——在那种情况下,失败永远不会触发事件。我不想轮询一个变量,如果几秒钟后它仍然是false,就触发on-fail回调。 - David
您想要检索什么类型的错误?加载失败?在nonexstant.js中执行javascript失败?HTTP响应错误?请更加具体地描述。 - Luca Matteis
以上任何一种都可以。但我特别关心404错误。 - David
404,意味着您想检查文件nonexistant.js是否不存在?但如果存在,您想检查它是否没有返回错误? - Luca Matteis
嘿,David,你应该能够假设如果this.readyState/...不为真,则脚本加载失败。基本上是一个onError。 - Cody
@Cody,他的意思是onload回调函数没有运行,因为没有onload事件(例如网络故障或服务器宕机等等)。 - Pacerier

7
我希望这篇文章不会被downvote,因为在特殊情况下,它是解决问题最可靠的方式。只要服务器允许您使用CORS(http://en.wikipedia.org/wiki/Cross-origin_resource_sharing)获取Javascript资源,您就有多种选择来实现这一点。
使用XMLHttpRequest获取资源将适用于所有现代浏览器,包括IE。由于您想要加载Javascript,因此首先可以使用Javascript。您可以使用readyState(http://en.wikipedia.org/wiki/XMLHttpRequest#The_onreadystatechange_event_listener)跟踪进度。最后,一旦接收到文件的内容,就可以使用eval()执行它。是的,我说了eval - 因为从安全性角度来看,它与正常加载脚本没有区别。事实上,John Resig建议使用类似的技术来获得更好的标记(http://ejohn.org/blog/degrading-script-tags/)。

这种方法还允许您将加载与 eval 分离,并在 eval 发生之前和之后执行函数。当并行加载脚本但逐个评估它们时,它变得非常有用 - 当您将 标签放入HTML中时,浏览器可以轻松完成此操作,但是通过使用Javascript在运行时添加脚本时则不行。

对于加载脚本(http://en.wikipedia.org/wiki/XMLHttpRequest#Cross-domain_requests),CORS也优于JSONP。但是,如果您正在开发自己的第三方小部件以嵌入到其他网站中,则应该在您自己的iframe中从您自己的域加载Javascript文件(同样使用AJAX)。

简而言之:

  1. 尝试使用 AJAX GET 加载资源

  2. 在成功加载后使用 eval

为了改进它:

  1. 查看发送的缓存控制标头

  2. 如果需要,考虑将内容缓存在 localStorage 中

  3. 查看 Resig 的“退化javascript”以获得更清晰的代码

  4. 查看 require.js


请注意,此方法要求源服务器启用CORS,这并非总是可控或可行的 :| - rogerdpack

5
这个技巧对我很有用,尽管我承认这可能不是解决这个问题的最佳方法。与其尝试这种方法,你应该看看为什么javascript没有加载。尝试在你的服务器上保留脚本的本地副本等,或者检查从哪个第三方供应商下载脚本。
无论如何,以下是解决方法: 1)将变量初始化为false 2)使用onload属性在javascript加载时将其设置为true 3)在HTML body加载后检查变量是true还是false
<html>
  <head>
    <script>
      var scriptLoaded = false;

      function checkScriptLoaded() {
        if (scriptLoaded) {
          // do something here
        } else {
          // do something else here!
        }
      }
    </script>
    <script src="http://some-external-script.js" onload="scriptLoaded=true;" />
  </head>
  <body onload="checkScriptLoaded()">
    <p>My Test Page!</p>
  </body>
</html>

1
如果脚本仍在加载中怎么办?你如何知道文件是否未找到或者只是需要等待? - Sergey Orshanskiy
这个解决方案在我的测试中有效,只要脚本标签直接放置在文档源中。文档的onload事件直到页面源中引用的所有资源已经加载(或失败)才会触发。但是,如果您需要稍后将脚本插入DOM,则需要另一种方法。 - Jamey Sharp
很棒的解决方案。我不明白为什么它没有被点赞。 - Lotfi
这假设脚本将同步加载阻塞其他所有内容,但这并不总是正确的。事实上,这不是一种推荐的加载脚本的方式。 - Vitim.us

4

这可以通过使用Promise来安全地完成

    function loadScript(src) {
      return new Promise(function(resolve, reject) {
        let script = document.createElement('script');
        script.src = src;
    
        script.onload = () => resolve(script);
        script.onerror = () => reject(new Error("Script load error: " + src));
    
        document.head.append(script);
      });
    }

并且可以像这样使用。
    let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");
    
    promise.then(
      script => alert(`${script.src} is loaded!`),
      error => alert(`Error: ${error.message}`)
    );

4

这里有另一种基于JQuery的解决方案,不需要任何计时器:

<script type="text/javascript">
function loadScript(url, onsuccess, onerror) {
$.get(url)
    .done(function() {
        // File/url exists
        console.log("JS Loader: file exists, executing $.getScript "+url)
        $.getScript(url, function() {
            if (onsuccess) {
                console.log("JS Loader: Ok, loaded. Calling onsuccess() for " + url);
                onsuccess();
                console.log("JS Loader: done with onsuccess() for " + url);
            } else {
                console.log("JS Loader: Ok, loaded, no onsuccess() callback " + url)
            }
        });
    }).fail(function() {
            // File/url does not exist
            if (onerror) {
                console.error("JS Loader: probably 404 not found. Not calling $.getScript. Calling onerror() for " + url);
                onerror();
                console.error("JS Loader: done with onerror() for " + url);
            } else {
                console.error("JS Loader: probably 404 not found. Not calling $.getScript. No onerror() callback " + url);
            }
    });
}
</script>

感谢: https://dev59.com/s3A65IYBdhLWcg3wxBir#14691735

示例用法(来自JQuery getScript文档):

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>jQuery.getScript demo</title>
  <style>
  .block {
     background-color: blue;
     width: 150px;
     height: 70px;
     margin: 10px;
  }
  </style>
  <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
</head>
<body>

<button id="go">&raquo; Run</button>
<div class="block"></div>

<script>


function loadScript(url, onsuccess, onerror) {
$.get(url)
    .done(function() {
        // File/url exists
        console.log("JS Loader: file exists, executing $.getScript "+url)
        $.getScript(url, function() {
            if (onsuccess) {
                console.log("JS Loader: Ok, loaded. Calling onsuccess() for " + url);
                onsuccess();
                console.log("JS Loader: done with onsuccess() for " + url);
            } else {
                console.log("JS Loader: Ok, loaded, no onsuccess() callback " + url)
            }
        });
    }).fail(function() {
            // File/url does not exist
            if (onerror) {
                console.error("JS Loader: probably 404 not found. Not calling $.getScript. Calling onerror() for " + url);
                onerror();
                console.error("JS Loader: done with onerror() for " + url);
            } else {
                console.error("JS Loader: probably 404 not found. Not calling $.getScript. No onerror() callback " + url);
            }
    });
}


loadScript("https://raw.github.com/jquery/jquery-color/master/jquery.color.js", function() {
  console.log("loaded jquery-color");
  $( "#go" ).click(function() {
    $( ".block" )
      .animate({
        backgroundColor: "rgb(255, 180, 180)"
      }, 1000 )
      .delay( 500 )
      .animate({
        backgroundColor: "olive"
      }, 1000 )
      .delay( 500 )
      .animate({
        backgroundColor: "#00f"
      }, 1000 );
  });
}, function() { console.error("Cannot load jquery-color"); });


</script>
</body>
</html>

很棒 Sergey!那么,你使用 .get() 来检查文件是否存在,并使用 .getScript() 来检查它是否可以被执行/加载而不出现错误? - jjwdesign
2
是的。就我所记得的,这种方法确实有局限性:你将无法以这种方式加载很多脚本 --- 如果我记得正确,这是由于跨域脚本限制。 - Sergey Orshanskiy
使用jQuery(或其他库)的问题在于您需要首先加载该库。那么,您如何检查是否未能加载库? - Pacerier
我也采用了这种方法,但我建议在 $.getScript 调用中使用 cache:true(默认情况下关闭)。这样,在大多数请求中,它会自动使用 getScript 调用的缓存版本(节省您的延迟时间)。 - Laurens Rietveld

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接