同步动态加载JavaScript

时间:2010-05-21 04:10:41

标签: javascript

我正在使用module pattern,我要做的其中一件事是动态地包含一个外部JavaScript文件,执行该文件,然后使用{{1}中文件中的函数/变量我的模块。

我无法弄清楚如何轻松地做到这一点。有没有标准的方法来执行伪同步外部脚本加载?

return { }

17 个答案:

答案 0 :(得分:52)

只有一种方法可以同步加载和执行脚本资源,即使用同步XHR

这是如何执行此操作的示例

// get some kind of XMLHttpRequest
var xhrObj = createXMLHTTPObject();
// open and send a synchronous request
xhrObj.open('GET', "script.js", false);
xhrObj.send('');
// add the returned content to a newly created script tag
var se = document.createElement('script');
se.type = "text/javascript";
se.text = xhrObj.responseText;
document.getElementsByTagName('head')[0].appendChild(se);

但是你通常不应该使用同步请求,因为这会阻止其他一切。 但话虽如此,当然也有适当的情况。

我可能会使用onload处理程序将包含函数重构为异步模式。

答案 1 :(得分:37)

accepted answer 正确。

同步加载文件与同步执行文件不同 - 这是OP请求的。

接受的答案会加载文件同步,但只会将脚本标记附加到DOM。只是因为 appendChild()已经返回并不能保证脚本已经完成执行并且它的成员被初始化以供使用。

实现OP问题的唯一(见警告)方法是按照规定同步加载XHR上的脚本,然后作为文本读取并传入eval()或新的Function()调用并等待该函数返回。这是保证脚本加载 AND 同步执行的唯一方法。

我没有评论从UI或安全角度来看这是否是明智的做法,但肯定有一些用例可以证明同步加载和放大器的合理性。执行。

买者: 除非你使用网络工作者,否则只需调用loadScripts();

答案 2 :(得分:10)

这是我在我的应用中用于多个文件加载的代码。

Utilities.require = function (file, callback) {
    callback = callback ||
    function () {};
    var filenode;
    var jsfile_extension = /(.js)$/i;
    var cssfile_extension = /(.css)$/i;

    if (jsfile_extension.test(file)) {
        filenode = document.createElement('script');
        filenode.src = file;
        // IE
        filenode.onreadystatechange = function () {
            if (filenode.readyState === 'loaded' || filenode.readyState === 'complete') {
                filenode.onreadystatechange = null;
                callback();
            }
        };
        // others
        filenode.onload = function () {
            callback();
        };
        document.head.appendChild(filenode);
    } else if (cssfile_extension.test(file)) {
        filenode = document.createElement('link');
        filenode.rel = 'stylesheet';
        filenode.type = 'text/css';
        filenode.href = file;
        document.head.appendChild(filenode);
        callback();
    } else {
        console.log("Unknown file type to load.")
    }
};

Utilities.requireFiles = function () {
    var index = 0;
    return function (files, callback) {
        index += 1;
        Utilities.require(files[index - 1], callBackCounter);

        function callBackCounter() {
            if (index === files.length) {
                index = 0;
                callback();
            } else {
                Utilities.requireFiles(files, callback);
            }
        };
    };
}();

可以使用此实用程序
Utilities.requireFiles(["url1", "url2",....], function(){
    //Call the init function in the loaded file.
    })

答案 3 :(得分:5)

我能想出的最类似Node.js的实现能够同步加载JS文件,并将它们用作对象/模块

var scriptCache = [];
var paths = [];
function Import(path)
{
    var index = 0;
    if((index = paths.indexOf(path)) != -1) //If we already imported this module
    {
        return scriptCache [index];
    }

    var request, script, source;
    var fullPath = window.location.protocol + '//' + window.location.host + '/' + path;

    request = new XMLHttpRequest();
    request.open('GET', fullPath, false);
    request.send();

    source = request.responseText;

    var module = (function concealedEval() {
        eval(source);
        return exports;
    })();

    scriptCache.push(module);
    paths.push(path);

    return module;
}

示例来源(addobjects.js):

function AddTwoObjects(a, b)
{
    return a + b;
}

this.exports = AddTwoObjects;

并像这样使用它:

var AddTwoObjects = Import('addobjects.js');
alert(AddTwoObjects(3, 4)); //7
//or even like this:
alert(Import('addobjects.js')(3, 4)); //7

答案 4 :(得分:3)

我对此问题的现有答案存在以下问题(以及此问题在其他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);
    }
};

