繁体   English   中英

同步动态加载JavaScript

[英]Dynamically loading JavaScript synchronously

我正在使用模块模式 ,我想要做的事情之一是动态包含外部JavaScript文件,执行文件,然后在我的模块的return { }中使用文件中的函数/变量。

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

function myModule() {
    var tag = document.createElement("script");
    tag.type = "text/javascript";
    tag.src = "http://some/script.js";
    document.getElementsByTagName('head')[0].appendChild(tag);

    //something should go here to ensure file is loaded before return is executed

    return {
        external: externalVariable 
    }
}

只有一种方法可以同步加载和执行脚本资源,即使用同步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处理程序将包含函数重构为异步模式。

接受的答案正确的。

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

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

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

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

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

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

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

我能想出的最类似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

我对此问题的现有答案(以及其他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);
    }
};
var xhrObj = new XMLHttpRequest();
xhrObj.open('GET', '/filename.js', false);
xhrObj.send(null);
eval(xhrObj.responseText);

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

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

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

callbackFunction将被同步调用。

要加载更多脚本,请参阅主题。

如果您需要加载任意数量的脚本并且仅在最后一个脚本完成时继续,并且您不能使用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之前触发)。

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

实际上有一种方法可以加载脚本列表同步执行它们 您需要将每个脚本标记插入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>

参考

接受的答案是不正确的:

script.async = false; 指令仅表示在脚本执行期间将暂停html解析。 这并不能保证javascript代码的运行顺序。 请参阅https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/

这里尚未提及的最简单,最优雅的解决方案是使用promises,如下所示:

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

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

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

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

tag.onload = function(load) {/*init code here*/}

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

我的策略,加载jQuery UI时的经典示例,我希望这可以帮到你

 ( function( tools, libs ){ // Iterator var require = function( scripts, onEnd ){ onEnd = onEnd || function(){}; if( !scripts || scripts.length < 1 )return onEnd(); var src = scripts.splice( 0, 1), script = document.createElement( "script" ); script.setAttribute( "src", src ); tools.addEvent( "load", script, function(){ require( scripts, onEnd ); } ); document.getElementsByTagName( "head" )[ 0 ].appendChild( script ); }; // Install all scripts with a copy of scripts require( libs.slice(), function(){ alert( "Enjoy :)" ); } ); // Timeout information var ti = setTimeout( function(){ if( !window.jQuery || !window.jQuery.ui )alert( "Timeout !" ); clearTimeout( ti ); }, 5000 ); } )( { // Tools addEvent : function( evnt, elem, func ){ try{ if( elem.addEventListener ){ elem.addEventListener( evnt, func, false ); }else if( elem.attachEvent ){ var r = elem.attachEvent( "on" + evnt, func ); } return true; }catch( e ){ return false; } } }, [ // Scripts "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js", "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" ] ); 

使用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"
   };
}]);

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

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

我目前的解决方案是递归添加<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)
}

这是我的代码

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

几天前我有类似的任务,这就是我做的。
此加载程序在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等。通过执行此操作,您将推迟下一个父脚本的加载,并且将优先考虑子脚本中的队列。

希望能帮助到你 :-)

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

<div id="js">
<!-- script will be inserted here --> 
</div>

...

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM