document.createElement («скрипт») синхронно

81

Можно ли вызвать .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()каждый файл в указанном порядке, в противном случае просто сохраните ответ).
Кан Рофинги

Ответы:

134

Вы можете создать свой <script>элемент с помощью обработчика «onload», который будет вызываться, когда скрипт будет загружен и оценен браузером.

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 не запускает событие «load» для <script>загружаемых / оцениваемых тегов. Таким образом, я полагаю, что следующее, что нужно сделать, это получить сценарий с помощью XMLHttpRequest, а затем eval()его самостоятельно. (Или, я полагаю, поместите текст в <script>тег, который вы добавляете; среда выполнения eval()зависит от локальной области видимости, поэтому она не обязательно будет делать то, что вы хотите.)

edit - По состоянию на начало 2013 года я настоятельно рекомендовал бы изучить более надежный инструмент загрузки скриптов, такой как Requirejs . Есть много особых случаев, о которых нужно беспокоиться. Для действительно простых ситуаций есть yepnope , который теперь встроен в Modernizr .

Заостренный
источник
3
к сожалению, это не кроссбраузерность.
gblazex
69
В самом деле?? Кто не запускает событие "load" при загрузке скрипта? Подожди - не говори мне.
Pointy
1
@Pointy Я решил эту проблему, используя XMLHttpRequest, а затем eval(). Однако отладка - это кошмар, потому что в сообщении об ошибке eval()появляется строка , а не сама ошибка
puk
3
Но как же тогда requirejs это делает ?? Как они включают много скриптов и запускают их в правильном порядке?
ммм
4
Конечно, вы ищете document.write (). Не очень красиво, но работает.
Jiri Vetyska
26

Это некрасиво, но работает:

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

Скрипт должен быть включен либо в отдельный <script>тег, либо раньше 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().

Джош Джонсон
источник
Вы уверены, что ваш первый фрагмент кода работает во всех браузерах?
Богдан Гусиев
@BogdanGusiev Я не уверен на 100%. Я тестировал IE 8, (текущие версии) Firefox и Chrome. Скорее всего, это не будет работать с типами документов XHTML, которые служат типом содержимого application/xhtml+xml.
Джош Джонсон
1
К сожалению, теги скриптов нельзя использовать в файлах JS.
Clem
@Clem Ты мог бы сделать document.write("<SCR" + "IPT>" + "...").
Джон Вайс
Это хорошая альтернатива для скриптов, внутри <head>которых загружаются несколько других зависимостей (или частные файлы).
alecov 01
18

Это уже поздно, но в будущем для тех, кто хотел бы это сделать, вы можете использовать следующее:

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 -гружено /

zcourts
источник
это действительно работает? см. мой вопрос: stackoverflow.com/questions/17978255/…
ммм
1
Это выглядит интересно. Один вопрос ... зачем нужно дважды выполнять метод обратного вызова? (script.onload = callback и callback (), используемые в onreadystatechange)
Клем,
1
onreadysteatechange предназначен для IE и будет запускаться только в IE, поскольку загрузка не будет запускаться для IE
Гильерме Феррейра
7

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

Дуглас Крокфорд ( блог YUI )

Хорошо, пристегни свои сиденья, потому что это будет ухабистая поездка. Все больше и больше людей спрашивают о динамической загрузке скриптов через javascript, кажется, это горячая тема.

Основные причины, по которым это стало таким популярным:

  • модульность на стороне клиента
  • более легкое управление зависимостями
  • обработка ошибок
  • преимущества производительности

О модульности : очевидно, что управление зависимостями на стороне клиента должно осуществляться прямо на стороне клиента. Если требуется определенный объект, модуль или библиотека, мы просто запрашиваем его и загружаем динамически.

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

Производительность стала конкурентным преимуществом между веб-сайтами, теперь это фактор ранжирования в поиске. Что могут делать динамические сценарии, так это имитировать асинхронное поведение в отличие от способа блокировки по умолчанию того, как браузеры обрабатывают сценарии. Скрипты блокируют другие ресурсы, скрипты блокируют дальнейший анализ HTML-документа, скрипты блокируют пользовательский интерфейс. Теперь с помощью динамических тегов сценария и его кросс-браузерных альтернатив вы можете выполнять реальные асинхронные запросы и выполнять зависимый код только тогда, когда они доступны. Ваши скрипты будут загружаться параллельно даже с другими ресурсами, и рендеринг будет безупречным.

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

Но единственное, о чем мы должны заботиться, когда нужно решить этот вопрос относительно дизайна приложения, - это опыт конечного пользователя . И в этой области асинхронность обыграть невозможно. Пользователь получает немедленные ответы (или произносит обещания), а обещание всегда лучше, чем ничего. Пустой экран пугает людей. Разработчики не должны лениться, чтобы улучшить воспринимаемую производительность .

И напоследок несколько слов о грязной стороне. Что вам нужно сделать, чтобы он работал в разных браузерах:

  1. научиться думать асинхронно
  2. сделайте свой код модульным
  3. организовать свой код, чтобы хорошо обрабатывать ошибки и крайние случаи
  4. прогрессивно улучшать
  5. всегда заботиться о правильном количестве отзывов
