使用CDN
前言
网站设计的优化是一个很大的话题,有一些通用的原则,也有针对不同开发平台的一些建议。这方面的研究一直没有停止过,我在不同的场合也分享过这样的话题。
作为通用的原则,雅虎的工程师团队曾经给出过35个最佳实践。这个列表请参考
Best Practices for Speeding Up Your Web Site http://developer.yahoo.com/performance/rules.html
同时,他们还发布了一个相应的测试工具Yslow http://developer.yahoo.com/yslow/
我强烈推荐所有的网站开发人员都应该学习这些最佳实践,并结合自己的实际项目情况进行应用。
接下来的一段时间,我将结合ASP.NET这个开发平台,针对这些原则,通过一个系列文章的形式,做些讲解和演绎,以帮助大家更好地理解这些原则,并且更好地使用他们。
准备工作
为了跟随我进行后续的学习,你需要准备如下的开发环境和工具
- Google Chrome 或者firefox ,并且安装 Yslow这个扩展组件.请注意,这个组件是雅虎提供的,但目前没有针对IE的版本。
- https://chrome.google.com/webstore/detail/yslow/ninejjcohidippngpapiilnmkgllmakh
- https://addons.mozilla.org/en-US/firefox/addon/yslow/
- 你应该对这些浏览器的开发人员工具有所了解,你可以通过按下F12键调出这个工具。
- Visaul Studio 2010 SP1 或更高版本,推荐使用Visual Studio 2012
- 你需要对ASP.NET的开发基本流程和核心技术有相当的了解,本系列文章很难对基础知识做普及。
本文要谈讨论的话题
这篇文章,我将来和大家探讨CDN的问题,这是第二条原则,相关概念可以参考这里 http://developer.yahoo.com/performance/rules.html#cdn
我将从几个方面来介绍这个话题:
1.什么是CDN?
CDN的全称是Content Delivery Network,中文直译过来是:内容交付网络。它的主要意思是,将某些内容进行交付的网络。对于网站开发而言,我们所讲的内容通常指的是内容文件(例如javascript,css,图片等等),也就是说,这里所说的CDN的意思是指,建立(或者使用)一个更加有利于交付这些内容交付的网络。
2.为什么需要CDN?
我们必须承认,在很早的时候,是没有CDN的概念和需求的。那时候我们网站所需要的javascript等文件,就是放在我们的网站目录中,其实这也是一种内容交付的方式,而且往往还是比较高效的。但直到有一天,我们做了各种各样的网站,我们就会发现另外一个问题:就是针对同一个javascript文件,浏览器可能会缓存多个版本,例如下面这个截图所示
之所以会这样做,是因为浏览器是根据域(Domain)来缓存内容资源的,只要域不一样,那么它就需要重复下载这些资源,而且使用同样的方式将它们缓存起来。
但是,这会带来一些小的问题:重复地下载,缓存这些同样的脚本文件是需要占用带宽和本地缓存文件空间的。
于是,人们想出来一个解决方法:既然浏览器是根据域来区分这些内容资源的,那么是否可以将这些内容都放在统一的一个域里面呢?这样就算是我们有很多网站,我们都可以使用同样的地址引用这些内容资源,那么就不会产生重复下载和缓存的问题了。
3.如何使用CDN
很多问题,关键在于想到了,只要想到了,接下去怎么做其实不难。就好比我们现在讨论的这个CDN的问题。
我们可以继续以博客园的主页为例来进行分析
发展到今天,我们知道博客园是有很多站点的,例如www.cnblogs.com, news.cnblogs.com , q.cnblogs.com 等等。他们应该或多或少都会用到jquery这个通用库。那么博客园是怎么做到底呢?
从上图中我发现,他们做了一个所谓的公用的子站点:common.cnblogs.com ,里面存放了他们使用的jquery最终的版本。
实际上这就是一个最直接也是最简单的使用CDN的做法:如果你有很多站点,他们之间可以共享某些内容(例如javascript,css,image等),那么与其每个站点放一份,就不如将他们统一地存在在一个地方,这样就可以减少下载的次数和缓存的体积了。
这样做还有一个好处就是:由于主流浏览器对于同一个域所允许保持的连接数都是有限制的(可参考 http://www.impng.com/web-dev/browser-max-parallel-connections.html 的介绍),HTTP 1.1协议甚至明确地建议将这个连接数限制为2(Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion)。那么,我们采用CDN的做法来将某些内容放在不同的域里面,从一定意义上可以增加下载的并行度。关于这个原则,也可以参考http://developer.yahoo.com/performance/rules.html#split
除了上述的做法(自己单独建一个站点来保存这些内容资源),如果你是做一个互联网应用,那么还可以享受到一些业界知名的厂商所提供的CDN服务,他们将很多最常用的javascript库,放在了统一的位置(通常他们的服务器是很快的),可以供全世界的网站开发人员免费使用,这样做的好处是扩大了共享的范围,例如如果你要访问cnblogs.com ,它使用jquery的库,也许你在访问microsoft.com的时候就下载过了,所以连第一次都无需下载。
这些提供CDN服务的厂商有:
微软的CDN服务
http://www.asp.net/ajaxlibrary/cdn.ashx
Google的CDN服务
https://developers.google.com/speed/libraries/devguide
选择谁的服务,完全取决你自己的喜好。事实上,他们的用法也很接近,例如
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.0.min.js" type="text/javascript"></script>
另外,我在上一篇文章中提到了为了减少请求数,我们可以采用Bundle的形式将多个文件进行打包合并,如果遇到我们希望对某个文件使用CDN的情况,应该如何改进呢?请参考下面的代码
config.UseCdn = true; config.Add(new ScriptBundle("~/jquery", "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.0.min.js").Include("~/scripts/jquery-2.0.0.min.js"));
需要注意的是,只有当web.config文件中,将compilation的debug设置为false,才会使用CDN, 否则将使用本地的文件。这也是为什么我们需要定义两个路径的原因。
<compilation debug="false" targetFramework="4.5"/>
看起来不错,不是吗?但是CDN的使用,也有一些额外需要考虑到负面作用。
4.使用CDN的负面作用
使用CDN,尤其是使用第三方的CDN,需要考虑网络的可到达性。这些内容既然是Host在别人的服务器上面,那么从一定意义上说,并非很可控。例如,因为众所周知的原因,我上面没有使用Google提供的CDN地址。
另外,使用CDN因为会涉及到多个域,那么将违背下面两条原则:
Minimize HTTP Requests (这个我在上一篇文章详细介绍过)
http://developer.yahoo.com/performance/rules.html#num_http
Reduce DNS Lookups (后续再介绍)
http://developer.yahoo.com/performance/rules.html#dns_lookups
很惊奇吗?为什么这些原则(同时也号称为最佳实践)会自相矛盾呢?其实一点都不奇怪,世界本来就是辩证统一的。这些矛盾是客观存在的,我们要做的是,综合他们的利弊,进行权衡。你说呢
由bootstrap-button.js谈js插件编写规范
bootstrap-button.js插件是一款基于jquery的为html原生的button扩展了一些简单功能的插件,用twitter bootstrap的朋友可能再熟悉不过了,只要向button标签添加一些额外的data属性,我们就能实现点击button出现loading文字以及模拟复选和单选等功能。
下面以bootstrap-button.js的源码为实例,谈一下js插件编写的一些基本规范,笔者也是刚刚接触JS插件,权且拿这一篇,希望能抛砖引玉,欢迎讨论~
1. 源码整体结构
-
1 !function ($) { 2 3 "use strict"; // jshint ;_; 4 5 /* BUTTON PUBLIC CLASS DEFINITION 6 * ============================== */ 7 var Button = function (element, options) {/*some code*/} 8 Button.prototype.setState = function (state) {/*some code*/} 9 Button.prototype.toggle = function () {/*some code*/} 10 11 /* BUTTON PLUGIN DEFINITION 12 * ======================== */ 13 var old = $.fn.button 14 $.fn.button = function (option) {return this.each(function () {/*some code*/})} 15 $.fn.button.defaults = {loadingText: 'loading...'} 16 $.fn.button.Constructor = Button 17 18 /* BUTTON NO CONFLICT 19 * ================== */ 20 $.fn.button.noConflict = function () {$.fn.button = old;return this;} 21 22 /* BUTTON DATA-API 23 * =============== */ 24 $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) {/*some code*/}) 25 26 }(window.jQuery);
1.1. 定义一个匿名函数,并将jQuery做为函数参数传递进来执行
这样我们就可以在闭包中定义自己的私有函数而不破坏全局的命名空间,而把javascript插件写在一个相对封闭的空间,并开放可以增加扩展的地方,将不可以修改的地方定义成私有成员属性或方法,以遵循“开闭原则”
-
1 !function($){ 2 //some code 3 }(window.jQuery)
其中,!function(){}()也是匿名函数一种写法,和(function(){})()的写法区别不大,类似的还有+function(){}(), -function(){}(),~function(){}()等等,只是返回值不同而已
1.2. 匿名函数内的代码构成
PUBLIC CLASS DEFINITION:类定义,定义了插件构造方法类及方法。
PLUGIN DEFINITION:插件定义,上面只是定义了插件的类,这里才是实现插件的地方。
PLUGIN NOCONFLICT:插件命名冲突解决
DATA-API:DATA-属性接口
2. PUBLIC CLASS DEFINITION:插件类定义
2.1. 构造方法:
-
1 var Button = function (element, options) { 2 this.$element = $(element) 3 this.options = $.extend({}, $.fn.button.defaults, options) 4 }
这里是JavaScript中OOP思想的体现,定义一个类的构造方法再定义类的方法(属性),这样new出来的对象(类的具体实现)就可以调用类的公共方法和访问类的公共属性了,这里,在Button函数体内部定义的属性和方法可以看做是类的私有属性和方法,为Button.prototype对象定义的属性和方法都可以看做是类的公共属性和方法。这个类封装了插件对象初始化所需的方法和属性。
这样,通过例如var btn = new Button(element, options);我们就定义了一个Button类型的btn对象,这里的this就是btn对象本身
Button(element, options)方法接受两个参数:element和options
element就是与插件相关联的DOM元素,通过
-
this.$element = $(element)
将element封装成为一个jQuery对象$element,并由this(btn)对象的$element属性引用
options是插件的一些设置选项,这里简单说一下$.extend(target [, object1] [, objectN]),作用是将object1,...objectN对象合并到target对象中,这是一个在编写jQuery插件过程中经常用到的方法,通过
-
this.options = $.extend({}, $.fn.button.defaults, options)
就实现了将用户自定义的options覆盖了插件的默认options: $.fn.button.defaults,并合并到一个空的对象{}中,并由this(btn)对象的options属性引用
通过构造方法,btn的方法setState,toggle就可以调用btn的$element和options属性了
2.2. 类的方法定义:
2.2.1. setState方法:
-
1 Button.prototype.setState = function (state) { 2 var d = 'disabled' 3 , $el = this.$element 4 , data = $el.data() 5 , val = $el.is('input') ? 'val' : 'html' 6 7 state = state + 'Text' 8 data.resetText || $el.data('resetText', $el[val]()) 9 10 $el[val](data[state] || this.options[state]) 11 12 // push to event loop to allow forms to submit 13 setTimeout(function () { 14 state == 'loadingText' ? 15 $el.addClass(d).attr(d, d) : 16 $el.removeClass(d).removeAttr(d) 17 }, 0) 18 }
setState(state)方法的作用是为$element添加'loading...'(loading...是$.fn.button.defaults属性loadingText默认设置,详见3)
这里简单说几点:
-
val = $el.is('input') ? 'val' : 'html'
是为了兼容<button>Submit</button>和<input type="button" value="submit">的两种写法
-
data.resetText || $el.data('resetText', $el[val]())
这是一个小技巧,||是短路或,意即||左边的表达式为true则不执行||右边的表达式,为false则执行||右边的表达式,等价于
-
if(!data.resetText){ $el.data('resetText', $el[val]()); }
2.2.2. toggle方法:
-
1 Button.prototype.toggle = function () { 2 var $parent = this.$element.closest('[data-toggle="buttons-radio"]') 3 $parent && $parent.find('.active').removeClass('active') 4 this.$element.toggleClass('active') 5 }
toggle()方法的作用是通过为button添加'active'的class来添加“已选中”的CSS样式
这里面
-
$parent && $parent.find('.active').removeClass('active')
和前面短路或的例子相似,&&是短路与,意即&&左边的表达式为false则不执行&&右边的表达式,为true则执行&&右边的表达式,等价于
-
if($parent){ $parent.find('.active').removeClass('active'); }
2.2.3 小结:
定义了插件的类之后,我们只是完成了对插件的抽象——即用属性和方法来描述这个插件,但是我们尚未完成插件的具体实现,所以我们还要通过定义jQuery级插件对象来实现
3. PLUGIN DEFINITION:插件定义
3.1 插件的jQuery对象级定义
-
1 $.fn.button = function (option) { 2 return this.each(function () { 3 var $this = $(this) 4 , data = $this.data('button') 5 , options = typeof option == 'object' && option 6 if (!data) $this.data('button', (data = new Button(this, options))) 7 if (option == 'toggle') data.toggle() 8 else if (option) data.setState(option) 9 }) 10 }
首先,$.fn.button=function(){}是在$.fn对象(插件的命名空间)下添加了button属性,这样我们以后就可以通过$(selector).button()来调用插件了,很简单吧!这里扩展一下,为什么在$.fn中添加方法,$(selector)就能直接调用该方法了呢?之前阅读jQuery的源码,发现了这样的架构
-
1 var jQuery = function( selector, context ) { 2 // The jQuery object is actually just the init constructor 'enhanced' 3 return new jQuery.fn.init( selector, context, rootjQuery ); 4 }, 5 //some code 6 jQuery.fn = jQuery.prototype = {/*some code*/} 7 jQuery.fn.init.prototype = jQuery.fn;
每次我们写$(selector)实际上就是调用了jQuery(selector)函数一次($是jQuery的别名),都会返回一个jQuery.fn.init类型的对象(每写一次$(selector)都会生成一个不同的jQuery对象),jQuery实例上是一个类(构造方法),jQuery.fn.init也是一个类(构造方法),jQuery.fn正是jQuery.prototype,jQuery.fn.init.prototype也正是jQuery.fn,所以添加到jQuery.fn的方法相当于被添加到了jQuery.fn.init类下面,$(selector)实质上是一个new的jQuery.fn.init类型的对象,理所当然的也就可以调用jQuery.fn.init.prototype下的方法了,也就是jQuery.fn下的方法了。看起来似乎很绕,感觉明明一个new就可以实现的对象为什么绕了这么大个圈子?其实一点都不绕,这里绕了这么多,所以jQuery才能自豪的声称“write less, do more”, 个中奥秘还须慢慢体会。
好像扯的有点远了,还是回归正题,下面这句也是需要注意的地方
-
return this.each(function () { var $this = $(this) //some code })
通过jQuery.each方法遍历$(selector)的所有DOM元素,然后再通过$(this)将每个遍历到的DOM元素封装为单一的jQuery对象,其作用在于:对于$(selector)得到的结果集,通过形如$(selector).attr('class')方法得到的是单个结果(第一个匹配的DOM元素的class属性),而不是一组结果,通过$(selector).attr('class','active')更会将全部的class设置为active!所以需要将$(selector)的结果集逐一封装成$对象再去get或者set属性,这样才是严谨的做法。
-
if (!data) $this.data('button', (data = new Button(this, options)))
这里是真正用到data = new Button(this, options);的地方,整个$.fn.button做的最主要的事情就是将每个匹配的DOM元素的data-button属性引用new Button(this, options)对象,其次通过判断option来调用toggle方法还是setState方法,至此,插件才算是基本定义完了。
3.2 插件的默认设置定义:
-
1 $.fn.button.defaults = { 2 loadingText: 'loading...' 3 }
将插件的默认设置做为了$.fn.button的defaults属性,这样做带来的好处就是给用户修改插件的一些默认设置提供了通道,我们只需设置$.fn.button.defaults = {/*some code*/}就改变了插件的默认配置。也就是说,插件对扩展是开放的。
3.3 插件的构造器:
-
1 $.fn.button.Constructor = Button
开放了插件的构造方法类做为$.fn.button的Constructor属性,使得用户可以读取插件的构造方法类。
4. NO CONFLICT插件名称冲突解决
-
1 $.fn.button.noConflict = function () { 2 $.fn.button = old 3 return this 4 }
用法同$.noConflict,释放$.fn.button的控制权,并重新为$.fn.button声明一个名称,旨在解决插件名称和其他插件有冲突的情况
5. DATA-API DATA-属性接口
-
1 $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { 2 var $btn = $(e.target) 3 if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 4 $btn.button('toggle') 5 })
此方法,向所有带有data-toggle以button开头的元素绑定了click事件(注意这里用了事件委托的写法)
-
<button data-toggle="button">Click Me</button>
好处就是不用再写$(selector).button(options)来初始化插件了,只要页面加载,插件就自动完成初始化了。甚至options的某些属性都可以写在data-属性中,但是$.fn.button.defaults设置的默认属性可能会无效
6.总结
以上就是bootstrap-button.js插件的源码分析,bootstrap-button.js也基本上是bootstrap中最简单的插件了,但是麻雀虽小五脏俱全,bootstrap插件的编写规范也很值得我们来学习,尤其是OOP思想和其他设计模式在插件开发过程中的体现,易维护性和可扩展性等都是我们应该考虑的因素,当然个中细节还需要我们来慢慢体会。
附:bootstrap-button使用方法http://twitter.github.io/bootstrap/javascript.html#buttons