我正在使用来自 JQuery 的几个插件、自定义小部件和一些其他库。结果我有几个 .js 和 .css 文件。我需要为我的网站创建一个加载器,因为它需要一些时间来加载。如果我可以在导入所有内容之前显示加载器,那就太好了:
<script type="text/javascript" src="js/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="js/myFunctions.js"></script>
<link type="text/css" href="css/main.css" rel="stylesheet" />
...
....
etc
我找到了几个教程,使我能够异步导入 JavaScript 库。例如我可以做类似的事情:
(function () {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'js/jquery-ui-1.8.16.custom.min.js';
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
})();
出于某种原因,当我对所有文件执行相同操作时,页面不起作用。我已经尝试了很长时间,试图找出问题所在,但我就是找不到。首先,我认为这可能是因为某些 javascript 函数依赖于其他函数。但是当一个完成时我使用超时功能以正确的顺序加载它们我继续下一个并且页面仍然表现得很奇怪。例如,我无法点击链接等...虽然动画仍然有效..
无论如何
这就是我一直在想的……我相信浏览器有缓存,这就是为什么第一次加载页面需要很长时间而下一次加载速度很快的原因。所以我想做的是用一个异步加载所有这些文件的页面替换我的 index.html 页面。当 ajax 完成加载所有这些文件时,重定向到我计划使用的页面。使用该页面时,加载时间不会很长,因为文件应该已经包含在浏览器的缓存中。在我的索引页面(.js 和 .css 文件异步加载的页面)上,我不在乎出现错误。完成后我将只显示一个加载器并重定向页面......
这个想法是一个很好的选择吗?还是我应该继续尝试实现异步方法?
编辑
我异步加载所有内容的方式就像:
importScripts();
function importScripts()
{
//import: jquery-ui-1.8.16.custom.min.js
getContent("js/jquery-1.6.2.min.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
//s.async = true;
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext1,1);
});
//import: jquery-ui-1.8.16.custom.min.js
function insertNext1()
{
getContent("js/jquery-ui-1.8.16.custom.min.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext2,1);
});
}
//import: jquery-ui-1.8.16.custom.css
function insertNext2()
{
getContent("css/custom-theme/jquery-ui-1.8.16.custom.css",function (code) {
var s = document.createElement('link');
s.type = 'text/css';
s.rel ="stylesheet";
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext3,1);
});
}
//import: main.css
function insertNext3()
{
getContent("css/main.css",function (code) {
var s = document.createElement('link');
s.type = 'text/css';
s.rel ="stylesheet";
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext4,1);
});
}
//import: jquery.imgpreload.min.js
function insertNext4()
{
getContent("js/farinspace/jquery.imgpreload.min.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext5,1);
});
}
//import: marquee.js
function insertNext5()
{
getContent("js/marquee.js",function (code) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext6,1);
});
}
//import: marquee.css
function insertNext6()
{
getContent("css/marquee.css",function (code) {
var s = document.createElement('link');
s.type = 'text/css';
s.rel ="stylesheet";
s.innerHTML=code;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
setTimeout(insertNext,1);
});
}
function insertNext()
{
setTimeout(pageReadyMan,10);
}
}
// get the content of url and pass that content to specified function
function getContent( url, callBackFunction )
{
// attempt to create the XMLHttpRequest and make the request
try
{
var asyncRequest; // variable to hold XMLHttpRequest object
asyncRequest = new XMLHttpRequest(); // create request object
// register event handler
asyncRequest.onreadystatechange = function(){
stateChange(asyncRequest, callBackFunction);
}
asyncRequest.open( 'GET', url, true ); // prepare the request
asyncRequest.send( null ); // send the request
} // end try
catch ( exception )
{
alert( 'Request failed.' );
} // end catch
} // end function getContent
// call function whith content when ready
function stateChange(asyncRequest, callBackFunction)
{
if ( asyncRequest.readyState == 4 && asyncRequest.status == 200 )
{
callBackFunction(asyncRequest.responseText);
} // end if
} // end function stateChange
奇怪的是所有样式的工作加上所有的javascript函数。该页面由于某种原因被冻结...
异步加载的几个解决方案:
//this function will work cross-browser for loading scripts asynchronously
function loadScript(src, callback)
{
var s,
r,
t;
r = false;
s = document.createElement('script');
s.type = 'text/javascript';
s.src = src;
s.onload = s.onreadystatechange = function() {
//console.log( this.readyState ); //uncomment this line to see which ready states are called.
if ( !r && (!this.readyState || this.readyState == 'complete') )
{
r = true;
callback();
}
};
t = document.getElementsByTagName('script')[0];
t.parentNode.insertBefore(s, t);
}
如果页面上已经有 jQuery,只需使用:
$.getScript(url, successCallback)
*
此外,您的脚本可能在文档加载完成之前就已加载/执行,这意味着您需要等待 document.ready
才能将事件绑定到元素。
如果不查看代码,就无法具体说明您的问题是什么。
最简单的解决方案是将所有脚本内联在页面底部,这样它们就不会在执行时阻止 HTML 内容的加载。它还避免了必须异步加载每个所需脚本的问题。
如果您有一个并不总是使用的特别花哨的交互,它需要某种更大的脚本,那么避免在需要之前加载该特定脚本(延迟加载)可能很有用。
* scripts loaded with $.getScript
will likely not be cached
对于任何可以使用现代功能(例如 Promise
对象)的人来说,loadScript
函数变得非常简单:
function loadScript(src) {
return new Promise(function (resolve, reject) {
var s;
s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
请注意,此版本不再接受 callback
参数,因为返回的承诺将处理回调。以前是 loadScript(src, callback)
现在是 loadScript(src).then(callback)
。
这具有能够检测和处理故障的额外好处,例如,可以调用...
loadScript(cdnSource)
.catch(loadScript.bind(null, localSource))
.then(successCallback, failureCallback);
...它会优雅地处理 CDN 中断。
HTML5 的新 'async' 属性应该可以解决问题。如果您关心 IE,大多数浏览器也支持“延迟”。
异步 - HTML
<script async src="siteScript.js" onload="myInit()"></script>
延迟 - HTML
<script defer src="siteScript.js" onload="myInit()"></script>
在分析新的 AdSense 广告单元代码时,我注意到了该属性,并且搜索将我带到了这里:http://davidwalsh.name/html5-async
async
或 defer
它将在加载时阻止呈现。设置 async
将告诉它不要阻止并尽快运行脚本。设置 defer
将告诉它不要阻塞,而是等待 running 脚本直到页面完成。通常只要脚本没有任何依赖项(例如 jQuery),尽快运行脚本是没有问题的。如果一个脚本依赖于另一个脚本,请使用 onLoad
等待它。
当完成加载的所有脚本将页面重定向到 index2.html 时,我异步加载了脚本(html 5 具有该功能),其中 index2.html 使用相同的库。因为一旦页面重定向到 index2.html,浏览器就会有缓存,因此 index2.html 会在不到一秒的时间内加载,因为它拥有加载页面所需的一切。在我的 index.html 页面中,我还加载了我计划使用的图像,以便浏览器将这些图像放在缓存中。所以我的 index.html 看起来像:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Project Management</title>
<!-- the purpose of this page is to load all the scripts on the browsers cache so that pages can load fast from now on -->
<script type="text/javascript">
function stylesheet(url) {
var s = document.createElement('link');
s.type = 'text/css';
s.async = true;
s.src = url;
var x = document.getElementsByTagName('head')[0];
x.appendChild(s);
}
function script(url) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = url;
var x = document.getElementsByTagName('head')[0];
x.appendChild(s);
}
//load scritps to the catche of browser
(function () {
stylesheet('css/custom-theme/jquery-ui-1.8.16.custom.css');
stylesheet('css/main.css');
stylesheet('css/marquee.css');
stylesheet('css/mainTable.css');
script('js/jquery-ui-1.8.16.custom.min.js');
script('js/jquery-1.6.2.min.js');
script('js/myFunctions.js');
script('js/farinspace/jquery.imgpreload.min.js');
script('js/marquee.js');
})();
</script>
<script type="text/javascript">
// once the page is loaded go to index2.html
window.onload = function () {
document.location = "index2.html";
}
</script>
</head>
<body>
<div id="cover" style="position:fixed; left:0px; top:0px; width:100%; height:100%; background-color:Black; z-index:100;">Loading</div>
<img src="images/home/background.png" />
<img src="images/home/3.png"/>
<img src="images/home/6.jpg"/>
<img src="images/home/4.png"/>
<img src="images/home/5.png"/>
<img src="images/home/8.jpg"/>
<img src="images/home/9.jpg"/>
<img src="images/logo.png"/>
<img src="images/logo.png"/>
<img src="images/theme/contentBorder.png"/>
</body>
</html>
另一个好处是我可以在页面中放置一个加载器,当页面加载完成时,加载器将消失,并在几毫秒内运行新页面。
来自谷歌的例子
<script type="text/javascript">
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/plusone.js?onload=onLoadCallback';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();
</script>
几个注意事项:
s.async = true 对于 HTML5 doctype 不是很正确,正确的是 s.async = 'async' (实际上使用 true 是正确的,感谢 amn 在下面的评论中指出)
使用超时来控制顺序不是很好也不安全,而且你也会让加载时间变大,等于所有超时的总和!
由于最近有一个异步加载文件的原因,但按顺序,我建议在您的示例中采用更多功能驱动的方式(删除 console.log
以供生产使用:)):
(function() {
var prot = ("https:"===document.location.protocol?"https://":"http://");
var scripts = [
"path/to/first.js",
"path/to/second.js",
"path/to/third.js"
];
function completed() { console.log('completed'); } // FIXME: remove logs
function checkStateAndCall(path, callback) {
var _success = false;
return function() {
if (!_success && (!this.readyState || (this.readyState == 'complete'))) {
_success = true;
console.log(path, 'is ready'); // FIXME: remove logs
callback();
}
};
}
function asyncLoadScripts(files) {
function loadNext() { // chain element
if (!files.length) completed();
var path = files.shift();
var scriptElm = document.createElement('script');
scriptElm.type = 'text/javascript';
scriptElm.async = true;
scriptElm.src = prot+path;
scriptElm.onload = scriptElm.onreadystatechange = \
checkStateAndCall(path, loadNext); // load next file in chain when
// this one will be ready
var headElm = document.head || document.getElementsByTagName('head')[0];
headElm.appendChild(scriptElm);
}
loadNext(); // start a chain
}
asyncLoadScripts(scripts);
})();
s.async = true
正确。属性 async
在 W3C 的 HTML 5 建议 w3.org/TR/html5/scripting-1.html#attr-script-async 中明确指定为布尔值。您将 async
属性 与实现 HTMLScriptElement
DOM 接口的对象所公开的 async
属性混淆了。事实上,当通过类似 Element.setAttribute
操作元素的相应属性时,您应该使用 element.setAttribute("async", "async")
,因为所有 HTML 属性首先是文本。
感谢 HTML5,您现在可以通过在标签中添加“async”来声明要异步加载的脚本:
<script async>...</script>
注意: async 属性仅适用于外部脚本(并且仅应在存在 src 属性时使用)。
注意:有几种方法可以执行外部脚本:
如果存在异步:脚本与页面的其余部分异步执行(脚本将在页面继续解析时执行)
如果 async 不存在并且 defer 存在:当页面完成解析时执行脚本
如果 async 或 defer 都不存在:在浏览器继续解析页面之前立即获取并执行脚本
看到这个:http://www.w3schools.com/tags/att_script_async.asp
我将通过检查是否存在回调并允许传递参数来完成 zzzzBov 的回答:
function loadScript(src, callback, args) {
var s, r, t;
r = false;
s = document.createElement('script');
s.type = 'text/javascript';
s.src = src;
if (typeof(callback) === 'function') {
s.onload = s.onreadystatechange = function() {
if (!r && (!this.readyState || this.readyState === 'complete')) {
r = true;
callback.apply(args);
}
};
};
t = document.getElementsByTagName('script')[0];
t.parent.insertBefore(s, t);
}
这是异步脚本加载的一个很好的当代解决方案,尽管它只使用 async false 处理 js 脚本。
www.html5rocks.com - Deep dive into the murky waters of script loading 中有一篇很棒的文章。
在考虑了许多可能的解决方案后,作者得出结论,将 js 脚本添加到 body 元素的末尾是避免 js 脚本阻塞页面渲染的最佳方法。
同时,作者为那些迫切需要异步加载和执行脚本的人添加了另一个很好的替代解决方案。
考虑到您有四个名为 script1.js, script2.js, script3.js, script4.js
的脚本,那么您可以使用 应用 async = false 来完成:
[
'script1.js',
'script2.js',
'script3.js',
'script4.js'
].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
现在,Spec说:一起下载,全部下载后按顺序执行。
Firefox < 3.6,Opera 说:我不知道这个“异步”的东西是什么,但碰巧我按照添加的顺序执行通过 JS 添加的脚本。
Safari 5.0 说:我理解“异步”,但不理解用 JS 将其设置为“false”。我会在你的脚本落地后立即执行,无论顺序如何。
IE < 10 说:不知道“异步”,但是有一个使用“onreadystatechange”的解决方法。
其他一切都在说:我是你的朋友,我们将照本宣科。
现在,使用 IE < 10 解决方法的完整代码:
var scripts = [
'script1.js',
'script2.js',
'script3.js',
'script4.js'
];
var src;
var script;
var pendingScripts = [];
var firstScript = document.scripts[0];
// Watch scripts load in IE
function stateChange() {
// Execute as many scripts in order as we can
var pendingScript;
while (pendingScripts[0] && ( pendingScripts[0].readyState == 'loaded' || pendingScripts[0].readyState == 'complete' ) ) {
pendingScript = pendingScripts.shift();
// avoid future loading events from this script (eg, if src changes)
pendingScript.onreadystatechange = null;
// can't just appendChild, old IE bug if element isn't closed
firstScript.parentNode.insertBefore(pendingScript, firstScript);
}
}
// loop through our script urls
while (src = scripts.shift()) {
if ('async' in firstScript) { // modern browsers
script = document.createElement('script');
script.async = false;
script.src = src;
document.head.appendChild(script);
}
else if (firstScript.readyState) { // IE<10
// create a script and add it to our todo pile
script = document.createElement('script');
pendingScripts.push(script);
// listen for state changes
script.onreadystatechange = stateChange;
// must set src AFTER adding onreadystatechange listener
// else we’ll miss the loaded event for cached scripts
script.src = src;
}
else { // fall back to defer
document.write('<script src="' + src + '" defer></'+'script>');
}
}
我写了一个小帖子来帮助解决这个问题,您可以在此处阅读更多内容https://timber.io/snippets/asynchronously-load-a-script-in-the-browser-with-javascript/,但我在下面附上了帮助程序类。它将自动等待脚本加载并返回指定的窗口属性。
export default class ScriptLoader {
constructor (options) {
const { src, global, protocol = document.location.protocol } = options
this.src = src
this.global = global
this.protocol = protocol
this.isLoaded = false
}
loadScript () {
return new Promise((resolve, reject) => {
// Create script element and set attributes
const script = document.createElement('script')
script.type = 'text/javascript'
script.async = true
script.src = `${this.protocol}//${this.src}`
// Append the script to the DOM
const el = document.getElementsByTagName('script')[0]
el.parentNode.insertBefore(script, el)
// Resolve the promise once the script is loaded
script.addEventListener('load', () => {
this.isLoaded = true
resolve(script)
})
// Catch any errors while loading the script
script.addEventListener('error', () => {
reject(new Error(`${this.src} failed to load.`))
})
})
}
load () {
return new Promise(async (resolve, reject) => {
if (!this.isLoaded) {
try {
await this.loadScript()
resolve(window[this.global])
} catch (e) {
reject(e)
}
} else {
resolve(window[this.global])
}
})
}
}
用法是这样的:
const loader = new Loader({
src: 'cdn.segment.com/analytics.js',
global: 'Segment',
})
// scriptToLoad will now be a reference to `window.Segment`
const scriptToLoad = await loader.load()
对于 HTML5,您可以使用“预取”
<link rel="prefetch" href="/style.css" as="style" />
看看 js 的“预加载”。
<link rel="preload" href="used-later.js" as="script">
脚本加载速度如此缓慢的一个原因是,如果您在加载页面时正在运行所有脚本,如下所示:
callMyFunctions();
代替:
$(window).load(function() {
callMyFunctions();
});
这第二段脚本会等到浏览器完全加载完所有的 Javascript 代码后才开始执行任何脚本,让用户觉得页面加载得更快了。
如果您希望通过减少加载时间来增强用户体验,我不会选择“加载屏幕”选项。在我看来,这比让页面加载更慢更烦人。
我建议您看看 Modernizr。它是一个小型轻量级库,您可以异步加载您的 javascript,其功能允许您检查文件是否已加载并在您指定的另一个中执行脚本。
下面是一个加载jquery的例子:
Modernizr.load([
{
load: '//ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.js',
complete: function () {
if ( !window.jQuery ) {
Modernizr.load('js/libs/jquery-1.6.1.min.js');
}
}
},
{
// This will wait for the fallback to load and
// execute if it needs to.
load: 'needs-jQuery.js'
}
]);
您可能会觉得这篇 wiki 文章很有趣:http://ajaxpatterns.org/On-Demand_Javascript
它解释了如何以及何时使用这种技术。
好吧,x.parentNode
返回 HEAD 元素,因此您将脚本插入到 head 标记之前。也许这就是问题所在。
请改用 x.parentNode.appendChild()
。
看看这个https://github.com/stephen-lazarionok/async-resource-loader。它有一个示例展示了如何一次加载 JS、CSS 和多个文件。
您是否考虑过使用 Fetch 注入?我推出了一个名为 fetch-inject 的开源库来处理此类情况。这是您的加载器使用 lib 时的样子:
fetcInject([
'js/jquery-1.6.2.min.js',
'js/marquee.js',
'css/marquee.css',
'css/custom-theme/jquery-ui-1.8.16.custom.css',
'css/main.css'
]).then(() => {
'js/jquery-ui-1.8.16.custom.min.js',
'js/farinspace/jquery.imgpreload.min.js'
})
为了向后兼容,利用功能检测并回退到 XHR 注入或脚本 DOM 元素,或者使用 document.write
将标签内联到页面中。
这是我消除渲染阻塞 JavaScript 的自定义解决方案:
// put all your JS files here, in correct order
const libs = {
"jquery": "https://code.jquery.com/jquery-2.1.4.min.js",
"bxSlider": "https://cdnjs.cloudflare.com/ajax/libs/bxslider/4.2.5/jquery.bxslider.min.js",
"angular": "https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0-beta.2/angular.min.js",
"ngAnimate": "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0-beta.2/angular-animate.min.js"
}
const loadedLibs = {}
let counter = 0
const loadAsync = function(lib) {
var http = new XMLHttpRequest()
http.open("GET", libs[lib], true)
http.onload = () => {
loadedLibs[lib] = http.responseText
if (++counter == Object.keys(libs).length) startScripts()
}
http.send()
}
const startScripts = function() {
for (var lib in libs) eval(loadedLibs[lib])
console.log("allLoaded")
}
for (var lib in libs) loadAsync(lib)
简而言之,它异步加载所有脚本,然后执行它们。
Github 存储库:https://github.com/mudroljub/js-async-loader
No 'Access-Control-Allow-Origin' header is present on the requested resource
例如,如果有人想在 React 中使用它,这里有一个小的 ES6 函数
import {uniqueId} from 'lodash' // 可选 /** * @param {String} file 要加载的文件的路径。 * @param {Function} 回调(可选)脚本加载时调用的函数。 * @param {String} id(可选)您要加载的文件的唯一 ID。 */ export const loadAsyncScript = (file, callback, id) => { const d = document if (!id) { id = uniqueId('async_script') } // 可选 if (!d.getElementById(id)) { const tag = 'script' let newScript = d.createElement(tag) let firstScript = d.getElementsByTagName(tag)[0] newScript.id = id newScript.async = true newScript.src = file if (callback) { // IE 支持newScript.onreadystatechange = () => { if (newScript.readyState === 'loaded' || newScript.readyState === 'complete') { newScript.onreadystatechange = null callback(file) } } // 其他(非IE) 浏览器支持 newScript.onload = () => { callback(file) } } firstScript.parentNode.insertBefore(newScript, firstScript) } else { console.error(`id ${id} 的脚本已经加载`) } }
const dynamicScriptLoading = async (src) =>
{
const response = await fetch(src);
const dataResponse = await response.text();
eval.apply(null, [dataResponse]);
}
我建议先考虑缩小文件,看看这是否能给你带来足够大的速度提升。如果您的主机速度很慢,可以尝试将静态内容放在 CDN 上。
不定期副业成功案例分享
document.head.appendChild
,但是将脚本添加到<head>
存在一些小众问题;没有什么会阻止脚本加载和执行。document.write
编写附加脚本标记,则该脚本将作为加载页面的一部分同步执行,并且不会单独包含任何异步回调行为。如果您正在创建脚本元素并将其添加到页面,那么您将有权添加事件侦听器以异步触发。 tl;dr: 这取决于您如何使用 JS 加载脚本。