答案 5 :(得分:2)

var xhrObj = new XMLHttpRequest();
xhrObj.open('GET', '/filename.js', false);
xhrObj.send(null);
eval(xhrObj.responseText);

如果这是跨域请求,则无效。在这种情况下,你必须将所请求的文件上传到你的服务器,或者制作一个输出它的镜像php,并且需要php。

使用jquery(也适用于跨域请求):

$.getScript('/filename.js',callbackFunction);

callbackFunction将同步调用。

要加载更多脚本,请参阅this thread。

答案 6 :(得分:1)

如果您需要加载任意数量的脚本并且仅在最后一个脚本完成时继续,并且您不能使用XHR(例如由于CORS限制),您可以执行以下操作。它不是同步的,但允许在最后一个文件加载时完全发生回调:

// Load <script> elements for all uris
// Invoke the whenDone callback function after the last URI has loaded
function loadScripts(uris,whenDone){
  if (!uris.length) whenDone && whenDone();
  else{
    for (var wait=[],i=uris.length;i--;){
      var tag  = document.createElement('script');
      tag.type = 'text/javascript';
      tag.src  = uris[i];
      if (whenDone){
        wait.push(tag)
        tag.onload = maybeDone; 
        tag.onreadystatechange = maybeDone; // For IE8-
      }
      document.body.appendChild(tag);
    }
  }
  function maybeDone(){
    if (this.readyState===undefined || this.readyState==='complete'){
      // Pull the tags out based on the actual element in case IE ever
      // intermingles the onload and onreadystatechange handlers for the same
      // script block before notifying for another one.
      for (var i=wait.length;i--;) if (wait[i]==this) wait.splice(i,1);
      if (!wait.length) whenDone();
    }
  }
}

编辑:已更新,可与IE7,IE8和IE9配合使用(在怪癖模式下)。这些IE版本不会触发onload事件,但针对onreadystatechange。标准模式下的IE9会激活两者onreadystatechange用于onload之前触发的所有脚本。

基于this page,旧版本的IE永远不会发送onreadystatechange事件readyState=='complete';如果是这种情况(我无法重现此问题),则上述脚本将失败,并且永远不会调用您的回调。

答案 7 :(得分:1)

实际上有一种方法可以同步加载脚本列表并执行它们。您需要将每个脚本标记插入到DOM中,明确将其async属性设置为false:

script.async = false;

默认情况下,已注入DOM的脚本是异步执行的,因此您必须手动将async属性设置为false以解决此问题。

实施例

<script>
(function() {
  var scriptNames = [
    "https://code.jquery.com/jquery.min.js",
    "example.js"
  ];
  for (var i = 0; i < scriptNames.length; i++) {
    var script = document.createElement('script');
    script.src = scriptNames[i];
    script.async = false; // This is required for synchronous execution
    document.head.appendChild(script);
  }
  // jquery.min.js and example.js will be run in order and synchronously
})();
</script>

<!-- Gotcha: these two script tags may still be run before `jquery.min.js`
     and `example.js` -->
<script src="example2.js"></script>
<script>/* ... */<script>

参考

答案 8 :(得分:1)

接受的答案不正确:

script.async = false;伪指令仅意味着在脚本执行期间将暂停html分析。这不能保证javascript代码将按哪个顺序运行。参见https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/

这里尚未提及的最简单,最优雅的解决方案是使用诺言,例如:

    function loadScript(url) {
      return new Promise((resolve, reject) => {
        var script = document.createElement('script')
        script.src = url
        script.onload = () => {
          resolve()
        }
        script.onerror = () => {
          reject('cannot load script '+ url)
        }
        document.body.appendChild(script)
      })
    }

,然后在要按顺序执行脚本时:

        loadScript('myfirstscript.js').then(() => {
          console.log('first script ran');
          loadScript('index.js').then(() => {
            console.log('second script ran');
          })
        })

答案 9 :(得分:0)

与肖恩的答案相同,但不是创建脚本标签,而是评估它。这确保了代码实际上可以使用。

