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

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


Я не з’ясував, як робити посилання на одне і те ж значення, не створюючи масив (для count). В іншому випадку я думаю, що це зрозуміло (коли все завантажується, 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);

Ви не можете зробити це синхронно.

редагувати - вказувалося, що IE, правда у формі, не запускає подію "навантаження" на <script>теги, які завантажуються / оцінюються. Таким чином, я припускаю, що наступним, що слід зробити, було б отримати сценарій за допомогою XMLHttpRequest, а потім eval()його самостійно. (Або, я гадаю, вкладіть текст у <script>доданий вами тег; на середовище виконання eval()впливає локальний обсяг, тому він не обов'язково робитиме те, що ви хочете.)

редагувати - На початку 2013 року я настійно рекомендую зайнятися більш надійним інструментом завантаження скриптів, таким як Requirejs . Є багато особливих випадків, про які слід турбуватися. Для справді простих ситуацій є yepnope , який зараз вбудований в Modernizr .


3
на жаль, це не крос-браузер.
gblazex

69
Справді ?? Хто не запускає подію "завантаження" при завантаженні сценарію? Чекай - не кажи мені.
Пойнті

1
@Pointy Я вирішив цю проблему за допомогою XMLHttpRequest, а потім eval(). Однак налагодження це кошмар b / c повідомлення про помилку повідомляє, що eval()з'являється рядок , а не фактична помилка
puk

3
Але як тоді вимагає це робити ?? Як вони включають багато сценаріїв і запускають їх у правильному порядку?
mmm

4
Звичайно, document.write () - це те, що ви шукаєте. Не гарно, але це працює.
Іржі Ветиська

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>

Те саме можна зробити зі створенням вузла, як це зробив Пойнті, але лише у 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

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 -завантажений /


це справді працює? див. моє запитання: stackoverflow.com/questions/17978255/…
ммм

1
Це виглядає цікаво. Одне питання ... чому потрібно виконувати метод зворотного виклику двічі? (script.onload = зворотний виклик та зворотний виклик (), використані в onreadystatechange)
Клем

1
onreadysteatechange призначений для IE і буде стріляти лише по IE, оскільки завантаження не буде спрацьовувати для IE
Guilherme Ferreira

7

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

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

Гаразд, закріпіть місця, бо це буде нерівна їзда. Все більше людей запитують про динамічне завантаження сценаріїв через javascript, здається, це гаряча тема.

Основними причинами, чому це стало настільки популярним, є:

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

Про модульність : очевидно, що управління залежностями на стороні клієнта повинно здійснюватися безпосередньо на стороні клієнта. Якщо потрібен певний об'єкт, модуль або бібліотека, ми просто запитуємо його та завантажуємо динамічно.

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

Ефективність стала конкурентною перевагою між веб-сайтами, зараз вона є фактором рейтингу пошуку. Динамічні сценарії можуть імітувати асинхронну поведінку, на відміну від способу блокування за замовчуванням способу обробки браузерами сценаріїв. Скрипти блокують інші ресурси, сценарії блокують подальший синтаксичний аналіз документа HTML, сценарії блокують інтерфейс користувача. Тепер за допомогою динамічних тегів сценаріїв та їх альтернативних переглядачів ви можете робити реальні асинхронні запити та виконувати залежний код лише тоді, коли вони доступні. Ваші сценарії завантажуватимуться паралельно навіть з іншими ресурсами, і візуалізація буде бездоганною.

Причина, чому деякі люди дотримуються синхронних сценаріїв, полягає в тому, що вони звикли до цього. Вони думають, що це спосіб за замовчуванням, це простіший спосіб, а деякі можуть навіть подумати, що це єдиний спосіб.

Але єдине, про що ми повинні дбати, коли це потрібно вирішити щодо дизайну додатків, - це досвід кінцевого користувача . І в цій області асинхронних перемогти неможливо. Користувач отримує негайні відповіді (або вимовляє обіцянки), і обіцянка завжди краща, ніж нічого. Порожній екран лякає людей. Розробники не повинні лінуватися покращувати сприйману ефективність .

І наостанок кілька слів про брудну сторону. Що потрібно зробити, щоб він працював у браузерах:

  1. навчитися мислити асинхронно
  2. організуйте свій код таким, щоб він був модульним
  3. упорядкуйте свій код, щоб добре обробляти помилки та крайні випадки
  4. посилювати поступово
  5. завжди дбайте про потрібну кількість відгуків

Дякую, галаме. Думаю, мені слід було бути більш чітким. Я сподівався, що це врешті буде асинхронно. Я просто хочу спосіб доступу до нього, який мав би логічний сенс для програміста. Я хотів уникати таких речей, як: Import ("package.mod1", function () {// робити речі з mod1}); Імпорт ("package.mod2", function () {// робити речі з mod2}); Я подивився ваш скрипт і labjs, і, хоч і приємний, здається, складніший для моїх потреб. Я думав, що може бути простіший спосіб, і хотів уникнути додаткових залежностей.
Джош Джонсон,

1
Ви пропустили суть мого допису. Вся справа в користувачах. Це має бути вашим першочерговим завданням. Все інше - другорядне.
gblazex

2
Галам, дуже хороший момент. Користувацький досвід дуже важливий. Щоб бути зрозумілим, я не готовий жертвувати досвідом користувача АБО якісним кодом, який підтримується. Я збираюся вивчити питання закриття та labjs, щоб побачити, що вони можуть зробити для мене. Але поки що мені може знадобитися дотримуватися тегів <script>. На жаль, я не працюю над цим самостійно. Я працюю із середньою командою розробників, тому підтримуваний код є пріоритетом. Якщо всі не можуть зрозуміти, як ефективно використовувати lib, користувач 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 ([ім'я файлу,' 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);
    }
};

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

Я не з’ясував, як робити посилання на одне і те ж значення, не створюючи масив (для count). В іншому випадку я думаю, що це зрозуміло (коли все завантажується,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' 
]) ;

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 Мені не так пощастило b / c Мені потрібно зробити широке перше завантаження пакетів зі скриптами в кільцях, які завантажуються асинхронно, а скрипти між кільцями завантажуються синхронно
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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.