CreateElement (“ script”)同步

是否可以同步调用 .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');

我只是想避免过多地了解内部原理,并且能够说,“我希望使用这个模块,现在我将使用它的一些代码。”

303276 次浏览

This looks like a decent overview of dynamic script loading: http://unixpapa.com/js/dyna.html

You can create your <script> element with an "onload" handler, and that will be called when the script has been loaded and evaluated by the browser.

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

You can't do it synchronously.

edit — it's been pointed out that, true to form, IE doesn't fire a "load" event on <script> tags being loaded/evaluated. Thus I suppose the next thing to do would be to fetch the script with an XMLHttpRequest and then eval() it yourself. (Or, I suppose, stuff the text into a <script> tag you add; the execution environment of eval() is affected by the local scope, so it won't necessarily do what you want it to do.)

editAs of early 2013, I'd strongly advise looking into a more robust script loading tool like Requirejs. There are a lot of special cases to worry about. For really simple situations, there's yepnope, which is now built into Modernizr.

Asynchronous programming is slightly more complicated because the consequence of making a request is encapsulated in a function instead of following the request statement. But the realtime behavior that the user experiences can be significantly better because they will not see a sluggish server or sluggish network cause the browser to act as though it had crashed. Synchronous programming is disrespectful and should not be employed in applications which are used by people.

Douglas Crockford (YUI Blog)

Alright, buckle your seats, because it's going to be a bumpy ride. More and more people ask about loading scripts dynamically via javascript, it seems to be a hot topic.

The main reasons why this became so popular are:

  • client-side modularity
  • easier dependency management
  • error handling
  • performance advantages

About modularity: it is obvious that managing client-side dependencies should be handled right on the client-side. If a certain object, module or library is needed we just ask for it and load it dynamically.

Error handling: if a resource fails we still get the chance to block only the parts that depend on the affected script, or maybe even give it another try with some delay.

Performance has become a competitive edge between websites, it is now a search ranking factor. What dynamic scripts can do is mimic asynchronous behavior as opposed to the default blocking way of how browsers handle scripts. Scripts block other resources, scripts block further parsing of the HTML document, scripts block the UI. Now with dynamic script tags and its cross-browser alternatives you can do real asynchronous requests, and execute dependent code only when they are available. Your scripts will load in-parallel even with other resources and the rendering will be flawless.

The reason why some people stick to synchronous scripting is because they are used to it. They think it is the default way, it is the easier way, and some may even think it is the only way.

But the only thing we should care about when this needs to be decided concerning an applications's design is the end-user experience. And in this area asynchronous cannot be beaten. The user gets immediate responses (or say promises), and a promise is always better than nothing. A blank screen scares people. Developers shouldn't be lazy to enhance perceived performance.

And finally some words about the dirty side. What you should do in order to get it working across browsers:

  1. learn to think asynchronously
  2. organize your code to be modular
  3. organize your code to handle errors and edge cases well
  4. enhance progressively
  5. always take care of the right amount of feedback

This isn't pretty, but it works:

<script type="text/javascript">
document.write('<script type="text/javascript" src="other.js"></script>');
</script>


<script type="text/javascript">
functionFromOther();
</script>

Or

<script type="text/javascript">
document.write('<script type="text/javascript" src="other.js"></script>');
window.onload = function() {
functionFromOther();
};
</script>

The script must be included either in a separate <script> tag or before window.onload().

This will not work:

<script type="text/javascript">
document.write('<script type="text/javascript" src="other.js"></script>');
functionFromOther(); // Error
</script>

The same can be done with creating a node, as Pointy did, but only in FF. You have no guarantee when the script will be ready in other browsers.

Being an XML Purist I really hate this. But it does work predictably. You could easily wrap those ugly document.write()s so you don't have to look at them. You could even do tests and create a node and append it then fall back on document.write().

Ironically, I have what you want, but want something closer to what you had.

I am loading things in dynamically and asynchronously, but with an load callback like so (using dojo and xmlhtpprequest)

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

For a more detailed explanation, see here

The problem is that somewhere along the line the code gets evaled, and if there's anything wrong with your code, the console.error(errorMessage); statement will indicate the line where eval() is, not the actual error. This is SUCH a big problem that I am actually trying to convert back to <script> statements (see here.

This is way late but for future reference to anyone who'd like to do this, you can use the following:

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

I did a short blog post on it some time ago http://crlog.info/2011/10/06/dynamically-requireinclude-a-javascript-file-into-a-page-and-be-notified-when-its-loaded/

I am used to having multiple .js files on my web site that depend one on another. To load them and ensure that the dependencies are evaluated in the right order, I have written a function that loads all the files and then, once they are all received, eval() them. The main drawback is that since this does not work with CDN. For such libraries (e.g., jQuery) it is better to include them statically. Note that inserting script nodes in the HTML dynamically won't guarantee that scripts are evaluated in the right order, at least not in Chrome (this was the major reason for writing this function).

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

I haven't figured out how to make references to the same value without creating an array (for count). Otherwise I think it is self-explanatory (when everything is loaded, eval() every file in the order given, otherwise just store the response).

Usage example:

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

I had the following problem(s) with the existing answers to this question (and variations of this question on other stackoverflow threads):

  • None of the loaded code was debuggable
  • Many of the solutions required callbacks to know when loading was finished instead of truly blocking, meaning I would get execution errors from immediately calling loaded (ie loading) code.

Or, slightly more accurately:

  • None of the loaded code was debuggable (except from the HTML script tag block, if and only if the solution added a script elements to the dom, and never ever as individual viewable scripts.) => Given how many scripts I have to load (and debug), this was unacceptable.
  • Solutions using 'onreadystatechange' or 'onload' events failed to block, which was a big problem since the code originally loaded dynamic scripts synchronously using 'require([filename, 'dojo/domReady']);' and I was stripping out dojo.

My final solution, which loads the script before returning, AND has all scripts properly accessible in the debugger (for Chrome at least) is as follows:

WARNING: The following code should PROBABLY be used only in 'development' mode. (For 'release' mode I recommend prepackaging and minification WITHOUT dynamic script loading, or at least without 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);
}
};

The answers above pointed me in the right direction. Here is a generic version of what I got working:

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

This works for modern 'evergreen' browsers that support async/await and fetch.

This example is simplified, without error handling, to show the basic principals at work.

// 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'
] )

// ...


await import_script('https://cdnjs.cloudflare.com/ajax/libs/...js');


async function import_script(url) {


const script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;




document.head.appendChild(script);


console.log(`import ${url} ...`);


await new Promise((resolve) => script.onload = resolve);
}