答案 10 :(得分:0)

由于显而易见的原因,不能和不应同步执行服务器操作。但是,您可以做的是让事件处理程序告诉您何时加载脚本:

tag.onreadystatechange = function() { if (this.readyState == 'complete' || this.readyState == 'loaded') this.onload({ target: this }); };

tag.onload = function(load) {/*init code here*/}
从内存来看,

onreadystatechange委托是IE的一种解决方法,它对onload提供了不完整的支持。

答案 11 :(得分:0)

使用Angular时,您可以利用每个Provider在实例化其他服务之前实例化的事实。您可以将此事实与使用xhr和@Neil提到的eval()结合起来。代码如下:

app.provider('SomeScriptSyncLoader', function() {

    var resourceUrl =  'http://some/script.js';
    var dummy = {};

    this.$get = function() {

        var q = jQuery.ajax({
            type: 'GET', url: resourceUrl, cache: false, async: false
        });

        if (q.status === 200) {
            eval(q.responseText); // execute some script synchronously as inline script - eval forces sync processing
        }
        return dummy;
    };
});

要强制提供者被初始化,您需要将其注入至少一个其他指令/服务。优选地,这将是利用脚本加载的代码的服务。

app.directive('myDirective', ['SomeScriptSyncLoader', function(someScriptSyncLoader) {

return {
    restrict: 'E',
    link: function(scope, element, attrs) {
        // some ode
    },
    template: "this is my template"
   };
}]);

答案 12 :(得分:0)

我知道这是一个老问题,但也许有人读过这个并发现它很有用! 刚刚创建的新组件使用ES6以同步方式动态加载脚本。 项目详细信息和源代码位于GitHub https://github.com/amgadfahmi/scripty

答案 13 :(得分:0)

我回答这个问题可能会迟到。

我目前的解决方案是以递归方式添加<script>标记,以便后续脚本的添加位于其前任的回调中。它假定每个函数包含一个函数,该函数与文件名相同(减去扩展名)。这可能不是最好的做事方式,但它运作正常。

要考虑的代码

代码目录结构:

- directory
---- index.html
---- bundle.js
---- test_module/
-------- a.js
-------- b.js
-------- log_num.js
-------- many_parameters.js

的index.html

<head>
  <script src="bundle.js"></script>
</head>

bundle.js

// Give JS arrays the .empty() function prototype
if (!Array.prototype.empty){
    Array.prototype.empty = function(){
        return this.length == 0;
    };
};

function bundle(module_object, list_of_files, directory="") {
  if (!list_of_files.empty()) {
    var current_file = list_of_files.pop()
    var [function_name, extension] = current_file.split(".")
    var new_script = document.createElement("script")
    document.head.appendChild(new_script)

    new_script.src = directory + current_file

    new_script.onload = function() {
      module_object[function_name] = eval(function_name)
      bundle(module_object, list_of_files, directory)
      /*
      nullify the function in the global namespace as - assumed -  last
      reference to this function garbage collection will remove it. Thus modules
      assembled by this function - bundle(obj, files, dir) - must be called
      FIRST, else one risks overwritting a funciton in the global namespace and
      then deleting it
      */
      eval(function_name + "= undefined")
    }
  }
}

var test_module = {}
bundle(test_module, ["a.js", "b.js", "log_num.js", "many_parameters.js"], "test_module/")

a.js

function a() {
  console.log("a")
}

b.js

function b() {
  console.log("b")
}

log_num.js

// it works with parameters too
function log_num(num) {
  console.log(num)
}

many_parameters.js

function many_parameters(a, b, c) {
  var calc = a - b * c
  console.log(calc)
}

答案 14 :(得分:0)

这是我的代码

var loaded_script = [];
function loadScript(urls, callback, sync) {
    var len = urls.length, count = 0;

    // check are all js loaded, then execute callback (if any)
    var check = function() {
        if (count == len) {
            callback && typeof callback=="function" && callback();
        }
    };

    for (var i = 0; i < len; i++) {
        var url = urls[i];

        // check if script not loaded (prevent load again)
        if (loaded_script.indexOf(url) == -1) {
            var script = document.createElement("script");
            script.type = "text/javascript";

            // set sync loading here (default is async)
            if (sync) {
                script.async = false;
            }

            // script onload event
            if (script.readyState) {    // IE
                script.onreadystatechange = function() {
                    if (script.readyState=="loaded" || script.readyState=="complete") {
                        script.onreadystatechange = null;
                        count++, check();
                    }
                };
            } else {    // Others
                script.onload = function() {
                    count++, check();
                };
            }

            // add script to head tag
            script.src = url;
            document.getElementsByTagName("head")[0].appendChild(script);

            // mark this script has loaded
            loaded_script.push(url);
        } else {
            count++, check();
        }
    }
}

