Динамическая загрузка JavaScript синхронно - PullRequest
57 голосов
/ 21 мая 2010

Я использую шаблон модуля , одна из вещей, которую я хочу сделать, это динамически включить внешний файл 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 
    }
}

Ответы [ 18 ]

53 голосов
/ 21 мая 2010

Существует только один способ синхронно загрузить и выполнить ресурс скрипта, а именно синхронный 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);

Но обычно вы не должны использовать синхронные запросы, так как это заблокирует все остальное. Но при этом, конечно, есть сценарии, где это уместно.

Я бы, вероятно, преобразовал содержащую функцию в асинхронный шаблон, используя обработчик загрузки.

37 голосов
/ 12 февраля 2014

принятый ответ равен НЕ правильно.

Загрузка файла синхронно - это не то же самое, что синхронное выполнение файла - это то, что запросил OP.

Принятый ответ загружает файл синхронизации, но не делает ничего, кроме добавления тега сценария в DOM.Тот факт, что appendChild () возвратился, ни в коем случае не гарантирует, что скрипт завершит выполнение и его члены инициализированы для использования.

Единственный (см. Предостережение) способ решения вопроса OPэто синхронизировать загрузку скрипта через XHR, как указано, затем читать как текст и передавать в eval () или новый вызов Function () и ждать возврата этой функции.Это единственный способ гарантировать, что скрипт загружен И выполняется синхронно.

Я не комментирую, разумно ли это делать с точки зрения пользовательского интерфейса или безопасности, нобезусловно, есть варианты использования, которые оправдывают синхронизацию загрузки и выполнения.

Предупреждение : Если вы не используете веб-работников, в этом случае просто вызовите loadScripts ();

10 голосов
/ 19 июня 2012

Это код, который я использую для загрузки нескольких файлов в моем приложении.

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.
    })
5 голосов
/ 08 августа 2016

Самая похожая на 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
3 голосов
/ 15 февраля 2014

У меня были следующие проблемы с существующими ответами на этот вопрос (и вариации этого вопроса в других потоках 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);
    }
};
2 голосов
/ 16 июля 2014
var xhrObj = new XMLHttpRequest();
xhrObj.open('GET', '/filename.js', false);
xhrObj.send(null);
eval(xhrObj.responseText);

Если это междоменный запрос, он не будет работать.В этом случае вы должны загрузить запрошенный файл на ваш сервер или создать зеркальный php, который выводит его, и потребовать, чтобы php.

с jquery (работает также с междоменным запросом):

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

callbackFunction будет вызываться синхронно.

Для загрузки дополнительных сценариев см. этот поток.

1 голос
/ 20 сентября 2018

принят неправильный ответ:

директива 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');
          })
        })
1 голос
/ 11 июля 2017

На самом деле есть способ загрузить список скриптов и выполнить их синхронно. Вам нужно вставить каждый тег сценария в 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>

Ссылки

1 голос
/ 20 июня 2012

Если вам нужно загрузить произвольное количество сценариев и продолжить работу только после завершения последнего, и вы не можете использовать 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 никогда не отправят событие onreadystatechange с readyState=='complete'; если это так (я не смог воспроизвести эту проблему), то приведенный выше сценарий завершится ошибкой, и ваш обратный вызов никогда не будет вызван.

0 голосов
/ 09 мая 2017

Я могу опоздать с ответом на этот вопрос.

Мое текущее решение состоит в том, чтобы рекурсивно добавлять теги <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)
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...