先来看这行代码:
<script src = "allMyClientSideCode.js"></script>
这有点儿……不怎样样。“这该放在哪儿?”开发人员会奇怪,“靠上点,放到<head>
标签里?还是靠下点,放到<body>
标签里?”这两种做法都会让富脚本站点的下场很凄惨。<head>
标签里的大脚本会滞压所有页面渲染工作,使得用户在脚本加载终了之前1直处于“白屏死机”状态。而<body>
标签末尾的大脚本只会让用户看到毫无生命力的静态页面,本来应当进行客户端渲染的地方却散布着不起作用
的控件和空空如也的方框。
完善解决这个问题需要对脚本分而治之:那些负责让页面更好看、更好用的脚本应当立即加载,而那些可以待会儿再加载的脚本稍后再加载。但是怎样才能既滞压这些脚本,又能保证它们在被调用时的可用性呢?
<script>
标签的再认识现代阅读器中的<script>
标签分成了两种新类型:经典型和非阻塞型。接下来讨论如何应用这两种标签来尽快加载页面。
1、阻塞型脚本何去何从?
标准版本的<script>
标签常常被称作阻塞型标签。这个词必须放在上下文中进行理解:现代阅读器看到阻塞型<script>
标签时,会跳过阻塞点继续读取文档及下载其他资源(脚本和样式表)。但直到脚本下载终了并运行以后,阅读器才会评估阻塞点以后的那些资源。因此,如果网页文档的<head>
标签里有5 个阻塞型<script>
标签,则在所有这5 个脚本均下载终了并运行之前,用户除页面标题以外看不到任何东西。不但如此,即使这些脚本运行了,它们也只能看到阻塞点之前的那部份文档。如果想看到<body>
标签中正等待加载的那些好东西,就必须给像document.onreadystatechange 这样的事件绑定1个事件处理器。
基于上述缘由,现在愈来愈流行把脚本放在页面<body>
标签的尾部。这样,1方面用户可以更快地看到页面,另外一方面脚本也能够主动密切接触DOM 而无需等待事件来触发自己。对大多数脚本而言,这次“搬家”是个巨大的进步。
但并不是所有脚本都1样。在向下搬动脚本之前,请先问自己2 个问题。
该脚本是不是有可能被<body>
标签里的内联JavaScript 直接调用?答案可能1目了然,但仍值得核对1遍。
该脚本是不是会影响已渲染页面的外观?Typekit 宿主字体就是1个例子。如果把Typekit 脚本放在文档末尾,那末页面文本就会渲染两次,即读取文档时即刻渲染,脚本运行时再次渲染。
上述问题只要有1个答案是肯定的,那末该脚本就应当放在<head>
标签中,否则就能够放在<body>
标签中,文档形如:
<html>
<head>
<!--metadata and stylesheets go here -->
<script src="headScripts.js"></scripts>
</head>
<body>
<!-- content goes here -->
<script src="bodyScripts.js"></script>
</body>
</html>
这确切大大缩短了加载时间,但要注意1点,这可能让用户有机会在加载bodyScripts.js 之前与页面交互。
2、 脚本的提早加载与延迟运行
上面建议将大多数脚本放在<body>
中,由于这样既能让用户更快地看到网页,又能避免操控DOM之前绑定“就绪”事件的开消。但这类方式也有1个缺点,即阅读器在加载完全个文档之前没法加载这些脚本,这对那些通过慢速连接传送的大型文档来讲会是1大瓶颈。
理想情况下,脚本的加载应当与文档的加载同时进行,并且不影响DOM 的渲染。这样,1旦文档就绪就能够运行脚本,由于已依照<script>
标签的次序加载了相应脚本。
如果大家已读到这里了,那末1定会迫不及待地想写1个自定义Ajax 脚本加载器以满足这样的需求!不过,大多数阅读器都支持1个更加简单的解决方案。
<script defer src = "deferredScript.js">
添加defer(延迟)属性相当于对阅读器说:“请马上开始加载这个脚本吧,但是,请等到文档就绪且所有此前具有defer 属性的脚本都结束运行以后再运行它。”在文档<head>
标签里放入延迟脚本,既能带来脚本置于<body>
标签时的全部好处,又能让大文档的加载速度大幅提升!
不足的地方就是,并不是所有阅读器都支持defer属性。这意味着,如果想确保自己的延迟脚本能在文档加载后运行,就必须将所有延迟脚本的代码都封装在诸如jQuery 之$(document).ready 之类的结构中。
上1节的页面例子改进以下:
<html>
<head>
<!-- metadata and stylesheets go here -->
<script src="headScripts.js"></scripts>
<script defer src="http://www.wfuyu.com/upload/caiji/20160601/deferredScripts.js"></script>
</head>
<body>
<!-- content goes here -->
</body>
</html>
请记住deferredScripts 的封装很重要,这样即便阅读器不支持defer,deferredScripts 也会在文档就绪事件以后才运行。如果页面主体内容远远超过几千字节,那末付出这点代价是完全值得的。
3、 脚本的并行加载
如果你是琐屑较量到毫秒级页面加载时间的完善主义者,那末defer或许就像是淡而无味的薄盐酱油。你可不想1直等到此前所有的defer 脚本都运行结束,固然也肯定不想等到文档就绪以后才运行这些脚本,你就是想尽快加载并且尽快运行这些脚本。这也正是现代阅读器提供了async(异步)属性的缘由。
<script async src = "speedyGonzales.js">
<script async src = "roadRunner.js">
如果说defer 让我们想到1种静静等待文档加载的有序排队场景,那末async 就会让我们想到混乱的无政府状态。前面给出的那两个脚本会以任意次序运行,而且只要JavaScript 引擎可用就会立即运行,而不论文档就绪与否。
对大多数脚本来讲,async 是1块难以下咽的鸡肋。async 不像defer那样得到广泛的支持。同时,由于异步脚本会在任意时刻运行,它实在太容易引发海森堡蚁虫之灾了(脚本恰好结束加载时就会蚁虫4起)。
当我们加载1些第3方脚本,而且也不在意它们谁先运行谁后运行。因此,对这些第3方脚本使用async 属性,相当于1分钱没花就提升了它们的运行速度。
上1个页面示例再添加两个独立的第3方小部件,得到的结果以下:
<html>
<head>
<!-- metadata and stylesheets go here -->
<script src="headScripts.js"></scripts>
<script src="http://www.wfuyu.com/upload/caiji/20160601/deferredScripts.js" defer></script>
</head>
<body>
<!-- content goes here -->
<script async defer src="feedbackWidget.js"></script>
<script async defer src="chatWidget.js"></script>
</body>
</html>
这个页面结构清晰展现了脚本的优先次序。对绝大多数阅读器,DOM的渲染只会延迟至headScripts.js 结束运行时。进行DOM渲染的同时会在后台加载deferredScripts.js。接着,在DOM 渲染结束时将运行deferredScripts.js 和那两个小部件脚本。这两个小部件脚本在那些支持async 的阅读器中会做无序运行。如果不肯定这是不是妥当,请勿使用async!
虽然<script>
标签简单得使人心动,但有些情况确切需要更精致的脚本加载方式。我们可能只想给那些满足1定条件的用户加载某个脚本,比方白金会员或到达1定级别的玩家,也可能只想当用户单击激活时才加载某个特性,比方聊天小部件。
1、直接加载脚本
我们可以用类似下面这样的代码来插入<script>
标签。
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = '/js/feature.js';
head.appendChild(script);
稍等,我们如何才能知道脚本什么时候加载结束呢?我们可以给脚本本身添加1些代码以触发事件,但如果要为每一个待加载脚本都添加这样的代码,那也太闹心了。或是另外1种情况,即我们不可能给第3方服务器上的脚本添加这样的代码。HTML5 规范定义了1个可以绑定回调的onload 属性。
script.onload = function() {
// 现在可以调用脚本里定义的函数了
};
不过, IE8 及更老的版本其实不支持onload , 它们支持的是onreadystatechange。某些阅读器在插入<script>
标签时还会出现1些“灵异事件”。而且,这里乃至还没谈到毛病处理呢!为了不
所有这些使人头疼的问题,在此强烈建议使用脚本加载库。
yepnope是1个简单的、轻量级的脚本加载库(紧缩后的精简版只有1.7KB),其设计目标就是真诚服务于最多见的动态脚本加载需求。
yepnope 最简单的用法是,加载脚本并对脚本完成运行这1事件返回1个回调。
yepnope({
load: 'oompaLoompas.js',
callback: function() {
console.log('oompa-Loompas ready!');
}
});
还是无动于中?下面我们要用yepnope 来并行加载多个脚本并按给定次序运行它们。举个例子,假定我们想加载Backbone.js,而这个脚本又依赖于Underscore.js。为此,我们只需用数组情势提供这两个脚本的位置作为加载参数。
yepnope({
load: ['underscore.js', 'backbone.js'],
complete: function() {
// 这里是Backbone 的业务逻辑
}
});
请注意,这里使用了complete(完成)而不是callback(回调)。
其差别在于,脚本加载列表中的每一个资源均会运行callback,而只有当所有脚本都加载完成后才会运行complete。yepnope 的标志性特点是条件加载。给定test 参数,yepnope 会根据该参数值是不是为真而加载不同的资源。举个例子,可以以1定的准确度判断用户是不是在用触摸屏装备,从而据此相应地加载不同的样式表及脚本。
yepnope({
test: Modernizr.touch,
yep: ['touchStyles.css', 'touchApplication.js'],
nope: ['mouseStyles.css', 'mouseApplication.js'],
complete: function() {
// 不论是哪种情况,利用程序均已就绪!
}
});
我们只用寥寥几行代码就搭好了舞台,可以基于用户的接入装备而给他们完全不同的使用体验。固然,不是所有的条件加载都需要备齐yep(是)和nope(否)这两种测试结果。yepnope 最多见的用法之1就是加载垫片脚本以弥补老式阅读器缺失的功能。
yepnope({
test: window.json,nope: ['json2.js'],
complete: function() {
// 现在可以放心肠用JSON 了
}
});
页面使用了yepnope 以后应当变成下面这类漂亮的标记结构:
<html>
<head>
<!-- metadata and stylesheets go here -->
<script src="headScripts.js"></scripts>
<script src="http://www.wfuyu.com/upload/caiji/20160601/deferredScripts.js" defer></script>
</head>
<body>
<!-- content goes here -->
</body>
</html>
很眼熟?这个结构和讨论defer 属性那1节给出的结构1样,唯1的区分是这里的某个脚本文件已拼接了yepnope.js(极可能就在deferredScripts.js 的顶部),这样就能够独立地加载那些根据条件再加载的脚本(由于阅读器需要垫片脚本)和那些想要动态加载的脚本(以便回利用户的动作)。结果将是1个更小巧的deferredScripts.js。
开发人员想通过脚本加载器让混乱不堪的富脚本利用变得更规整有序1些,而Require.js 就是这样1种选择。Require.js 这个强大的工具包能够自动和AMD技术1起捋顺哪怕最复杂的脚本依赖图。
现在先来看1个用到Require.js 同名函数的简单脚本加载示例。
require(['moment'], function(moment) {
console.log(moment().format('dddd')); // 星期几
});
require 函数接受1个由模块名称构成的数组,然后并行地加载所有这些脚本模块。与yepnope 不同,Require.js 不会保证按顺序运行目标脚本,只是保证它们的运行次序能满足各自的依赖性要求,但条件是
这些脚本的定义遵照了AMD(Asynchronous Module Definition,异步模块定义)规范。
案例1: 加载 JavaScript 文件
<script src="./js/require.js"></script>
<script>
require(["./js/a.js", "./js/b.js"], function() {
myFunctionA();
myFunctionB();
});
</script>
如案例1 所示,有两个 JavaScript 文件 a.js 和 b.js,里面各自定义了 myFunctionA 和 myFunctionB 两个方法,通过下面这个方式可以用 RequireJS 来加载这两个文件,在 function 部份的代码可以援用这两个文件里的方法。
require 方法里的这个字符串数组参数可以允许不同的值,当字符串是以”.js”结尾,或以”/”开头,或就是1个 URL 时,RequireJS 会认为用户是在直接加载1个 JavaScript 文件,否则,当字符串是类似”my/module”的时候,它会认为这是1个模块,并且会以用户配置的 baseUrl 和 paths 来加载相应的模块所在的 JavaScript 文件。配置的部份会在稍后详细介绍。
这里要指出的是,RequireJS 默许情况下并没有保证 myFunctionA 和 myFunctionB 1定是在页面加载完成以后履行的,在有需要保证页面加载以后履行脚本时,RequireJS 提供了1个独立的 domReady 模块,需要去 RequireJS 官方网站下载这个模块,它并没有包括在 RequireJS 中。有了 domReady 模块,案例1 的代码稍做修改加上对 domReady 的依赖就能够了。
案例2: 页面加载后履行 JavaScript
<script src="./js/require.js"></script>
<script>
require(["domReady!", "./js/a.js", "./js/b.js"], function() {
myFunctionA();
myFunctionB();
});
</script>
履行案例2的代码后,通过 Firebug 可以看到 RequireJS 会在当前的页面上插入为 a.js 和 b.js 分别声明了1个 < script> 标签,用于异步方式下载 JavaScript 文件。async 属性目前绝大部份阅读器已支持,它表明了这个 < script> 标签中的 js 文件不会阻塞其他页面内容的下载。
案例3:RequireJS 插入的 < script>
<script type="text/javascript" charset="utf⑻" async="" data-requirecontext="_"
data-requiremodule="js/a.js" src="js/a.js"></script>
AMD推行1个由Require.js 负责提供的名叫define 的全局函数,该函数有3 个参数:
使用 RequireJS 来定义 JavaScript 模块
这里的 JavaScript 模块与传统的 JavaScript 代码不1样的地方在于它不必访问全局的变量。模块化的设计使得 JavaScript 代码在需要访问”全局变量”的时候,都可以通过依赖关系,把这些”全局变量”作为参数传递到模块的实现体里,在实现中就避免了访问或声明全局的变量或函数,有效的避免大量而且复杂的命名空间管理。
犹如 CommonJS 的 AMD 规范所述,定义 JavaScript 模块是通过 define 方法来实现的。
下面我们先来看1个简单的例子,这个例子通过定义1个 student 模块和1个 class 模块,在主程序中实现创建 student 对象并将 student 对象放到 class 中去。
案例4: student 模块,student.js
define(function(){
return {
createStudent: function(name, gender){
return {
name: name,
gender: gender
};
}
};
});
案例5:class 模块,class.js
define(function() {
var allStudents = [];
return {
classID: "001",
department: "computer",
addToClass: function(student) {
allStudents.push(student);
},
getClassSize: function() {
return allStudents.length;
}
};
}
);
案例6: 主程序
require(["js/student", "js/class"], function(student, clz) {
clz.addToClass(student.createStudent("Jack", "male"));
clz.addToClass(student.createStudent("Rose", "female"));
console.log(clz.getClassSize()); // 输出 2
});
student 模块和 class 模块都是独立的模块,下面我们再定义1个新的模块,这个模块依赖 student 和 class 模块,这样主程序部份的逻辑也能够包装进去了。
案例7: 依赖 student 和 class 模块的 manager 模块,manager.js
define(["js/student", "js/class"], function(student, clz){
return {
addNewStudent: function(name, gender){
clz.addToClass(student.createStudent(name, gender));
},
getMyClassSize: function(){
return clz.getClassSize();
}
};
});
案例8:新的主程序
require(["js/manager"], function(manager) {
manager.addNewStudent("Jack", "male");
manager.addNewStudent("Rose", "female");
console.log(manager.getMyClassSize());// 输出 2
});
通过上面的代码示例,我们已清楚的了解了如何写1个模块,这个模块如何被使用,模块间的依赖关系如何定义。
要想让自己的站点更快捷,可以异步加载那些暂时用不到的脚本。为此最简单的做法是审慎地使用defer 属性和async 属性。如果要求根据条件来加载脚本,请斟酌像yepnope 这样的脚本加载器。如果站点存在大量相互依赖的脚本,请斟酌Require.js。选择最合适任务的工具,然后使用它,享受它带来的便捷。
参考: