同源策略和跨域资源请求(一)

同源策略和跨域资源请求

url:uniform resource lacator,统一资源定位符,俗称网址

  • http默认端口:80
  • https默认端口:443
    (协议默认加上端口)
http://user:pass@www.example.jp:80/dir/index.html?uid=1#chl

(http协议方案名,还有https)(user:pass登录信息[认证],几乎已经不使用)(www.example.jp,服务器地址)(:80,服务器端口号)(/dir/index.html,带层次的文件路径)(?uid=1,查询字符串,键值对的方式)(#chl,片段标示符,#后面为哈希)
  • #片段标示符一般为页面内跳转,在a上设置锚点,不刷新页面。对serve来说没有效果,不会传递到服务器,文档内定位。#后面为hash,哈希

查看源origin:location.origin查看完整的源,输出域名、协议、端口号。

  • location.origin:IE下不支持这个属性,所以所以可以写成:if(location.origin===undefined){ location.protocol+location.hostname+location.port},这样就可以兼容ie

同源策略(same-origin policy)

同源策略是浏览器的一个功能,同源就是协议:域名、端口号完全一致

不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源

通过配置hosts文件,可配置本地服务器对应的域名:
C:\Windows\System32\drivers\etc\hosts

  • 如图可以设置本地访问的域名为www.baidu.com,这样浏览器访问baidu.com时就会跳转到本地(本地服务器打开)

http协议的默认端口是80,默认情况下,80端口只有系统管理员才可以监听,mac下系统管理员身份是sudo命令,window下以管理员身份启动命令行

什么不是同源策略

a.com/index.html
可以引用b.com/main.js或b.com/style.css/b.com/logo.png

但是a.com里面的b.com/main.js不能对b.com的资源进行读写(ajax报错)

  • 比如对于一个使用ajax求情数据获取更多内容的操作,可以在本地搭建配置,查看浏览器对不同源策略的限制。

1、 设置并开启本地服务器,修改hosts文件,模拟两个不同的域名a.com和b.com:

2、使用jQuery的ajax请求,url地址为:’/getmore’,当前目录下请求
初始状态有两个内容条,每次点击加载更多,获取六条内容,并添加到dom,本地服务器开启使用8080端口,浏览器输入http://a.com:8080/jQajax.html,点击可以实现ajax请求:

3、修改ajax的url请求地址为url: ‘//b.com:8080/getmore’
点击加载浏览器控制台报错,跨域请求不支持

jQajax.html:1 XMLHttpRequest cannot load http://b.com:8080/getmore?start=3&len=6. No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://a.com:8080‘ is therefore not allowed access.

这是一个跨域请求典型的例子,解决跨域请求一共以下几种方法

降域
JSONP
Cross-origin resource sharing(CORS,跨域资源共享)
HTML5 postMessage
其他hack:利用hash;利用windo.name

关于js中(function(){…})()立即执行函数

关于js中(function(){…})()立即执行函数

js中的函数有一些基本概念:函数声明、函数表达式、匿名函数

  • 函数声明:function fnName () {…};使用function关键字声明一个函数,再指定一个函数名,叫函数声明。

  • 函数表达式 var fnName = function () {…};使用function关键字声明一个函数,但未给函数命名,最后将匿名函数赋予一个变量,叫函数表达式,这是最常见的函数表达式语法形式。

  • 匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数属于函数表达式,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。

函数声明和函数表达式不同之处在于:

  • 一、Javascript引擎在解析javascript代码时,函数声明会‘函数声明提升’(Function declaration Hoisting)当前执行环境(作用域)上的函数声明,而函数表达式必须等到Javascirtp引擎执行到它所在行时,才会从上而下一行一行地解析函数表达式,
  • 二、函数表达式后面可以加括号立即调用该函数,函数声明不可以,只能以fnName()形式调用 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fnName();
function fnName(){
...
}//正常,因为‘提升’了函数声明,函数调用可在函数声明之前

fnName();
var fnName=function(){
...
}//报错,变量fnName还未保存对函数的引用,函数调用必须在函数表达式之后

var fnName=function(){
alert('Hello World');
}();//函数表达式后面加括号,当javascript引擎解析到此处时能立即调用函数

function fnName(){
alert('Hello World');
}();//不会报错,但是javascript引擎只解析函数声明,忽略后面的括号,函数声明不会被调用

function(){
console.log('Hello World');
}();//语法错误,虽然匿名函数属于函数表达式,但是未进行赋值操作,
//所以javascript引擎将开头的function关键字当做函数声明,报错:要求需要一个函数名

了解了函数声明和函数表达式,接下来看看( function(){…} )()和( function (){…} () )这两种形式的立即执行函数。这种形式的函数之所以可以立即执行,是因为()前面的函数体在一个()运算符之内构成了一个函数表达式,也就是在一个函数表达式后面加一个()就可以变成立即执行函数,()前不能是函数声明。

表达式是由数字、运算符、数字分组符号(如括号)、自由变量和约束变量等以能求得数值的有意义排列方法所得的组合。约束变量在表达式中已被指定数值,而自由变量则可以在表达式之外另行指定数值。一个表达式代表一个函数,其输入为自由变量的定值,而其输出则为表达式因之后所产生出的数值。 ——维基百科

这两种形式的运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function(a){
console.log(a); //firebug输出123,使用()运算符
})(123);

(function(a){
console.log(a); //firebug输出1234,使用()运算符
}(1234));

!function(a){
console.log(a); //firebug输出12345,使用!运算符
}(12345);

+function(a){
console.log(a); //firebug输出123456,使用+运算符
}(123456);

-function(a){
console.log(a); //firebug输出1234567,使用-运算符
}(1234567);

var fn=function(a){
console.log(a); //firebug输出12345678,使用=运算符
}(12345678)

可以看到输出结果,在function前面加!、+、 -甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、=等运算符,都将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义,告诉javascript引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码。

加括号是最安全的做法,因为!、+、-等运算符还会和函数的返回值进行运算,有时造成不必要的麻烦。

这样的写法可以形成一个独立的作用域?

javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,可以使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。

JQuery使用的就是这种方法,将JQuery代码包裹在( function (window,undefined){…jquery代码…})(window)中,在全局作用域中调用JQuery代码时,可以达到保护JQuery内部变量的作用。

原文参考地址:http://my.oschina.net/u/2331760/blog/468672?p=

跨域资源共享

跨域资源共享(Cross-origin resource sharing)

  1. b.com声明:我允许a.com访问我
  2. a.com中的js发起对b.com的ajax。

window.name:

window.name不随url的改变而改变,浏览器tab标签不改变就一直存在,利用这种特性可以传递数据

JavaScript跨域总结与解决办法*转

JavaScript跨域总结与解决办法
原文地址

解决跨域问题的几种方式:
1、document.domain+iframe的设置
2、动态创建script
3、利用iframe和location.hash
4、window.name实现的跨域数据传输
5、使用HTML5 postMessage
6、利用flash

什么是跨域

  • JavaScript出于安全方面的考虑,不允许跨域调用其他页面的对象。但在安全限制的同时也给注入iframe或是ajax应用上带来了不少麻烦。这里把涉及到跨域的一些问题简单地整理一下:

  • 首先什么是跨域,简单地理解就是因为JavaScript同源策略的限制,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。更详细的说明可以看下表:

特别注意两点:

第一,如果是协议和端口造成的跨域问题“前台”是无能为力的,

第二:在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。

“URL的首部”指window.location.protocol +window.location.host,也可以理解为“Domains, protocols and ports must match”。

接下来简单地总结一下在“前台”一般处理跨域的办法,后台proxy这种方案牵涉到后台配置,这里就不阐述了,有兴趣的可以看看yahoo的这篇文章:《JavaScript: Use a Web Proxy for Cross-Domain XMLHttpRequest Calls》

1、document.domain+iframe的设置

对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。

具体的做法是可以在http://www.a.com/a.html和http://script.a.com/b.html两个文件中分别加上document.domain = ‘a.com’;然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。当然这种办法只能解决主域相同而二级域名不同的情况,如果你异想天开的把script.a.com的domian设为alibaba.com那显然是会报错地!

代码如下:

  • www.a.com上的a.html
1
2
3
4
5
6
7
8
9
10
document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://script.a.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 在这里操纵b.html
alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
};
  • script.a.com上的b.html
1
document.domain = 'a.com';

这种方式适用于{www.kuqin.com, kuqin.com, script.kuqin.com, css.kuqin.com}中的任何页面相互通信。

备注:某一页面的domain默认等于window.location.hostname。主域名是不带www的域名,例如a.com,主域名前面带前缀的通常都为二级域名或多级域名,例如www.a.com其实是二级域名。 domain只能设置为主域名,不可以在b.a.com中将domain设置为c.a.com。

问题:

1、安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。

2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。

2、动态创建script

虽然浏览器默认禁止了跨域访问,但并不禁止在页面中引用其他域的JS文件,并可以自由执行引入的JS文件中的function(包括操作cookie、Dom等等)。根据这一点,可以方便地通过创建script节点的方法来实现完全跨域的通信。具体的做法可以参考YUI的Get Utility

这里判断script节点加载完毕还是蛮有意思的:ie只能通过script的readystatechange属性,其它浏览器是script的load事件。以下是部分判断script加载完毕的方法。

1
2
3
4
5
6
js.onload = js.onreadystatechange = function() {
if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
// callback在此处执行
js.onload = js.onreadystatechange = null;
}
};

3、利用iframe和location.hash

这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用location.hash来进行传值。在url: http://a.com#helloword中的‘#helloworld’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。假设域名a.com下的文件cs1.html要和cnblogs.com域名下的cs2.html传递信息,cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向cnblogs.com域名下的cs2.html页面,这时的hash值可以做参数传递用。

cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一点有变化则获取获取hash值。代码如下:

先是a.com下的文件cs1.html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function startRequest(){
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://www.cnblogs.com/lab/cscript/cs2.html#paramdo';
document.body.appendChild(ifr);
}

function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is '+data);
}
} catch(e) {};
}
setInterval(checkHash, 2000);

cnblogs.com域名下的cs2.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//模拟一个简单的参数处理操作
switch(location.hash){
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}

function callBack(){
try {
parent.location.hash = 'somedata';
} catch (e) {
// ie、chrome的安全机制无法修改parent.location.hash,
// 所以要利用一个中间的cnblogs域下的代理iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://a.com/test/cscript/cs3.html#somedata'; // 注意该文件在"a.com"域下
document.body.appendChild(ifrproxy);
}
}

a.com下的域名cs3.html

1
2
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);

当然这样做也存在很多缺点,诸如数据直接暴露在了url中,数据容量和类型都有限等……

4、window.name实现的跨域数据传输

文章较长列在此处不便于阅读,详细请看 window.name实现的跨域数据传输。

5、使用HTML5 postMessage

HTML5中最酷的新功能之一就是 跨文档消息传输Cross Document Messaging。下一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 Facebook已经使用了这个功能,用postMessage支持基于web的实时消息传递。

  1. otherWindow.postMessage(message, targetOrigin);
  2. otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性;
  3. window.open的返回值;通过name或下标从window.frames取到的值。
  4. message: 所要发送的数据,string类型。
  5. targetOrigin: 用于限制otherWindow,“*”表示不作限制

a.com/index.html中的代码:

1
2
3
4
5
6
7
8
9
<iframe id="ifr" src="b.com/index.html"></iframe>
<script type="text/javascript">
window.onload = function() {
var ifr = document.getElementById('ifr');
var targetOrigin = 'http://b.com'; // 若写成'http://b.com/c/proxy.html'效果一样
// 若写成'http://c.com'就不会执行postMessage了
ifr.contentWindow.postMessage('I was there!', targetOrigin);
};
</script>

b.com/index.html中的代码:

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript">
window.addEventListener('message', function(event){
// 通过origin属性判断消息来源地址
if (event.origin == 'http://a.com') {
alert(event.data); // 弹出"I was there!"
alert(event.source); // 对a.com、index.html中window对象的引用
// 但由于同源策略,这里event.source不可以访问window对象
}
}, false);
</script>

参考文章:《精通HTML5编程》第五章——跨文档消息机制、https://developer.mozilla.org/en/dom/window.postmessage

6、利用flash

这是从YUI3的IO组件中看到的办法,具体可见http://developer.yahoo.com/yui/3/io/。
可以看在Adobe Developer Connection看到更多的跨域代理文件规范:ross-Domain Policy File Specifications、HTTP Headers Blacklist。

stopImmediatePropagation介绍*转

stopImmediatePropagation介绍

原文地址

在众多的方法里面,event.stopImmediatePropagation 算是比较少用的一个方法,拼写上感觉一半像 event.stopPropagation。

对于stopPropagation 的用法大家是众所周知的,他是W3C标准事件方法,用于阻止事件冒泡(非标准情况下,用window.event.stopBubble来阻止冒泡)

而stopImmediatePropagation 的功能比stopPropagation 多一些,除了可以阻止事件冒泡之外,还可以把这个元素绑定的同类型事件也阻止了。

先把下面的代码片断(摘自MDN)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html>
<head>
<style>
p { height: 30px; width: 150px; background-color: #ccf; }
div {height: 30px; width: 150px; background-color: #cfc; }
</style>
</head>
<body>
<div>
<p>paragraph</p>
</div>
<script>
document.querySelector("p").addEventListener("click", function(event)
{
alert("我是p元素上被绑定的第一个监听函数");
}, false);
document.querySelector("p").addEventListener("click", function(event)
{
alert("我是p元素上被绑定的第二个监听函数");
event.stopImmediatePropagation();
//执行stopImmediatePropagation方法,阻止click事件冒泡,并且阻止p元素上绑定的其他click事件的事件监听函数的执行.
}, false);
document.querySelector("p").addEventListener("click", function(event)
{
alert("我是p元素上被绑定的第三个监听函数");
//该监听函数排在上个函数后面,该函数不会被执行.
}, false);
document.querySelector("div").addEventListener("click", function(event)
{
alert("我是div元素,我是p元素的上层元素");
//p元素的click事件没有向上冒泡,该函数不会被执行.
}, false);
</script>
</body>
</html>

p标签绑定了三个click事件,div绑定了一个click事件。

  • 其中p第二个click事件中使用了event.stopImmediatePropagation();

当点击时P标签时,第一和第二个alert会正常出现,
第三个alert被event.stopImmediatePropagation();的特性取消了以后的同类型(click)事件
第四个alert不出来同被阻止了事件冒泡,所以也不会出现。
相当于做了event.stopPropagation()后,再return false; jQuery对事件代理做了类似的封装。

我们了解特性之后,看一个简单应用场景:

假设点击一个按钮后,需要检测两个input控件的值,值为空时打印’error’,并停止检测下一个input,同时也需停止提单提交。
我们通常可能会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<form>
<input type="text" id="txt1">
     <input type="text" id="txt2">
<input type="submit" value="button" id="btn">
</form>
<script>
var txt1 = document.querySelector("#txt1");
       var txt2 = document.querySelector("#txt2");
var btn = document.querySelector("#btn");
btn.addEventListener("click", function(event) {
if (txt1.value == '') {
alert('error1');
return false;
}
          if (txt2.value == '') {
alert('error2');
return false;
}
return true;
}, false);
</script>
</body>
</html>

但用了event.stopImmediatePropagation时,可以对代码进行更好的抽象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<form>
<input type="text" id="txt1">
     <input type="text" id="txt2">
<input type="submit" value="button" id="btn">
</form>
<script>
var txt1 = document.querySelector("#txt1");
      var txt2 = document.querySelector("#txt2");
var btn = document.querySelector("#btn");

btn.addEventListener("click", function(event) {
if (txt1.value == '') {
alert('error1');
event.stopImmediatePropagation();
}
        if (txt2.value == '') {
alert('error2');
event.stopImmediatePropagation();
}


}, false);
      
btn.addEventListener("click", function() {
alert('Done');
});
</script>
</body>
</html>

题外:
event.isImmediatePropagationStopped 可以用来确定该元素是否有调用过event.stopImmediatePropagation。

浏览器支持情况:
Firefox >=10Chrome
IE >= 9
Opera
Safari

jQuery动画animate()方法小介绍介绍

.animate()是jQuery实现动画的一种方法,它接受一个必选参数(properties,动画所要改变的属性),和三个可选参数[, duration ] [, easing ] [, complete ] 。

  • [, duration ] 字符串或者数字决定动画将运行多久,默认: 400。
  • [, easing ]一个字符串,表示过渡使用哪种缓动函数,jQuery自身提供”linear” 和 “swing”,默认: swing
  • [, complete ] Function()在动画完成时执行的函数。

Function()函数是一个很重要的参数方法
看如下jQuery操作

1
$('div').animate({height: 0},500).hide();

div原来宽高是300px和400px,想要实现的功能是让div的高度变为0,然后再隐藏,这里采用jQuery的链式写法。但实际效果是div元素直接消失,也就是直接hide().原因是height的变化有一个持续500毫秒的时间,在这个动画执行时就已经执行.hide()隐藏。所以看不到效果。

要想看到效果就必须把hide()放到动画完成后,所以可以这样改写:

1
2
3
$('div').animate({height: 0},500,function(){
$('div').hide();
});

这样隐藏操作就会在动画完成后执行。
这个操作在需要动画完成后进行一些列方法调用的时候用很大的用处

更具体的使用可以参看这个中文文档:http://www.css88.com/jqapi-1.9/animate/

字面量简介

  • 字面量简介:

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。

在编程语言中,字面量是一种表示值的记法。例如,”Hello, World!” 在许多语言中都表示一个字符串字面量(string literal ),JavaScript也不例外。以下也是JavaScript字面量的例子,如5、true、false和null,它们分别表示一个整数、两个布尔值和一个空对象。

  • JavaScript还支持对象和数组字面量,允许使用一种简洁而可读的记法来创建数组和对象。考虑以下语句,其中创建了一个包含两个属性的对象(firstName和lastName):
1
2
3
var o = new Object();
o.name = 'Byron';
o.age = 24;

上面是创建对象的方法,可以使用下面字面量的方式:

1
2
3
4
var o = {
name: 'Byron',
age:24
}

以上赋值语句的右边是一个对象字面量(object literal)。对象字面量是一个名值对列表,每个名值对之间用逗号分隔,并用一个大括号括起。各名值对表示对象的一个属性,名和值这两部分之间用一个冒号分隔。要创建一个数组,可以创建Array对象的一个实例:

1
2
var a=new Array();
a.push(1,2,3);

不过首选的方法是使用一个数组字面量(array literal),这是一个用逗号分隔的值列表,用中括号括起:

1
var a = [1,2,3];
  • 函数字面量(function literal)如下构造:前面是一个function关键字,后面是一个函数名(可选)和参数表。然后是函数体,包围在大括号中。

赋给team变量的对象有3个属性:name、members和count

1
2
3
4
5
var team = {
name: 'hard',
work: 'learn',
sum: function(){ return 1+2;}
}
  • JavaScript对象记法(JavaScript Object Notation,JSON),这是一种用于描述文件和数组的记法,由JavaScript字面量的一个子集组成。JSON在Ajax开发人员中越来越流行,因为这种格式可以用于交换数据,通常取代了XML。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
对象字面量:


//只能添加静态属性和方法
var myObject={
propertyA: sha ,
propertyB: feng ,
methodA:function(){
alert(this.propertyA++this.propertyB);
},
methodB:function(){}
}

myObject.methodA();


//利用prototype属性可以添加公有属性和方法

function myConstructor2(){}; //声明构造函数,可以使用对象字面量语法来向prototype属性中添加所有公有成员

myConstructor2.prototype={
propertyA: sha ,
propertyB: feng ,
methodA:function(){
alert(this.propertyA++this.propertyB);
},
methodB:function(){}
}

var myconstrustor=new myConstructor2(); //声明对象
myconstrustor.methodA();

参考链接

JavaSript模块规范-AMD规范与CMD规范介绍*转

另一篇参考博客地址

JavaSript模块规范 - AMD规范与CMD规范介绍

JavaSript模块化

在了解AMD,CMD规范前,还是需要先来简单地了解下什么是模块化,模块化开发?

模块化是指在解决某一个复杂问题或者一系列的杂糅问题时,依照一种分类的思维把问题进行系统性的分解以之处理。模块化是一种处理复杂系统分解为代码结构更合理,可维护性更高的可管理的模块的方式。可以想象一个巨大的系统代码,被整合优化分割成逻辑性很强的模块时,对于软件是一种何等意义的存在。对于软件行业来说:解耦软件系统的复杂性,使得不管多么大的系统,也可以将管理,开发,维护变得“有理可循”。

还有一些对于模块化一些专业的定义为:模块化是软件系统的属性,这个系统被分解为一组高内聚,低耦合的模块。那么在理想状态下我们只需要完成自己部分的核心业务逻辑代码,其他方面的依赖可以通过直接加载被人已经写好模块进行使用即可。

首先,既然是模块化设计,那么作为一个模块化系统所必须的能力:

  1. 定义封装的模块。
  2. 定义新模块对其他模块的依赖。
  3. 可对其他模块的引入支持。

好了,思想有了,那么总要有点什么来建立一个模块化的规范制度吧,不然各式各样的模块加载方式只会将局搅得更为混乱。那么在JavaScript中出现了一些非传统模块开发方式的规范 CommonJS的模块规范,AMD(Asynchronous Module Definition),CMD(Common Module Definition)等。

AMD 与 RequireJS

AMD

Asynchronous Module Definition,用白话文讲就是 异步模块定义,对于 JSer 来说,异步是再也熟悉不过的词了,所有的模块将被异步加载,模块加载不影响后面语句运行。所有依赖某些模块的语句均放置在回调函数中。

AMD规范定义了一个自由变量或者说是全局变量 define 的函数。

define( id?, dependencies?, factory );

AMD规范 https://github.com/amdjs/amdjs-api/wiki/AMD

第一个参数 id 为字符串类型,表示了模块标识,为可选参数。若不存在则模块标识应该默认定义为在加载器中被请求脚本的标识。如果存在,那么模块标识必须为顶层的或者一个绝对的标识。

第二个参数,dependencies ,是一个当前模块依赖的,已被模块定义的模块标识的数组字面量。

第三个参数,factory,是一个需要进行实例化的函数或者一个对象。

  • 创建模块标识为 alpha 的模块,依赖于 require, export,和标识为 beta 的模块
1
2
3
4
5
6
7
define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
export.verb = function(){
return beta.verb();
// or:
return require("beta").verb();
}
});
  • 一个返回对象字面量的异步模块
1
2
3
4
5
6
7
define(["alpha"], function( alpha ){
return {
verb : function(){
return alpha.verb() + 1 ;
}
}
});
  • 无依赖模块可以直接使用对象字面量来定义
1
2
3
4
5
define( {
add : function( x, y ){
return x + y ;
}
} );
  • 类似与 CommonJS 方式定义
1
2
3
4
5
6
define( function( require, exports, module){
var a = require('a'),
b = require('b');

exports.action = function(){};
} );

require();

require API 介绍 https://github.com/amdjs/amdjs-api/wiki/require

在 AMD 规范中的 require 函数与一般的 CommonJS中的 require 不同。由于动态检测依赖关系使加载异步,对于基于回调的 require 需求强烈。

局部 与 全局 的require

局部的 require 需要在AMD模式中的 define 工厂函数中传入 require。
1
2
3
4
5
6
7
define( ['require'], function( require ){
// ...
} );
or:
define( function( require, exports, module ){
// ...
} );

局部的 require 需要其他特定的 API 来实现。

全局的 require 函数是唯一全局作用域下的变量,像 define一样。全局的 require 并不是规范要求的,但是如果实现全局的 require函数,那么其需要具有与局部 require 函数 一样的以下的限定:

  1. 模块标识视为绝对的,而不是相对的对应另一个模块标识。
  2. 只有在异步情况下,require的回调方式才被用来作为交互操作使用。因为他不可能在同步的情况下通过 require(String) 从顶层加载模块。

依赖相关的API会开始模块加载。如果需要有互操作的多个加载器,那么全局的 reqiure 应该被加载顶层模块来代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require(String)
define( function( require ){
var a = require('a'); // 加载模块a
} );

require(Array, Function)
define( function( require ){
require( ['a', 'b'], function( a,b ){ // 加载模块a b 使用
// 依赖 a b 模块的运行代码
} );
} );

require.toUrl( Url )
define( function( require ){
var temp = require.toUrl('./temp/a.html'); // 加载页面
} );

amdjs 的API https://github.com/amdjs/amdjs-api/wiki

RequireJS

官网 http://www.requirejs.org/ API http://www.requirejs.org/docs/api.html

RequireJS 是一个前端的模块化管理的工具库,遵循AMD规范,它的作者就是AMD规范的创始人 James Burke。所以说RequireJS是对AMD规范的阐述一点也不为过。

RequireJS 的基本思想为:通过一个函数来将所有所需要的或者说所依赖的模块实现装载进来,然后返回一个新的函数(模块),我们所有的关于新模块的业务代码都在这个函数内部操作,其内部也可无限制的使用已经加载进来的以来的模块。

1
<script data-main='scripts/main' src='scripts/require.js'></script>

那么scripts下的main.js则是指定的主代码脚本文件,所有的依赖模块代码文件都将从该文件开始异步加载进入执行。

defined用于定义模块,RequireJS要求每个模块均放在独立的文件之中。按照是否有依赖其他模块的情况分为独立模块和非独立模块。

    1. 独立模块,不依赖其他模块。直接定义:
      1
      2
      3
      4
      define({
      method1: function(){},
      method2: function(){}
      });

也等价于

1
2
3
4
5
6
define(function(){
return{
method1: function(){},
method2: function(){}
}
});
    1. 非独立模块,对其他模块有依赖。
1
2
3
define([ 'module1', 'module2' ], function(m1, m2){
...
});

或者:

1
2
3
4
5
define( function( require ){
var m1 = require( 'module1' ),
m2 = require( 'module2' );
...
});

简单看了一下RequireJS的实现方式,其 require 实现只不过是将 function 字符串然后提取 require 之后的模块名,将其放入依赖关系之中。

require方法调用模块

在require进行调用模块时,其参数与define类似。
1
2
3
4
require( ['foo', 'bar'], function( foo, bar ){
foo.func();
bar.func();
} );
在加载 foo 与 bar 两个模块之后执行回调函数实现具体过程。

当然还可以如之前的例子中的,在define定义模块内部进行require调用模块
1
2
3
4
5
define( function( require ){
var m1 = require( 'module1' ),
m2 = require( 'module2' );
...
});
define 和 require 这两个定义模块,调用模块的方法合称为AMD模式,定义模块清晰,不会污染全局变量,清楚的显示依赖关系。AMD模式可以用于浏览器环境并且允许非同步加载模块,也可以按需动态加载模块。

CMD 与 seaJS

CMD

在CMD中,一个模块就是一个文件,格式为:
define( factory );

全局函数define,用来定义模块。

参数 factory 可以是一个函数,也可以为对象或者字符串。

当 factory 为对象、字符串时,表示模块的接口就是该对象、字符串。

定义JSON数据模块:

1
define({ "foo": "bar" });

通过字符串定义模板模块:

1
define('this is {{data}}.');

factory 为函数的时候,表示模块的构造方法,执行构造方法便可以得到模块向外提供的接口。

1
2
3
define( function(require, exports, module) { 
// 模块代码
});

define( id?, deps?, factory );

define也可以接受两个以上的参数,字符串id为模块标识,数组deps为模块依赖:

1
2
3
define( 'module', ['module1', 'module2'], function( require, exports, module ){
// 模块代码
} );

其与 AMD 规范用法不同。

require 是 factory 的第一个参数。

require( id );
接受模块标识作为唯一的参数,用来获取其他模块提供的接口:

1
2
3
4
define(function( require, exports ){
var a = require('./a');
a.doSomething();
});

require.async( id, callback? );
require是同步往下执行的,需要的异步加载模块可以使用 require.async 来进行加载:

1
2
3
4
5
define( function(require, exports, module) { 
require.async('.a', function(a){
a.doSomething();
});
});

require.resolve( id )
可以使用模块内部的路径机制来返回模块路径,不会加载模块。

exports 是 factory 的第二个参数,用来向外提供模块接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
define(function( require, exports ){
exports.foo = 'bar'; // 向外提供的属性
exports.do = function(){}; // 向外提供的方法
});
```

当然也可以使用 return 直接向外提供接口。

```
define(function( require, exports ){
return{
foo : 'bar', // 向外提供的属性
do : function(){} // 向外提供的方法
}
});
```

也可以简化为直接对象字面量的形式:

```
define({
foo : 'bar', // 向外提供的属性
do : function(){} // 向外提供的方法
});
```

与nodeJS中一样需要注意的是,一下方式是错误的:

define(function( require, exports ){
exports = {
foo : ‘bar’, // 向外提供的属性
do : function(){} // 向外提供的方法
}
});

1
2

需要这么做

define(function( require, exports, module ){
module.exports = {
foo : ‘bar’, // 向外提供的属性
do : function(){} // 向外提供的方法
}
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

传入的对象引用可以添加属性,一旦赋值一个新的对象,那么值钱传递进来的对象引用就会失效了。开始之初,exports 是作为 module.exports 的一个引用存在,一切行为只有在这个引用上 factory 才得以正常运行,赋值新的对象后就会断开引用,exports就只是一个新的对象引用,对于factory来说毫无意义,就会出错。

- module 是factory的第三个参数,为一个对象,上面存储了一些与当前模块相关联的属性与方法。
module.id 为模块的唯一标识。
module.uri 根据模块系统的路径解析规则得到模块的绝对路径。
module.dependencies 表示模块的依赖。
module.exports 当前模块对外提供的接口。


## seaJS
官网 http://seajs.org/docs/
API快速参考 https://github.com/seajs/seajs/issues/266
sea.js 核心特征:
1. 遵循CMD规范,与NodeJS般的书写模块代码。
2. 依赖自动加载,配置清晰简洁。
兼容 Chrome 3+,Firefox 2+,Safari 3.2+,Opera 10+,IE 5.5+。

seajs.use
用来在页面中加载一个或者多个模块

// 加载一个模块
seajs.use(‘./a’);
// 加载模块,加载完成时执行回调
seajs.use(‘./a’,function(a){
a.doSomething();
});
// 加载多个模块执行回调
seajs.use([‘./a’,’./b’],function(a , b){
a.doSomething();
b.doSomething();
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

其define 与 require 使用方式基本就是CMD规范中的示例。


AMD 与 CMD 区别到底在哪里?

看了以上 AMD,requireJS 与 CMD, seaJS的简单介绍会有点感觉模糊,总感觉较为相似。因为像 requireJS 其并不是只是纯粹的AMD固有思想,其也是有CMD规范的思想,只不过是推荐 AMD规范方式而已, seaJS也是一样。

下面是玉伯对于 AMD 与 CMD 区别的解释:

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。

类似的还有 CommonJS Modules/2.0 规范,是 BravoJS 在推广过程中对模块定义的规范化产出还有不少??

这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的。
目前这些规范的实现都能达成浏览器端模块化开发的目的。

区别:

1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
2. CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:

// CMD
define(function(require, exports, module) {
var a = require(‘./a’)
a.doSomething()
// 此处略去 100 行
var b = require(‘./b’) // 依赖可以就近书写
b.doSomething()
// …
})

// AMD 默认推荐的是
define([‘./a’, ‘./b’], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// 此处略去 100 行
b.doSomething()
// …
})
```

虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
4. 还有一些细节差异,具体看这个规范的定义就好,就不多说了。

另外,SeaJS 和 RequireJS 的差异,可以参考:https://github.com/seajs/seajs/issues/277

TDD测试驱动开发*转

另有一篇介绍测试驱动开发上的五大错误,也可以看看

转载自360图书馆
测试驱动开发(Test-Driven Development,TDD)是通过测试定义所要开发的功能的接口,然后实现功能的开发过程。
Test-Driven Development(TDD),是Extreme Programming (XP)–极限编程的一个重要组成部分。

测试驱动开发TDD简介入门

测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。

TDD 是通过测试定义所要开发的功能的接口,然后实现功能的开发过程。是Extreme Programming (XP)–极限编程的一个重要组成部分。

在上面的图中,列出的的是XP的12个团队实践。Test-Driven Development是其中之一。

  • Kent Beck 的著作TDD(Test Driven Development) 中详细讲述了测试驱动开发。

  • 当你使用TDD的时候一定要说明是测试驱动开发还是测试驱动设计。这两者是有区别的。测试驱动开发,是通过测试定义所要开发的功能的接口,然后实现功能的开发过程。对于测试驱动设计,在XP中似乎已经消失了,而是被测试驱动开发所取代。另外在XP中有用于描述设计的,SimpleDesign ,Design Improvement.

一、测试驱动开发的基本过程

1) 明确当前要完成的功能。可以记录成一个 TODO 列表。
2) 快速完成针对此功能的测试用例编写。
3) 测试代码编译不通过。
4) 编写对应的功能代码。
5) 测试通过。
6) 对代码进行重构,并保证测试通过。
7) 循环完成所有功能的开发。

二、测试驱动开发的原则

1)测试隔离。不同代码的测试应该相互隔离。对一块代码的测试只考虑此代码的测试,不要考虑其实现细节(比如它使用了其他类的边界条件)。

2)一顶帽子。开发人员开发过程中要做不同的工作,比如:编写测试代码、开发功能代码、对代码重构等。做不同的事,承担不同的角色。开发人员完成对应的工作时应该保持注意力集中在当前工作上,而不要过多的考虑其他方面的细节,保证头上只有一顶帽子。避免考虑无关细节过多,无谓地增加复杂度。

3)测试列表。需要测试的功能点很多。应该在任何阶段想添加功能需求问题时,把相关功能点加到测试列表中,然后继续手头工作。然后不断的完成对应的测试用例、功能代码、重构。一是避免疏漏,也避免干扰当前进行的工作。

4)测试驱动。这个比较核心。完成某个功能,某个类,首先编写测试代码,考虑其如何使用、如何测试。然后在对其进行设计、编码。

5)先写断言。测试代码编写时,应该首先编写对功能代码的判断用的断言语句,然后编写相应的辅助语句。

6)可测试性。功能代码设计、开发时应该具有较强的可测试性。其实遵循比较好的设计原则的代码都具备较好的测试性。比如比较高的内聚性,尽量依赖于接口等。

7)及时重构。无论是功能代码还是测试代码,对结构不合理,重复的代码等情况,在测试通过后,及时进行重构。

三、测试驱动开发的测试范围

按大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试,测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。

四、TDD的优点

『充满吸引力的优点』

完工时完工。表明我可以很清楚的看到自己的这段工作已经结束了,而传统的方式很难知道什么时候编码工作结束了。 
全面正确的认识代码和利用代码,而传统的方式没有这个机会。 
为利用你成果的人提供Sample,无论它是要利用你的源代码,还是直接重用你提供的组件。 
开发小组间降低了交流成本,提高了相互信赖程度。 
避免了过渡设计。 
系统可以与详尽的测试集一起发布,从而对程序的将来版本的修改和扩展提供方便。 
TDD给了我们自信,让我们今天的问题今天解决,明天的问题明天解决,今天不能解决明天的问题,因为明天的问题还没有出现(没有TestCase),除非有TestCase否则我决不写任何代码;明天也不必担心今天的问题,只要我亮了绿灯。

『不显而易见的优点』

逃避了设计角色。对于一个敏捷的开发小组,每个人都在做设计。 
大部分时间代码处在高质量状态,100%的时间里成果是可见的。 
由于可以保证编写测试和编写代码的是相同的程序员,降低了理解代码所花费的成本。 
为减少文档和代码之间存在的细微的差别和由这种差别所引入的Bug作出杰出贡献。 
在预先设计和紧急设计之间建立一种平衡点,为你区分哪些设计该事先做、哪些设计该迭代时做提供了一个可靠的判断依据。

『有争议的优点』

 事实上提高了开发效率。每一个正在使用TDD并相信TDD的人都会相信这一点,但观望者则不同,不相信TDD的人甚至坚决反对这一点,这很正常,世界总是这样。 
发现比传统测试方式更多的Bug。 
使IDE的调试功能失去意义,或者应该说,避免了令人头痛的调试和节约了调试的时间。 
总是处在要么编程要么重构的状态下,不会使人抓狂。(两顶帽子) 
单元测试非常有趣。