我在pjax网站上使用它。

loadScript(
    [
        "js/first.js",
        "js/second.js",
    ],
    function() {
        alert("Scripts loaded.");
    },
    true
);

答案 15 :(得分:0)

前几天我也有类似的任务,这就是我的工作方法。
该加载程序既可以在file://前缀中使用,也可以在http://https://中使用,并且可以跨浏览器兼容。
但是,它不能从脚本中加载特定的类或函数作为模块;它将全部加载整个脚本并使其可用于DOM。

// Loads a script or an array of scripts (including stylesheets)
// in their respective index order, synchronously.
// By Sayanjyoti Das @https://stackoverflow.com/users/7189950/sayanjyoti-das
var Loader={
    queue: [], // Scripts queued to be loaded synchronously
    loadJsCss: function(src, onl) {
        var ext=src.toLowerCase().substring(src.length-3, src.length);
        if(ext=='.js') {
            var scrNode=el('script', null, null, null);
            scrNode.type='text/javascript';
            scrNode.onload=function() {onl();};
            scrNode.src=src;
            document.body.appendChild(scrNode);
        }else if(ext=='css') {
            var cssNode=el('link', null, null, null);
            cssNode.rel='stylesheet';
            cssNode.type='text/css';
            cssNode.href=src;
            document.head.appendChild(cssNode);
            onl();
        }
    },
    add: function(data) {
        var ltype=(typeof data.src).toLowerCase();

        // Load a single script
        if(ltype=='string') {
            data.src=data.src;
            Loader.queue.splice(0, 1, data, Loader.queue[0]);
            Loader.next();
        }
        // Load an array of scripts
        else if(ltype=='object') {
            for(var i=data.src.length-1; i>=0; i--) {
                Loader.queue.splice(0, 1, {
                    src: data.src[i],
                    onload: function() {
                        if(Loader.next()==false) {
                            data.onload();
                            return;
                        }
                        Loader.next();
                    }
                }, Loader.queue[0]);
            }
            Loader.next();
        }
    },
    next: function() {
        if(Loader.queue.length!=0 && Loader.queue[0]) {
            var scr=Loader.queue[0];

            // Remove the script from the queue
            if(Loader.queue.length>1)
                Loader.queue.splice(0, 2, Loader.queue[1]);
            else
                Loader.queue=[];

            // Load the script
            Loader.loadJsCss(scr.src, scr.onload);
        }else return false;
    }
};

上述功能非常强大和优雅;它允许您同步加载单个脚本或脚本数组(即,直到上一个脚本加载完成才加载下一个脚本)。此外,加载的脚本可能会加载更多的脚本,从而延迟了父脚本中的队列。

顺便说一句,这里的脚本表示JavaScript文件或CSS样式表

使用方法如下:-

// Load a single script
Loader.add({
    src: 'test.js',
    onload: function() {
        alert('yay!');
    }
});

// Load multiple scripts
Loader.add({
    src: ['test1.js', 'test2.js', 'mystyles.css', 'test3.js'],
    onload: function() {
        alert('all loaded!');
    }
});

请注意,加载了所有个脚本后,将调用Loader参数中的onload函数,而不是加载一个或单个脚本时。

您还可以在已加载的脚本中加载更多脚本,例如在test.jstest1.js等中加载脚本。这样做可以推迟下一个父脚本和队列中的加载。子脚本将获得优先级。

希望它会有所帮助:-)

答案 16 :(得分:-1)

我使用应用于div元素的jquery load 方法。

之类的东西
<div id="js">
<!-- script will be inserted here --> 
</div>

...

$("#js").load("path", function() {  alert("callback!" });

您可以多次加载脚本,每次一个脚本将完全替换之前加载的脚本