gblazex
источник
Спасибо, Галам. Думаю, мне следовало быть более ясным. Я действительно ожидал, что в конце концов это будет асинхронно. Мне просто нужен способ доступа к нему, который имел бы логический смысл для программиста. Я хотел избежать таких вещей, как: Import ("package.mod1", function () {// что-то делать с mod1}); Import ("package.mod2", function () {// что-то делать с mod2}); Я взглянул на ваш скрипт и labjs, и, хоть и приятный, но, похоже, более сложный для моих нужд. Я подумал, что может быть более простой способ, и хотел избежать дополнительных зависимостей.
Джош Джонсон
1
Вы упустили суть моего поста. Все дело в пользователях. Это должно быть вашим первым приоритетом. Все остальное вторично.
gblazex
2
Галам, очень хорошее замечание. Пользовательский опыт очень важен. Чтобы было ясно, я не хочу жертвовать пользовательским интерфейсом ИЛИ качеством, поддерживаемым кодом. Я собираюсь изучить closure и labjs, чтобы увидеть, что они могут для меня сделать. Но пока мне, возможно, придется придерживаться тегов <script>. К сожалению, я не работаю над этим один. Я работаю с командой разработчиков среднего размера, поэтому поддерживаемый код является приоритетом. Если каждый не может понять, как эффективно использовать библиотеку, то пользовательский exp уходит прямо в окно. Обратные вызовы интуитивно понятны. Обратный вызов, потому что вы импортировали пакет, нет.
Джош Джонсон
Опять же, для ясности, «синхронный» был плохим выбором слов, чтобы выразить мою точку зрения. Я не хочу, чтобы браузер завис во время загрузки.
Джош Джонсон
1
Что делать, если вам нужна синхронная загрузка? Если вам действительно нужно заблокировать, чтобы сохранить пользовательский опыт. Если вы используете систему тестирования A / B или MVT на основе JavaScript. Как вы хотите асинхронно загружать контент и заменять значение по умолчанию, не получая эффекта мерцания, разрушающего взаимодействие с пользователем? Я открыт для предложений. У меня более 500 коллег, которые хотели бы узнать решение этой проблемы. Если у вас его нет, пожалуйста, не используйте выражения вроде «Синхронное программирование неуважительно и не должно использоваться в приложениях, которые используются людьми».
transilvlad
6

Приведенные выше ответы указали мне правильное направление. Вот общая версия того, что у меня получилось:

  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()называется?
Джош Джонсон
1
@JoshJohnson script.addEventListener('load', postLoadFunction);означает, что функция postLoadFunction вызывается при загрузке скрипта.
Эрик
4

У меня возникли следующие проблемы с существующими ответами на этот вопрос (и варианты этого вопроса в других потоках stackoverflow):

  • Ни один из загруженных кодов не поддавался отладке
  • Многие решения требуют, чтобы обратные вызовы знали, когда загрузка была завершена, вместо того, чтобы действительно блокировать, что означает, что я получал бы ошибки выполнения при немедленном вызове загруженного (то есть загружающего) кода.

Или, чуть точнее:

  • Ни один из загруженных кодов не поддавался отладке (кроме блока тега HTML-скрипта, если и только если решение добавляло элементы скрипта в дом, и никогда как отдельные скрипты, доступные для просмотра.) => Учитывая, сколько скриптов мне нужно загрузить ( и отладка), это было недопустимо.
  • Решения, использующие события 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);
    }
};
Jeremykentbgross
источник
4
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');
  })
  */
}
Дагги Бланкс - Дуглас Мванги
источник
1

Я привык к тому, что на моем веб-сайте есть несколько файлов .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' 
]) ;
user1251840
источник
0

Как ни странно, у меня есть то, что вы хотите, но я хочу чего-то более близкого к тому, что было у вас.

Я загружаю вещи динамически и асинхронно, но с таким loadобратным вызовом (используя dojo и xmlhtpprequest)

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

Более подробное объяснение см. Здесь

Проблема в том, что где-то в этой строке код искажается, и если с вашим кодом что-то не так, в console.error(errorMessage);инструкции будет указана строка, где eval()находится, а не фактическая ошибка. Это ТАКАЯ большая проблема, которую я пытаюсь преобразовать обратно в <script>операторы (см. Здесь .

пук
источник
Забавный факт: я тоже вернулся к <script>тегам и использовал соглашения (вместе с некоторыми пакетами сборки), чтобы просто упаковать свои js таким образом, чтобы это имело смысл.
Джош Джонсон,
@JoshJohnson Я не повезло б / с , что нужно сделать в ширину первой загрузки пакетов с помощью скриптов внутри колец загружаются асинхронно и сценарии между кольцами загружается синхронно
Puk
Мне повезло, и я кое-что придумал. Я не завидую твоему положению.
Джош Джонсон
0

Это работает для современных «вечнозеленых» браузеров, поддерживающих async / await и fetch .

Этот пример упрощен без обработки ошибок, чтобы показать основные принципы работы.

// 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. для каждого скрипта он отправляет a new HTTP request, 2. Это также не будет проверять зависимости между ними, 3. Не все браузеры поддерживают async/awaitи 4. Производительность, если она утомительна, тогда нормальная. Было бы хорошо добавить это вhead
santosh