五、TDD的 优势

TDD的基本思路就是通过测试来推动整个开发的进行。而测试驱动开发技术并不只是单纯的测试工作。

需求向来就是软件开发过程中感觉最不好明确描述、易变的东西。这里说的需求不只是指用户的需求,还包括对代码的使用需求。很多开发人员最害怕的就是后期还要修改某个类或者函数的接口进行修改或者扩展,为什么会发生这样的事情就是因为这部分代码的使用需求没有很好的描述。测试驱动开发就是通过编写测试用例,先考虑代码的使用需求(包括功能、过程、接口等),而且这个描述是无二义的,可执行验证的。

通过编写这部分代码的测试用例,对其功能的分解、使用过程、接口都进行了设计。而且这种从使用角度对代码的设计通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。因此测试驱动开发也是一种代码设计的过程。

开发人员通常对编写文档非常厌烦,但要使用、理解别人的代码时通常又希望能有文档进行指导。而测试驱动开发过程中产生的测试用例代码就是对代码的最好的解释。

快乐工作的基础就是对自己有信心,对自己的工作成果有信心。当前很多开发人员却经常在担心:“代码是否正确?”“辛苦编写的代码还有没有严重bug?”“修改的新代码对其他部分有没有影响?”。这种担心甚至导致某些代码应该修改却不敢修改的地步。测试驱动开发提供的测试集就可以作为你信心的来源。

当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位bug。而迅速发现、定位bug是很多开发人员的梦想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位bug提供了条件。

我的一段功能非常复杂的代码使用TDD开发完成,真实环境应用中只发现几个bug,而且很快被定位解决。您在应用后,也一定会为那种自信的开发过程,功能不断增加、完善的感觉,迅速发现、定位bug的能力所感染,喜欢这个技术的。

那么是什么样的原理、方法提供上面说的这些好处哪?下面我们就看看TDD的原理。

六、TDD的原理

测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完全部功能的开发。

我们这里把这个技术的应用领域从代码编写扩展到整个开发过程。应该对整个开发过程的各个阶段进行测试驱动,首先思考如何对这个阶段进行测试、验证、考核,并编写相关的测试文档,然后开始下一步工作,最后再验证相关的工作。下图是一个比较流行的测试模型:V测试模型。

图 V测试模型

在开发的各个阶段,包括需求分析、概要设计、详细设计、编码过程中都应该考虑相对应的测试工作,完成相关的测试用例的设计、测试方案、测试计划的编写。这里提到的开发阶段只是举例,根据实际的开发活动进行调整。相关的测试文档也不一定是非常详细复杂的文档,或者什么形式,但应该养成测试驱动的习惯。

关于测试模型,还有X测试模型。这个测试模型,我认为,是对详细阶段和编码阶段进行建模,应该说更详细的描述了详细设计和编码阶段的开发行为。及针对某个功能进行对应的测试驱动开发。


【图 X测试模型】

基本原理应该说非常简单,那么如何进行实际操作哪,下面对开发过程进行详细的介绍。

七、测试技术 

1. 测试范围、粒度

对哪些功能进行测试?会不会太繁琐?什么时候可以停止测试?这些问题比较常见。按大师 Kent Benk 的话,对那些你认为应该测试的代码进行测试。就是说,要相信自己的感觉,自己的经验。那些重要的功能、核心的代码就应该重点测试。感到疲劳就应该停下来休息一下。感觉没有必要更详细的测试,就停止本轮测试。

测试驱动开发强调测试并不应该是负担,而应该是帮助我们减轻工作量的方法。而对于何时停止编写测试用例,也是应该根据你的经验,功能复杂、核心功能的代码就应该编写更全面、细致的测试用例,否则测试流程即可。

测试范围没有静态的标准,同时也应该可以随着时间改变。对于开始没有编写足够的测试的功能代码,随着bug的出现,根据bug补齐相关的测试用例即可。

小步前进的原则,要求我们对大的功能块测试时,应该先分拆成更小的功能块进行测试,比如一个类A使用了类B、C,就应该编写到A使用B、C功能的测试代码前,完成对B、C的测试和开发。那么是不是每个小类或者小函数都应该测试哪?我认为没有必要。你应该运用你的经验,对那些可能出问题的地方重点测试,感觉不可能出问题的地方就等它真正出问题的时候再补测试吧。

2. 怎么编写测试用例

测试用例的编写就用上了传统的测试技术。

操作过程尽量模拟正常使用的过程。 
全面的测试用例应该尽量做到分支覆盖,核心代码尽量做到路径覆盖。 
测试数据尽量包括:真实数据、边界数据。 
测试语句和测试数据应该尽量简单,容易理解。 
为了避免对其他代码过多的依赖,可以实现简单的桩函数或桩类(Mock Object)。 
如果内部状态非常复杂或者应该判断流程而不是状态,可以通过记录日志字符串的方式进行验证。

八、Tips

很多朋友有疑问,“测试代码的正确性如何保障?是写测试代码还是写测试文档?”这样是不是会陷入“鸡生蛋,蛋生鸡”的循环。其实是不会的。通常测试代码通常是非常简单的,通常围绕着某个情况的正确性判断的几个语句,如果太复杂,就应该继续分解啦。而传统的开发过程通常强调测试文档。但随着开发节奏的加快,用户需求的不断变化,维护高层(需求、概要设计)的测试文档可以,更低层的测试文档的成本的确太大了。而且可实时验证功能正确性的测试代码就是对代码最好的文档。

软件开发过程中,除了遵守上面提到的测试驱动开发的几个原则外,一个需要注意的问题就是,谨防过度设计。编写功能代码时应该关注于完成当前功能点,通过测试,使用最简单、直接的方式来编码。过多的考虑后期的扩展,其他功能的添加,无疑增加了过多的复杂性,容易产生问题。应该等到要添加这些特性时在进行详细的测试驱动开发。到时候,有整套测试用例做基础,通过不断重构很容易添加相关特性。

九、FAQ

[什么时候重构?]
如果您在软件公司工作,就意味着您成天都会和想通过重构改善代码质量的想法打交道,不仅您如此,您的大部分同事也都如此。可是,究竟什么时候该重构,什么情况下应该重构呢?我相信您和您的同事可能有很多不同的看法,最常见的答案是“该重构时重构”,“写不下去的时候重构”,和“下一次迭代开始之前重构”,或者干脆就是“最近没时间,就不重构了,下次有时间的时候重构吧”。正如您已经预见到我想说的——这些想法都是对重构的误解。重构不是一种构建软件的工具,不是一种设计软件的模式,也不是一个软件开发过程中的环节,正确理解重构的人应该把重构看成一种书写代码的方式,或习惯,重构时时刻刻有可能发生。在TDD中,除去编写测试用例和实现测试用例之外的所有工作都是重构,所以,没有重构任何设计都不能实现。至于什么时候重构嘛,还要分开看,有三句话是我的经验:实现测试用例时重构代码,完成某个特性时重构设计,产品的重构完成后还要记得重构一下测试用例哦。

[什么时候设计?]
这个问题比前面一个要难回答的多,实话实说,本人在依照TDD开发软件的时候也常常被这个问题困扰,总是觉得有些问题应该在写测试用例之前定下来,而有些问题应该在新增一个一个测试用例的过程中自然出现,水到渠成。所以,我的建议是,设计的时机应该由开发者自己把握,不要受到TDD方式的限制,但是,不需要事先确定的事一定不能事先确定,免得捆住了自己的手脚。

[什么时候增加新的TestCase?]
没事做的时候。通常我们认为,如果你要增加一个新的功能,那么先写一个不能通过的TestCase;如果你发现了一个bug,那么先写一个不能通过的TestCase;如果你现在什么都没有,从0开始,请先写一个不能通过的TestCase。所有的工作都是从一个TestCase开始。此外,还要注意的是,一些大师要求我们每次只允许有一个TestCase亮红灯,在这个TestCase没有Green之前不可以写别的TestCase,这种要求可以适当考虑,但即使有多个TestCase亮红灯也不要紧,并未违反TDD的主要精神。

[TestCase该怎么写?]
测试用例的编写实际上就是两个过程:使用尚不存在的代码和定义这些代码的执行结果。所以一个TestCase也就应该包括两个部分——场景和断言。第一次写TestCase的人会有很大的不适应的感觉,因为你之前所写的所有东西都是在解决问题,现在要你提出问题确实不大习惯,不过不用担心,你正在做正确的事情,而这个世界上最难的事情也不在于如何解决问题,而在于ask the right question!

[TDD能帮助我消除Bug吗?]
答:不能!千万不要把“测试”和“除虫”混为一谈!“除虫”是指程序员通过自己的努力来减少bug的数量(消除bug这样的字眼我们还是不要讲为好^_^),而“测试”是指程序员书写产品以外的一段代码来确保产品能有效工作。虽然TDD所编写的测试用例在一定程度上为寻找bug提供了依据,但事实上,按照TDD的方式进行的软件开发是不可能通过TDD再找到bug的(想想我们前面说的“完工时完工”),你想啊,当我们的代码完成的时候,所有的测试用例都亮了绿灯,这时隐藏在代码中的bug一个都不会露出马脚来。

但是,如果要问“测试”和“除虫”之间有什么联系,我相信还是有很多话可以讲的,比如TDD事实上减少了bug的数量,把查找bug战役的关注点从全线战场提升到代码战场以上。还有,bug的最可怕之处不在于隐藏之深,而在于满天遍野。如果你发现了一个用户很不容易才能发现的bug,那么不一定对工作做出了什么杰出贡献,但是如果你发现一段代码中,bug的密度或离散程度过高,那么恭喜你,你应该抛弃并重写这段代码了。TDD避免了这种情况,所以将寻找bug的工作降低到了一个新的低度。

[我该为一个Feature编写TestCase还是为一个类编写TestCase?]
初学者常问的问题。虽然我们从TDD的说明书上看到应该为一个特性编写相应的TestCase,但为什么著名的TDD大师所写的TestCase都是和类/方法一一对应的呢?为了解释这个问题,我和我的同事们都做了很多试验,最后我们得到了一个结论,虽然我不知道是否正确,但是如果您没有答案,可以姑且相信我们。

我们的研究结果表明,通常在一个特性的开发开始时,我们针对特性编写测试用例,如果您发现这个特性无法用TestCase表达,那么请将这个特性细分,直至您可以为手上的特性写出TestCase为止。从这里开始是最安全的,它不会导致任何设计上重大的失误。但是,随着您不断的重构代码,不断的重构TestCase,不断的依据TDD的思想做下去,最后当产品伴随测试用例集一起发布的时候,您就会不经意的发现经过重构以后的测试用例很可能是和产品中的类/方法一一对应的。

[什么时候应该将全部测试都运行一遍?]
Good Question!大师们要求我们每次重构之后都要完整的运行一遍测试用例。这个要求可以理解,因为重构很可能会改变整个代码的结构或设计,从而导致不可预见的后果,但是如果我正在开发的是一个ERP怎么办?运行一遍完整的测试用例可能将花费数个小时,况且现在很多重构都是由工具做到的,这个要求的可行性和前提条件都有所动摇。所以我认为原则上你可以挑几个你觉得可能受到本次重构影响的TestCase去run,但是如果运行整个测试包只要花费数秒的时间,那么不介意你按大师的要求去做。

[什么时候改进一个TestCase?]
增加的测试用例或重构以后的代码导致了原来的TestCase的失去了效果,变得无意义,甚至可能导致错误的结果,这时是改进TestCase的最好时机。但是有时你会发现,这样做仅仅导致了原来的TestCase在设计上是臃肿的,或者是冗余的,这都不要紧,只要它没有失效,你仍然不用去改进它。记住,TestCase不是你的产品,它不要好看,也不要怎么太科学,甚至没有性能要求,它只要能完成它的使命就可以了——这也证明了我们后面所说的“用Ctrl-C/Ctrl-V编写测试用例”的可行性。

但是,美国人的想法其实跟我们还是不太一样,拿托尼巴赞的MindMap来说吧,其实画MindMap只是为了表现自己的思路,或记忆某些重要的事情,但托尼却建议大家把MindMap画成一件艺术品,甚至还有很多艺术家把自己画的抽象派MindMap拿出来帮助托尼做宣传。同样,大师们也要求我们把TestCase写的跟代码一样质量精良,可我想说的是,现在国内有几个公司能把产品的代码写的精良??还是一步一步慢慢来吧。

[为什么原来通过的测试用例现在不能通过了?]
这是一个警报,Red Alert!它可能表达了两层意思——都不是什么好意思——1)你刚刚进行的重构可能失败了,或存在一些错误未被发现,至少重构的结果和原来的代码不等价了。2)你刚刚增加的TestCase所表达的意思跟前面已经有的TestCase相冲突,也就是说,新增的功能违背了已有的设计,这种情况大部分可能是之前的设计错了。但无论哪错了,无论是那层意思,想找到这个问题的根源都比TDD的正常工作要难。

[我怎么知道那里该有一个方法还是该有一个类?]
这个问题也是常常出现在我的脑海中,无论你是第一次接触TDD或者已经成为TDD专家,这个问题都会缠绕着你不放。不过问题的答案可以参考前面的“什么时候设计”一节,答案不是唯一的。其实多数时候你不必考虑未来,今天只做今天的事,只要有重构工具,从方法到类和从类到方法都很容易。

[我要写一个TestCase,可是不知道从哪里开始?]
从最重要的事开始,what matters most?从脚下开始,从手头上的工作开始,从眼前的事开始。从一个没有UI的核心特性开始,从算法开始,或者从最有可能耽误时间的模块开始,从一个最严重的bug开始。这是TDD主义者和鼠目寸光者的一个共同点,不同点是前者早已成竹在胸。

[为什么我的测试总是看起来有点愚蠢?]
哦?是吗?来,握个手,我的也是!不必担心这一点,事实上,大师们给的例子也相当愚蠢,比如一个极端的例子是要写一个两个int变量相加的方法,大师先断言2+3=5,再断言5+5=10,难道这些代码不是很愚蠢吗?其实这只是一个极端的例子,当你初次接触TDD时,写这样的代码没什么不好,以后当你熟练时就会发现这样写没必要了,要记住,谦虚是通往TDD的必经之路!从经典开发方法转向TDD就像从面向过程转向面向对象一样困难,你可能什么都懂,但你写出来的类没有一个纯OO的!我的同事还告诉我真正的太极拳,其速度是很快的,不比任何一个快拳要慢,但是初学者(通常是指学习太极拳的前10年)太不容易把每个姿势都做对,所以只能慢慢来。

[什么场合不适用TDD?]
问的好,确实有很多场合不适合使用TDD。比如对软件质量要求极高的军事或科研产品——神州六号,人命关天的软件——医疗设备,等等,再比如设计很重要必须提前做好的软件,这些都不适合TDD,但是不适合TDD不代表不能写TestCase,只是作用不同,地位不同罢了

js模板引擎介绍搜集

js模板引擎介绍搜集*转载

s模板引擎越来越多的得到应用,如今已经出现了几十种js模板引擎,国内各大互联网公司也都开发了自己的js模板引擎(淘宝的kissy template,腾讯的artTemplate,百度的baiduTemplate等),如何从这么多纷繁的模板引擎中选择一款适合自己的呢

从这几个指标来比较js模板引擎:
1 文件大小 - 影响网络传输时间
2 执行速度(性能) - 影响响应速度,涉及模板解析和渲染
3 语法简明/易用/灵活/自定义操作符 - 影响开发效率和维护难度
4 错误处理/调试 - 影响开发效率和维护难度
5 安全(XSS) - 是否防止XSS

1 文件大小(压缩后)
Mustache:5k
doT:4k
juicer:7.65k
artTemplate(腾讯):5k
baiduTemplate(百度):3k
Underscore(Arale):11.7k - 不只是模板,还包含很多js语言增强功能
Handlebars(Arale):30.5k

2 执行速度(不要迷恋速度)
对于执行速度,不得不提模板“编译速度”和“渲染速度”。这几个主流模板都支持将模板预编译,然后再渲染。
这里有一篇文章《高性能JavaScript模板引擎原理解析》,说artTemplate的速度达到理论极限,实际说的是渲染速度,它的综合速度并不快。
一般情况下,每页面只有一两个模板,执行时间差别不大。

这里<<有个测试页面,根据artTemplate的测试页面《引擎渲染速度竞赛》改的,揭示了几个主流js模板引擎的性能情况,大致结果截图如下:

可以看出artTemplate,juicer,doT比其他模板引擎快很多。

3 语法 简明/易用/灵活/自定义操作符 - 影响开发效率和维护难度

语法需要一段时间的使用经验才能更深切地体会到优缺点,并且每个人喜欢的语法风格也不同,这部分可能略带个人主观色彩。
这几个js模板引擎的语法可以粗略分为两种,一种是类似javascript的语法(doT, artTemplate, underscore),另一种是与javascript差异较大的语法(Mustache, juicer, handlebars)。从易上手的角度来看,类javascript语法更容易被新手掌握,但是熟练掌握之后,各个模板的语法都能满足我们的需求,可以按个人喜好来选择。

Mustache声称是无逻辑(logic-less)模板,没有for、if…else…语法,但实际可以实现循环和分支,还可以实现更复杂的逻辑。
doT模板语法灵活,阅读起来很易懂。可以方便地改造成jquery插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
[javascript] view plaincopy 
<!--Mustache 的模板 -->
<script id="Mustache" type="text/tmpl">
<ul>
{{#list}}
<li>{{{index}}}. 用户: {{{user}}}/ 网站:{{{site}}}</li>
{{/list}}
</ul>
</script>

<!-- doT 的模板 -->
<script id="doT" type="text/tmpl">
<ul>
{{ for (var val, i = 0, l = it.list.length; i < l; i ++) { }}
{{ val = it.list; }}
<li>{{=val[i].index}}. 用户: {{=val[i].user}}/ 网站:{{=val[i].site}}</li>
{{ } }}
</ul>
</script>

<!--juicer 的模板 -->
<script id="juicer" type="text/tmpl">
<ul>
{@each list as val}
<li>
val.index.用户:
{val.user}/ 网站:$${val.site}</li>
{@/each}
</ul>
</script>

<!-- artTemplate 的模板 -->
<script id="template" type="text/tmpl">
<ul>
<% for (i = 0, l = list.length; i < l; i ++) { %>
<li><%=list[i].index%>. 用户: <%=list[i].user%>/ 网站:<%=list[i].site%></li>
<% } %>
</ul>
</script>

<!-- underscore 的模板 -->
<script id="underscoreTemplate" type="text/tmpl">
<ul>
<% for (var i = 0, l = list.length; i < l; i ++) { %>
<li><%=list[i].index%>. 用户: <%=list[i].user%>/ 网站:<%=list[i].site%></li>
<% } %>
</ul>
</script>

<!-- Handlebars 的模板 -->
<script id="Handlebars" type="text/tmpl">
<ul>
{{#list}}
<li>{{{index}}}. 用户: {{{user}}}/ 网站:{{{site}}}</li>
{{/list}}
</ul>
</script>

4 错误处理/调试 - 影响开发效率和维护难度
artTemplate 有详细的错误提示信息,查错方便,不影响后面代码的继续执行
kissy template 错误信息直接输出在页面,而不是在控制台。不影响后面代码的继续执行
juicer 控制台提示模板渲染出错,不影响后面代码的继续执行
mustache 没有任何错误信息,不影响后面代码的继续执行
其他控制台报脚本错误 js执行中断,不知道是哪里出错

5 安全- 是否防止XSS
以上几个模板引擎全都支持html转义,防止XSS

最终的一个对比:


介绍 X 款 JavaScript 的模板引擎。(排名不分先后顺序)

  1. Mustache
    基于javascript 实现的模板引擎,类似于 Microsoft’s jQuery template plugin,但更简单易用!

  2. EasyTemplate
    在使用过Freemarker模板后,感觉它的 语法比较朴实,平易近人,容易上手,于是决定按它的语法风格实现一个前端的 模板引擎,这就有了下面的EasyTemplate! EasyTemplate模板的函数大小为1.34k(未压缩),暂时只实现了 list,list index,if elseif else等功能,应该可以满足大部分的使用需求了。 EasyTemplate模板 引擎的解析速度测试,渲染1000行数据,在不同的浏览器中,平均速度大约在30豪秒以内(测试机器性能较弱)。

  3. jSmart
    jSmart 是著名的 PHP 模板引擎 Smarty 的 JavaScript 移植版本。

  4. Trimpath
    Trimpath JavaScript 是个轻量级的,基于JavaScript的,跨浏览器,采用APL/GPL开放源代码协议的,可以让你轻松进行基于模板编程方式的纯JS引擎。新浪的评论系统使用的就是此模板。

  5. jade
    Jade是受Haml的影响以JavaScript实现用于node的高性能模板引擎。

  6. Hogan.js
    来自 Twitter 的 JavaScript 模板引擎。

  7. Handlebars
    Handlebars 是一个 JavaScript 的页面模板库

  8. doT.js
    doT.js 包含为浏览器和Node.js 准备的 JavaScript 模板引擎。

  9. dom.js
    dom.js 是一款可用在客户端和服务器端的 JavaScript 模板引擎

  10. Plates.js
    Plates.js 是一个轻量级、无逻辑、DSL 的 JavaScript 模板引擎。

  11. ICanHaz.js
    ICanHaz.js 是一个简单而且功能强大的客户端的 JavaScript 模板引擎。

  12. dotpl-js
    Dotpl-JS 是一个纯javascript模板引擎,支持IF和FOR关键字,多循环衔套及字段渲染,跨浏览器支持。是一个实用的javascipt工具,页面静态化利器!

  13. EJS
    EJS 可以将数据和模板合并然后生成 HTML 文本。

,