<h2 id="8.1">console对象</h2>
`console`对象是JavaScript的原生对象,它有点像Unix系统的标准输出`stdout`和标准错误`stderr`,可以输出各种信息用来调试程序,而且还提供了很多额外的方法,供开发者调用。它的常见用途有两个。
- 显示网页代码运行时的错误信息。
- 提供了一个命令行接口,用来与网页代码互动。
## 浏览器实现
`console`对象的浏览器实现,包含在浏览器自带的开发工具之中。以Chrome浏览器的“开发者工具”(Developer Tools)为例,首先使用下面三种方法的一种打开它。
1. 按F12或者`Control + Shift + i`(PC平台)/ `Alt + Command + i`(Mac平台)。
2. 在菜单中选择“工具/开发者工具”。
3. 在一个页面元素上,打开右键菜单,选择其中的“Inspect Element”。
![开发者工具](https://developers.google.com/chrome-developer-tools/images/image03.png)
打开“开发者工具”以后,可以看到在顶端有八个面板卡可供选择,分别是:
- **Elements**:用来调试网页的HTML源码和CSS代码。
- **Resources**:查看网页加载的各种资源文件(比如代码文件、字体文件、css文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)。
- **Network**:查看网页的HTTP通信情况。
- **Sources**:调试JavaScript代码。
- **Timeline**:查看各种网页行为随时间变化的情况。
- **Profiles**:查看网页的性能情况,比如CPU和内存消耗。
- **Audits**:提供网页优化的建议。
- **Console**:用来运行JavaScript命令。
这八个面板都有各自的用途。以下内容只针对Console面板,又称为控制台。Console面板基本上就是一个命令行窗口,你可以在提示符下,键入各种命令。
## console对象的方法
`console`对象提供的各种方法,用来与控制台窗口互动。
### log(),info(),debug()
`console.log`方法用于在console窗口输出信息。它可以接受多个参数,将它们的结果连接起来输出。
```javascript
console.log("Hello World")
// Hello World
console.log("a","b","c")
// a b c
```
`console.log`方法会自动在每次输出的结尾,添加换行符。
```javascript
console.log(1);
console.log(2);
console.log(3);
// 1
// 2
// 3
```
如果第一个参数是格式字符串(使用了格式占位符),console.log方法将依次用后面的参数替换占位符,然后再进行输出。
```javascript
console.log(' %s + %s = %s', 1, 1, 2)
// 1 + 1 = 2
```
上面代码中,`console.log`方法的第一个参数有三个占位符(`%s`),第二、三、四个参数会在显示时,依次替换掉这个三个占位符。`console.log`方法支持的占位符格式有以下一些,不同格式的数据必须使用对应格式的占位符。
- %s 字符串
- %d 整数
- %i 整数
- %f 浮点数
- %o 对象的链接
- %c CSS格式字符串
```javascript
var number = 11 * 9;
var color = 'red';
console.log('%d %s balloons', number, color);
// 99 red balloons
```
上面代码中,第二个参数是数值,对应的占位符是`%d`,第三个参数是字符串,对应的占位符是`%s`。
使用`%c`占位符时,对应的参数必须是CSS语句,用来对输出内容进行CSS渲染。
```javascript
console.log('%cThis text is styled!',
'color: red; background: yellow; font-size: 24px;'
)
```
上面代码运行后,输出的内容将显示为蓝底绿字。
`console.log`方法的两种参数格式,可以结合在一起使用。
```javascript
console.log(' %s + %s ', 1, 1, '= 2')
// 1 + 1 = 2
```
如果参数是一个对象,`console.log`会显示该对象的值。
```javascript
console.log({foo: 'bar'})
// Object {foo: "bar"}
console.log(Date)
// function Date() { [native code] }
```
上面代码输出`Date`对象的值,结果为一个构造函数。
`console.info()`和`console.debug()`都是`console.log`方法的别名,用法完全一样。只不过`console.info`方法会在输出信息的前面,加上一个蓝色图标。
console对象的所有方法,都可以被覆盖。因此,可以按照自己的需要,定义console.log方法。
```javascript
['log', 'info', 'warn', 'error'].forEach(function(method) {
console[method] = console[method].bind(
console,
new Date().toISOString()
);
});
console.log("出错了!");
// 2014-05-18T09:00.000Z 出错了!
```
上面代码表示,使用自定义的`console.log`方法,可以在显示结果添加当前时间。
### warn(),error()
warn方法和error方法也是输出信息,它们与log方法的不同之处在于,warn方法输出信息时,在最前面加一个黄色三角,表示警告;error方法输出信息时,在最前面加一个红色的叉,表示出错,同时会显示错误发生的堆栈。其他用法都一样。
```javascript
console.error("Error: %s (%i)", "Server is not responding",500)
// Error: Server is not responding (500)
console.warn('Warning! Too few nodes (%d)', document.childNodes.length)
// Warning! Too few nodes (1)
```
本质上,log方法是写入标准输出(stdout),warn方法和error方法是写入标准错误(stderr)。
### table()
对于某些复合类型的数据,console.table方法可以将其转为表格显示。
```javascript
var languages = [
{ name: "JavaScript", fileExtension: ".js" },
{ name: "TypeScript", fileExtension: ".ts" },
{ name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);
```
上面代码的language,转为表格显示如下。
(index)|name|fileExtension
-------|----|-------------
0|"JavaScript"|".js"
1|"TypeScript"|".ts"
2|"CoffeeScript"|".coffee"
复合型数据转为表格显示的条件是,必须拥有主键。对于上面的数组来说,主键就是数字键。对于对象来说,主键就是它的最外层键。
```javascript
var languages = {
csharp: { name: "C#", paradigm: "object-oriented" },
fsharp: { name: "F#", paradigm: "functional" }
};
console.table(languages);
```
上面代码的language,转为表格显示如下。
(index)|name|paradigm
-------|----|--------
csharp|"C#"|"object-oriented"
fsharp|"F#"|"functional"
### count()
count方法用于计数,输出它被调用了多少次。
```javascript
function greet(user) {
console.count();
return "hi " + user;
}
greet('bob')
// : 1
// "hi bob"
greet('alice')
// : 2
// "hi alice"
greet('bob')
// : 3
// "hi bob"
```
上面代码每次调用greet函数,内部的console.count方法就输出执行次数。
该方法可以接受一个字符串作为参数,作为标签,对执行次数进行分类。
```javascript
function greet(user) {
console.count(user);
return "hi " + user;
}
greet('bob')
// bob: 1
// "hi bob"
greet('alice')
// alice: 1
// "hi alice"
greet('bob')
// bob: 2
// "hi bob"
```
上面代码根据参数的不同,显示bob执行了两次,alice执行了一次。
### dir()
dir方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示。
```javascript
console.log({f1: 'foo', f2: 'bar'})
// Object {f1: "foo", f2: "bar"}
console.dir({f1: 'foo', f2: 'bar'})
// Object
// f1: "foo"
// f2: "bar"
// __proto__: Object
```
上面代码显示dir方法的输出结果,比log方法更易读,信息也更丰富。
该方法对于输出DOM对象非常有用,因为会显示DOM对象的所有属性。
```javascript
console.dir(document.body)
```
### assert()
assert方法接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为false,才会输出第二个参数,否则不会有任何结果。
```javascript
// 实例
console.assert(true === false, "判断条件不成立")
// Assertion failed: 判断条件不成立
```
下面是另一个例子,判断子节点的个数是否大于等于500。
```javascript
console.assert(list.childNodes.length < 500, "节点个数大于等于500")
```
### time(),timeEnd()
这两个方法用于计时,可以算出一个操作所花费的准确时间。
```javascript
console.time("Array initialize");
var array= new Array(1000000);
for (var i = array.length - 1; i >= 0; i--) {
array[i] = new Object();
};
console.timeEnd("Array initialize");
// Array initialize: 1914.481ms
```
time方法表示计时开始,timeEnd方法表示计时结束。它们的参数是计时器的名称。调用timeEnd方法之后,console窗口会显示“计时器名称: 所耗费的时间”。
### profile(),profileEnd()
console.profile方法用来新建一个性能测试器(profile),它的参数是性能测试器的名字。
```javascript
console.profile('p')
// Profile 'p' started.
```
console.profileEnd方法用来结束正在运行的性能测试器。
```javascript
console.profileEnd()
// Profile 'p' finished.
```
打开浏览器的开发者工具,在profile面板中,可以看到这个性能调试器的运行结果。
### group(),groupend(),groupCollapsed()
console.group和console.groupend这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。
```javascript
console.group('Group One');
console.group('Group Two');
// some code
console.groupEnd(); // Group Two 结束
console.groupEnd(); // Group One 结束
```
console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。
```javascript
console.groupCollapsed('Fetching Data');
console.log('Request Sent');
console.error('Error: Server not responding (500)');
console.groupEnd();
```
上面代码只显示一行”Fetching Data“,点击后才会展开,显示其中包含的两行。
### trace(),clear()
`console.trace`方法显示当前执行的代码在堆栈中的调用路径。
```javascript
console.trace()
// console.trace()
// (anonymous function)
// InjectedScript._evaluateOn
// InjectedScript._evaluateAndWrap
// InjectedScript.evaluate
```
console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。
## 命令行API
在控制台中,除了使用console对象,还可以使用一些控制台自带的命令行方法。
(1)$_
$_属性返回上一个表达式的值。
```javascript
2+2
// 4
$_
// 4
```
(2)$0 - $4
控制台保存了最近5个在Elements面板选中的DOM元素,$0代表倒数第一个,$1代表倒数第二个,以此类推直到$4。
(3)$(selector)
$(selector)返回一个数组,包括特定的CSS选择器匹配的所有DOM元素。该方法实际上是document.querySelectorAll方法的别名。
```javascript
var images = $('img');
for (each in images) {
console.log(images[each].src);
}
```
上面代码打印出网页中所有img元素的src属性。
(4)$$(selector)
$$(selector)返回一个选中的DOM对象,等同于document.querySelectorAll。
(5)$x(path)
$x(path)方法返回一个数组,包含匹配特定XPath表达式的所有DOM元素。
```javascript
$x("//p[a]")
```
上面代码返回所有包含a元素的p元素。
(6)inspect(object)
inspect(object)方法打开相关面板,并选中相应的元素:DOM元素在Elements面板中显示,JavaScript对象在Profiles中显示。
(7)getEventListeners(object)
getEventListeners(object)方法返回一个对象,该对象的成员为登记了回调函数的各种事件(比如click或keydown),每个事件对应一个数组,数组的成员为该事件的回调函数。
(8)keys(object),values(object)
keys(object)方法返回一个数组,包含特定对象的所有键名。
values(object)方法返回一个数组,包含特定对象的所有键值。
```javascript
var o = {'p1': 'a', 'p2': 'b'};
keys(o)
// ["p1", "p2"]
values(o)
// ["a", "b"]
```
(9)`monitorEvents(object[, events]) ,unmonitorEvents(object[, events])`
`monitorEvents(object[, events])`方法监听特定对象上发生的特定事件。当这种情况发生时,会返回一个Event对象,包含该事件的相关信息。unmonitorEvents方法用于停止监听。
```javascript
monitorEvents(window, "resize");
monitorEvents(window, ["resize", "scroll"])
```
上面代码分别表示单个事件和多个事件的监听方法。
```javascript
monitorEvents($0, "mouse");
unmonitorEvents($0, "mousemove");
```
上面代码表示如何停止监听。
monitorEvents允许监听同一大类的事件。所有事件可以分成四个大类。
- mouse:"mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel"
- key:"keydown", "keyup", "keypress", "textInput"
- touch:"touchstart", "touchmove", "touchend", "touchcancel"
- control:"resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset"
```javascript
monitorEvents($("#msg"), "key");
```
上面代码表示监听所有key大类的事件。
(10)`profile([name])`,profileEnd()
profile方法用于启动一个特定名称的CPU性能测试,profileEnd方法用于结束该性能测试。
```javascript
profile("My profile")
profileEnd("My profile")
```
(11)其他方法
命令行API还提供以下方法。
- clear()方法清除控制台的历史。
- copy(object)方法复制特定DOM元素到剪贴板。
- dir(object)方法显示特定对象的所有属性,是console.dir方法的别名。
- dirxml(object)方法显示特定对象的XML形式,是console.dirxml方法的别名。
## debugger语句
`debugger`语句必须与除错工具配合使用,如果没有除错工具,debugger语句不会产生任何结果。
在Chrome浏览器中,当代码运行到debugger指定的行时,就会暂停运行,自动打开控制台界面。它的作用类似于设置断点。
```javascript
for(var i = 0;i < 5;i++){
console.log(i);
if (i === 2) debugger;
}
```
上面代码打印出0,1,2以后,就会暂停,自动打开console窗口,等待进一步处理。
## 移动端开发
(本节暂存此处)
### 模拟手机视口(viewport)
chrome浏览器的开发者工具,提供一个选项,可以模拟手机屏幕的显示效果。
打开“设置”的Overrides面板,选择相应的User Agent和Device Metrics选项。
![选择User Agent](https://developers.google.com/chrome-developer-tools/docs/mobile-emulation/image_3.png)
User Agent可以使得当前浏览器发出手机浏览器的Agent字符串,Device Metrics则使得当前浏览器的视口变为手机的视口大小。这两个选项可以独立选择,不一定同时选中。
### 模拟touch事件
我们可以在PC端模拟JavaScript的touch事件。
首先,打开chrome浏览器的开发者工具,选择“设置”中的Overrides面板,勾选“Enable touch events”选项。
![Enable touch events的图片](https://developers.google.com/chrome-developer-tools/docs/mobile-emulation/image_0.png)
然后,鼠标就会触发touchstart、touchmove和touchend事件。(此时,鼠标本身的事件依然有效。)
至于多点触摸,必须要有支持这个功能的设备才能模拟,具体可以参考[Multi-touch web development](http://www.html5rocks.com/en/mobile/touch/)。
### 模拟经纬度
chrome浏览器的开发者工具,还可以模拟当前的经纬度数据。打开“设置”的Overrides面板,选中Override Geolocation选项,并填入相应经度和纬度数据。
![模拟经纬度](https://developers.google.com/chrome-developer-tools/docs/mobile-emulation/image_11.png)
### 远程除错
(1) Chrome for Android
Android设备上的Chrome浏览器支持USB除错。PC端需要安装Android SDK和Chrome浏览器,然后用usb线将手机和PC连起来,可参考[官方文档](https://developers.google.com/chrome-developer-tools/docs/remote-debugging)。
(2) Opera
Opera浏览器的除错环境Dragonfly支持远程除错([教程](http://www.codegeek.net/blog/2012/mobile-debugging-with-opera-dragonfly/))。
(3) Firefox for Android
参考[官方文档](https://hacks.mozilla.org/2012/08/remote-debugging-on-firefox-for-android/)。
(4) Safari on iOS6
你可以使用Mac桌面电脑的Safari 6浏览器,进行远程除错([教程](http://www.mobilexweb.com/blog/iphone-5-ios-6-html5-developers))。
## Google Closure
(本节暂存此处)
Google Closure是Google提供的一个JavaScript源码处理工具,主要用于压缩和合并多个JavaScript脚本文件。
Google Closure使用Java语言开发,使用之前必须先安装Java。然后,前往[官方网站](https://developers.google.com/closure/)进行下载,这里我们主要使用其中的编译器(compiler)。
首先,查看使用帮助。
```bash
java -jar /path/to/closure/compiler.jar --help
```
直接在脚本命令后面跟上要合并的脚本,就能完成合并。
```bash
java -jar /path/to/closure/compiler.jar *.js
```
使用--js参数,可以确保按照参数的先后次序合并文件。
```bash
java -jar /path/to/closure/compiler.jar --js script1.js --js script2.js --js script3.js
```
但是,这样的运行结果是将合并后的文件全部输出到屏幕(标准输出),因此需要使用--js_output_file参数,指定合并后的文件名。
```bash
java -jar /path/to/closure/compiler.jar --js script1.js --js script2.js --js script3.js --js_output_file scripts-compiled.js
```
## Javascript 性能测试
(本节暂存此处)
### 第一种做法
最常见的测试性能的做法,就是同一操作重复n次,然后计算每次操作的平均时间。
```javascript
var totalTime,
start = new Date,
iterations = 6;
while (iterations--) {
// Code snippet goes here
}
// totalTime → the number of milliseconds it took to execute
// the code snippet 6 times
totalTime = new Date - start;
```
上面代码的问题在于,由于计算机的性能不断提高,如果只重复6次,很可能得到0毫秒的结果,即不到1毫秒,Javascript引擎无法测量。
### 第二种做法
另一种思路是,测试单位时间内完成了多少次操作。
```javascript
var hz,
period,
startTime = new Date,
runs = 0;
do {
// Code snippet goes here
runs++;
totalTime = new Date - startTime;
} while (totalTime < 1000);
// convert ms to seconds
totalTime /= 1000;
// period → how long per operation
period = totalTime / runs;
// hz → the number of operations per second
hz = 1 / period;
// can be shortened to
// hz = (runs * 1000) / totalTime;
```
这种做法的注意之处在于,测试结构受外界环境影响很大,为了得到正确结构,必须重复多次。
<h2 id="8.2">Gulp:任务自动管理工具</h2>
Gulp与Grunt一样,也是一个自动任务运行器。它充分借鉴了Unix操作系统的管道(pipe)思想,很多人认为,在操作上,它要比Grunt简单。
## 安装
Gulp需要全局安装,然后再在项目的开发目录中安装为本地模块。先进入项目目录,运行下面的命令。
```bash
npm install -g gulp
npm install --save-dev gulp
```
除了安装gulp以外,不同的任务还需要安装不同的gulp插件模块。举例来说,下面代码安装了gulp-uglify模块。
```bash
$ npm install --save-dev gulp-uglify
```
## gulpfile.js
项目根目录中的gulpfile.js,是Gulp的配置文件。下面就是一个典型的gulpfile.js文件。
```javascript
var gulp = require('gulp');
var uglify = require('gulp-uglify');
gulp.task('minify', function () {
gulp.src('js/app.js')
.pipe(uglify())
.pipe(gulp.dest('build'))
});
```
上面代码中,gulpfile.js加载gulp和gulp-uglify模块之后,使用gulp模块的task方法指定任务minify。task方法有两个参数,第一个是任务名,第二个是任务函数。在任务函数中,使用gulp模块的src方法,指定所要处理的文件,然后使用pipe方法,将上一步的输出转为当前的输入,进行链式处理。
task方法的回调函数使用了两次pipe方法,也就是说做了两种处理。第一种处理是使用gulp-uglify模块,压缩源码;第二种处理是使用gulp模块的dest方法,将上一步的输出写入本地文件,这里是build.js(代码中省略了后缀名js)。
执行minify任务时,就在项目目录中执行下面命令就可以了。
```bash
$ gulp minify
```
从上面的例子中可以看到,gulp充分使用了“管道”思想,就是一个数据流(stream):src方法读入文件产生数据流,dest方法将数据流写入文件,中间是一些中间步骤,每一步都对数据流进行一些处理。
下面是另一个数据流的例子。
```javascript
gulp.task('js', function () {
return gulp.src('js/*.js')
.pipe(jshint())
.pipe(uglify())
.pipe(concat('app.js'))
.pipe(gulp.dest('build'));
});
```
上面代码使用pipe命令,分别进行jshint、uglify、concat三步处理。
## gulp模块的方法
### src()
gulp模块的src方法,用于产生数据流。它的参数表示所要处理的文件,这些指定的文件会转换成数据流。参数的写法一般有以下几种形式。
- js/app.js:指定确切的文件名。
- js/*.js:某个目录所有后缀名为js的文件。
- js/\*\*/*.js:某个目录及其所有子目录中的所有后缀名为js的文件。
- !js/app.js:除了js/app.js以外的所有文件。
- *.+(js|css):匹配项目根目录下,所有后缀名为js或css的文件。
src方法的参数还可以是一个数组,用来指定多个成员。
```javascript
gulp.src(['js/**/*.js', '!js/**/*.min.js'])
```
### dest()
dest方法将管道的输出写入文件,同时将这些输出继续输出,所以可以依次调用多次dest方法,将输出写入多个目录。如果有目录不存在,将会被新建。
```javascript
gulp.src('./client/templates/*.jade')
.pipe(jade())
.pipe(gulp.dest('./build/templates'))
.pipe(minify())
.pipe(gulp.dest('./build/minified_templates'));
```
dest方法还可以接受第二个参数,表示配置对象。
```javascript
gulp.dest('build', {
cwd: './app',
mode: '0644'
})
```
配置对象有两个字段。cwd字段指定写入路径的基准目录,默认是当前目录;mode字段指定写入文件的权限,默认是0777。
### task()
task方法用于定义具体的任务。它的第一个参数是任务名,第二个参数是任务函数。下面是一个非常简单的任务函数。
```javascript
gulp.task('greet', function () {
console.log('Hello world!');
});
```
task方法还可以指定按顺序运行的一组任务。
```javascript
gulp.task('build', ['css', 'js', 'imgs']);
```
上面代码先指定build任务,它由css、js、imgs三个任务所组成,task方法会并发执行这三个任务。注意,由于每个任务都是异步调用,所以没有办法保证js任务的开始运行的时间,正是css任务运行结束。
如果希望各个任务严格按次序运行,可以把前一个任务写成后一个任务的依赖模块。
```javascript
gulp.task('css', ['greet'], function () {
// Deal with CSS here
});
```
上面代码表明,css任务依赖greet任务,所以css一定会在greet运行完成后再运行。
task方法的回调函数,还可以接受一个函数作为参数,这对执行异步任务非常有用。
```javascript
// 执行shell命令
var exec = require('child_process').exec;
gulp.task('jekyll', function(cb) {
// build Jekyll
exec('jekyll build', function(err) {
if (err) return cb(err); // return error
cb(); // finished task
});
});
```
如果一个任务的名字为default,就表明它是“默认任务”,在命令行直接输入gulp命令,就会运行该任务。
```javascript
gulp.task('default', function () {
// Your default task
});
// 或者
gulp.task('default', ['styles', 'jshint', 'watch']);
```
执行的时候,直接使用gulp,就会运行styles、jshint、watch三个任务。
### watch()
watch方法用于指定需要监视的文件。一旦这些文件发生变动,就运行指定任务。
```javascript
gulp.task('watch', function () {
gulp.watch('templates/*.tmpl.html', ['build']);
});
```
上面代码指定,一旦templates目录中的模板文件发生变化,就运行build任务。
watch方法也可以用回调函数,代替指定的任务。
```javascript
gulp.watch('templates/*.tmpl.html', function (event) {
console.log('Event type: ' + event.type);
console.log('Event path: ' + event.path);
});
```
另一种写法是watch方法所监控的文件发生变化时(修改、增加、删除文件),会触发change事件。可以对change事件指定回调函数。
```javascript
var watcher = gulp.watch('templates/*.tmpl.html', ['build']);
watcher.on('change', function (event) {
console.log('Event type: ' + event.type);
console.log('Event path: ' + event.path);
});
```
除了change事件,watch方法还可能触发以下事件。
- end:回调函数运行完毕时触发。
- error:发生错误时触发。
- ready:当开始监听文件时触发。
- nomatch:没有匹配的监听文件时触发。
watcher对象还包含其他一些方法。
- watcher.end():停止watcher对象,不会再调用任务或回调函数。
- watcher.files():返回watcher对象监视的文件。
- watcher.add(glob):增加所要监视的文件,它还可以附件第二个参数,表示回调函数。
- watcher.remove(filepath):从watcher对象中移走一个监视的文件。
## gulp-load-plugins模块
一般情况下,gulpfile.js中的模块需要一个个加载。
```javascript
var gulp = require('gulp'),
jshint = require('gulp-jshint'),
uglify = require('gulp-uglify'),
concat = require('gulp-concat');
gulp.task('js', function () {
return gulp.src('js/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'))
.pipe(uglify())
.pipe(concat('app.js'))
.pipe(gulp.dest('build'));
});
```
上面代码中,除了gulp模块以外,还加载另外三个模块。
这种一一加载的写法,比较麻烦。使用gulp-load-plugins模块,可以加载package.json文件中所有的gulp模块。上面的代码用gulp-load-plugins模块改写,就是下面这样。
```javascript
var gulp = require('gulp'),
gulpLoadPlugins = require('gulp-load-plugins'),
plugins = gulpLoadPlugins();
gulp.task('js', function () {
return gulp.src('js/*.js')
.pipe(plugins.jshint())
.pipe(plugins.jshint.reporter('default'))
.pipe(plugins.uglify())
.pipe(plugins.concat('app.js'))
.pipe(gulp.dest('build'));
});
```
上面代码假设package.json文件包含以下内容。
```javascript
{
"devDependencies": {
"gulp-concat": "~2.2.0",
"gulp-uglify": "~0.2.1",
"gulp-jshint": "~1.5.1",
"gulp": "~3.5.6"
}
}
```
## gulp-livereload模块
gulp-livereload模块用于自动刷新浏览器,反映出源码的最新变化。它除了模块以外,还需要在浏览器中安装插件,用来配合源码变化。
```javascript
var gulp = require('gulp'),
less = require('gulp-less'),
livereload = require('gulp-livereload'),
watch = require('gulp-watch');
gulp.task('less', function() {
gulp.src('less/*.less')
.pipe(watch())
.pipe(less())
.pipe(gulp.dest('css'))
.pipe(livereload());
});
```
上面代码监视less文件,一旦编译完成,就自动刷新浏览器。
<h2 id="8.3">Browserify:浏览器加载Node.js模块</h2>
随着JavaScript程序逐渐模块化,在ECMAScript 6推出官方的模块处理方案之前,有两种方案在实践中广泛采用:一种是AMD模块规范,针对模块的异步加载,主要用于浏览器端;另一种是CommonJS规范,针对模块的同步加载,主要用于服务器端,即node.js环境。
Browserify是一个node.js模块,主要用于改写现有的CommonJS模块,使得浏览器端也可以使用这些模块。使用下面的命令,在全局环境下安装Browserify。
```bash
$ npm install -g browserify
```
## 基本用法
先看一个例子。假定有一个很简单的CommonJS模块文件foo.js。
```javascript
// foo.js
module.exports = function(x) {
console.log(x);
};
```
然后,还有一个main.js文件,用来加载foo模块。
```javascript
// main.js
var foo = require("./foo");
foo("Hi");
```
使用Browserify,将main.js转化为浏览器可以加载的脚本compiled.js。
```bash
browserify main.js > compiled.js
# 或者
browserify main > compiled.js
# 或者
browserify main.js -o compiled.js
```
之所以转化后的文件叫做compiled.js,是因为该文件不仅包括了main.js,还包括了它所依赖的foo.js。两者打包在一起,保证浏览器加载时的依赖关系。
```html
<script src="compiled.js"></script>
```
使用上面的命令,在浏览器中运行compiled.js,控制台会显示Hi。
我们再看一个在服务器端的backbone模块转为客户端backbone模块的例子。先安装backbone和它所依赖的jQuery模块。
```bash
npm install backbone jquery
```
然后,新建一个main.js文件。
```javascript
// main.js
var Backbone = require('backbone');
var $ = Backbone.$ = require('jquery/dist/jquery')(window);
var AppView = Backbone.View.extend({
render: function(){
$('main').append('<h1>Browserify is a great tool.</h1>');
}
});
var appView = new AppView();
appView.render();
```
接着,使用browserify将main.js转为app.js。
```bash
browserify main.js -o app.js
```
app.js就可以直接插入HTML网页了。
```html
<script src="app.js"></script>
```
注意,只要插入app.js一个文件就可以了,完全不需要再加载backbone.js和jQuery了。
## 管理前端模块
Browserify的主要作用是将CommonJS模块转为浏览器可以调用的格式,但是纯粹的前端模块,也可以用它打包。
首先,新建一个项目目录,添加package.json文件。
```javascript
{
"name": "demo",
"version": "1.0.0"
}
```
接着,新建index.html。
```html
<!doctype html>
<html>
<head>
<title>npm and jQuery demo</title>
</head>
<body>
<span class="title-tipso tipso_style" title="This is a loaded TIPSO!">
Roll over to see the tip
</span>
<script src="./bundle.js"/>
</body>
</html>
```
上面代码中的bundle.js,就是Browserify打包后将生成的文件。
然后,安装jquery和它的插件。
```javascript
$ npm install --save jquery tipso
```
接着,新建一个文件entry.js。
```javascript
global.jQuery = require('jquery');
require('tipso');
jQuery(function(){
jQuery('.title-tipso').tipso();
});
```
上面的文件中,第一行之所以要把jQuery写成global的属性,是为了转码之后,它可以变成一个全局变量。
最后,Browserify打包。
```bash
$ browserify entry.js --debug > bundle.js
```
上面代码中,--debug参数表示在打包后的文件中加入source map以便除错。
这时,浏览器打开index.html,脚本已经可以运行。如果不希望将jQuery一起打包,而是通过CDN加载,可以使用browserify-shim模块。
另外一个问题是,某些jQuery插件还有自带的CSS文件,这时可以安装parcelify模块。
```bash
$ npm install -g parcelify
```
然后,在package.json中写入规则,声明CSS文件的位置。
```javascript
"style": [
"./node_modules/tipso/src/tipso.css"
]
```
接着,运行parcelify进行CSS打包。
```bash
$ parcelify entry.js -c bundle.css
```
最后,将打包后的CSS文件插入index.html。
```html
<link rel="stylesheet" href="bundle.css" />
```
## 生成前端模块
有时,我们只是希望将node.js的模块,移植到浏览器,使得浏览器端可以调用。这时,可以采用browserify的-r参数(--require的简写)。
```bash
browserify -r through -r ./my-file.js:my-module > bundle.js
```
上面代码将through和my-file.js(后面的冒号表示指定模块名为my-module)都做成了模块,可以在其他script标签中调用。
```html
<script src="bundle.js"></script>
<script>
var through = require('through');
var myModule = require('my-module');
/* ... */
</script>
```
可以看到,-r参数的另一个作用,就是为浏览器端提供require方法。
## 脚本文件的实时生成
Browserify还可以实时生成脚本文件。
下面是一个服务器端脚本,启动Web服务器之后,外部用户每次访问这个脚本,它的内容是实时生成的。
```javascript
var browserify = require('browserify');
var http = require('http');
http.createServer(function (req, res) {
if (req.url === '/bundle.js') {
res.setHeader('content-type', 'application/javascript');
var b = browserify(__dirname + '/main.js').bundle();
b.on('error', console.error);
b.pipe(res);
}
else res.writeHead(404, 'not found')
});
```
## browserify-middleware模块
上面是将服务器端模块直接转为客户端脚本,然后在网页中调用这个转化后的脚本文件。还有一种思路是,在运行时动态转换模块,这就需要用到[browserify-middleware模块](https://github.com/ForbesLindesay/browserify-middleware)。
比如,网页中需要加载app.js,它是从main.js转化过来的。
```html
<!-- index.html -->
<script src="app.js"></script>
```
你可以在服务器端静态生成一个app.js文件,也可以让它动态生成。这就需要用browserify-middleware模块,服务器端脚本要像下面这样写。
```javascript
var browserify = require('browserify-middleware');
var express = require('express');
var app = express();
app.get('/app.js', browserify('./client/main.js'));
app.get('/', function(req, res){
res.render('index.html');
});
```
<h2 id="8.4">Source Map</h2>
## 概述
随着JavaScript脚本变得越来越复杂,大部分源码(尤其是各种函数库和框架)都要经过转换,才能投入生产环境。
常见的源码转换,主要是以下三种情况:
- 压缩,减小体积。比如jQuery 1.9的源码,压缩前是252KB,压缩后是32KB。
- 多个文件合并,减少HTTP请求数。
- 其他语言编译成JavaScript。最常见的例子就是CoffeeScript。
这三种情况,都使得实际运行的代码不同于开发代码,除错(debug)变得困难重重。
通常,JavaScript的解释器会告诉你,第几行第几列代码出错。但是,这对于转换后的代码毫无用处。举例来说,jQuery 1.9压缩后只有3行,每行3万个字符,所有内部变量都改了名字。你看着报错信息,感到毫无头绪,根本不知道它所对应的原始位置。
这就是Source map想要解决的问题。
简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。
有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。
目前,暂时只有Chrome浏览器支持这个功能。在Developer Tools的Setting设置中,确认选中"Enable source maps"。
## 生成和启用
生成Source Map的最常用方法,是使用Google的[Closure编译器](https://developers.google.com/closure/compiler/)。
生成命令的格式如下:
```java
java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js
```
各个参数的意义如下:
- js: 转换前的代码文件
- create_source_map: 生成的source map文件
- source_map_format:source map的版本,目前一律采用V3。
- js_output_file: 转换后的代码文件。
其他的生成方法可以参考[这篇文章](http://net.tutsplus.com/tutorials/tools-and-tips/source-maps-101/)。
启用Source map的方法很简单,只要在转换后的代码头部或尾部,加上一行就可以了。
```javascript
//# sourceMappingURL=/path/to/file.js.map
```
或者
```javascript
/*# sourceMappingURL=/path/to/file.js.map */
```
map文件可以放在网络上,也可以放在本地文件系统。
## 格式
打开Source map文件,它大概是这个样子:
```javascript
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
```
整个文件就是一个JavaScript对象,可以被解释器读取。它主要有以下几个属性:
- version:Source map的版本,目前为3。
- file:转换后的文件名。
- sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
- sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。
- names:转换前的所有变量名和属性名。
- mappings:记录位置信息的字符串。
## mappings属性
转换前后的代码一一对应的关键,就是map文件的mappings属性。这是一个很长的字符串,它分成三层。
第一层是行对应,以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
第二层是位置对应,以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
第三层是位置转换,以[VLQ编码](http://en.wikipedia.org/wiki/Variable-length_quantity)表示,代表该位置对应的转换前的源码位置。
举例来说,假定mappings属性的内容如下:
```javascript
mappings:"AAAAA,BBBBB;CCCCC"
```
它表示,转换后的源码分成两行,第一行有两个位置,第二行有一个位置。
每个位置使用五位,表示五个字段。从左边算起,
- 第一位,表示这个位置在(转换后的代码的)的第几列。
- 第二位,表示这个位置属于sources属性中的哪一个文件。
- 第三位,表示这个位置属于转换前代码的第几行。
- 第四位,表示这个位置属于转换前代码的第几列。
- 第五位,表示这个位置属于names属性中的哪一个变量。
有几点需要说明。首先,所有的值都是以0作为基数的。其次,第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位。再次,每一位都采用VLQ编码表示;由于VLQ编码是变长的,所以每一位可以由多个字符构成。
如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。
## VLQ编码
这种编码最早用于MIDI文件,后来被多种格式采用。它的特点就是可以非常精简地表示很大的数值。
VLQ编码是变长的。如果(整)数值在-15到+15之间(含两个端点),用一个字符表示;超出这个范围,就需要用多个字符表示。它规定,每个字符使用6个两进制位,正好可以借用[Base 64编码](http://en.wikipedia.org/wiki/Base_64)的字符表。
在这6个位中,左边的第一位(最高位)表示是否"连续"(continuation)。如果是1,代表这6个位后面的6个位也属于同一个数;如果是0,表示该数值到这6个位结束。
这6个位中的右边最后一位(最低位)的含义,取决于这6个位是否是某个数值的VLQ编码的第一个字符。如果是的,这个位代表"符号"(sign),0为正,1为负(Source map的符号固定为0);如果不是,这个位没有特殊含义,被算作数值的一部分。
```bash
Continuation
| Sign
| |
V V
101011
```
下面举例如何对数值16进行VLQ编码。
(1) 将16改写成二进制形式10000。
(2) 在最右边补充符号位。因为16大于0,所以符号位为0,整个数变成100000。
(3) 从右边的最低位开始,将整个数每隔5位,进行分段,即变成1和00000两段。如果最高位所在的段不足5位,则前面补0,因此两段变成00001和00000。
(4) 将两段的顺序倒过来,即00000和00001。
(5) 在每一段的最前面添加一个"连续位",除了最后一段为0,其他都为1,即变成100000和000001。
(6) 将每一段转成Base 64编码。查表可知,100000为g,000001为B。因此,数值16的VLQ编码为gB。
上面的过程,看上去好像很复杂,做起来其实很简单,具体的实现可以参考官方的[base64-vlq.js](https://github.com/mozilla/source-map/blob/master/lib/source-map/base64-vlq.js)文件,里面有详细的注释。
<h2 id="8.5">JavaScript 程序测试</h2>
## 为什么要写测试?
Web应用程序越来越复杂,这意味着有更多的可能出错。测试是帮助我们提高代码质量、降低错误的最好方法和工具之一。
- 测试可以确保得到预期结果。
- 加快开发速度。
- 方便维护。
- 提供用法的文档。
通过测试提供软件的质量,在开始的时候,可能会降低开发速度。但是从长期看,尤其是那种代码需要长期维护、不断开发的情况,测试会大大加快开发速度,减轻维护难度。
## 测试的类型
### 单元测试
单元测试(unit testing)指的是以软件的单元(unit)为单位,对软件进行测试。单元可以是一个函数,也可以是一个模块或组件。它的基本特征就是,只要输入不变,必定返回同样的输出。
“单元测试”这个词,本身就暗示,软件应该以模块化结构存在。每个模块的运作,是独立于其他模块的。一个软件越容易写单元测试,往往暗示着它的模块化结构越好,各模块之间的耦合就越弱;越难写单元测试,或者每次单元测试,不得不模拟大量的外部条件,很可能暗示软件的模块化结构越差,模块之间存在较强的耦合。
单元测试的要求是,每个模块都必须有单元测试,而软件由模块组成。
单元测试通常采取断言(assertion)的形式,也就是测试某个功能的返回结果,是否与预期结果一致。如果与预期不一致,就表示测试失败。
单元测试是函数正常工作、不出错的最基本、最有效的方法之一。 每一个单元测试发出一个特定的输入到所要测试的函数,看看函数是否返回预期的输出,或者采取了预期的行动。单元测试证明了所测试的代码行为符合预期。
单元测试有助于代码的模块化,因此有助于长期的重用。因为有了测试,你就知道代码是可靠的,可以按照预期运行。从这个角度说,测试可以节省开发时间。单元测试的另一个好处是,有了测试,就等于就有了代码功能的文档,有助于其他开发者了解代码的意图。
单元测试应该避免依赖性问题,比如不存取数据库、不访问网络等等,而是使用工具虚拟出运行环境。这种虚拟使得测试成本最小化,不用花大力气搭建各种测试环境。
一般来说,单元测试的步骤如下。
- 准备所有的测试条件
- 调用(触发)所要测试的函数
- 验证运行结果是否正确
- 还原被修改的记录
### 其他测试类型
(1)集成测试
集成测试(Integration test)指的是多个部分在一起测试,比如测试一个数据库连接模块,是否能够连接数据库。
(2)功能测试
功能测试(Functional test)指的是,自动测试整个应用程序的某个功能,比如使用Selenium工具自动打开浏览器运行程序。
(3)端对端测试
端对端测试(End-to-End testing)指的是全链路测试,即从开始端到终止端的测试,比如测试从用户界面、通过网络、经过应用程序处理、到达数据库,是否能够返回正确结果。端对端测试的目的是,确保整个系统能够正常运行,各个子系统之间依赖关系正常,数据能够在子系统之间、模块之间正确传递。
(4)冒烟测试
冒烟测试(smoke testing)指的是,正式的全面测试开始之前,对主要功能进行的预测试。它的主要目的是,确认主要功能能否满足需要,软件是否能运行。冒烟测试可以是手工测试,也可以是自动化测试。
这个名字最早来自对电子元件的测试,第一次对电子元件通电,看看它是否会冒烟。如果没有冒烟,说明通过了测试;如果电流达到某个临界点之后,才出现冒烟,这时可以评估是否能够接受这个临界点。
## 开发模式
测试不仅能够验证软件功能、保证代码质量,也能够影响软件开发的模式。
### TDD
TDD是“测试驱动的开发”(Test-Driven Development)的简称,指的是先写好测试,然后再根据测试完成开发。使用这种开发方式,会有很高的测试覆盖率。
TDD的开发步骤如下。
- 先写一个测试。
- 写出最小数量的代码,使其能够通过测试。
- 优化代码。
- 重复前面三步。
TDD开发的测试覆盖率通常在90%以上,这意味着维护代码和新增特性会非常容易。因为测试保证了你可以信任这些代码,修改它们不会破坏其他代码的运行。
TDD接口提供以下四个方法。
- suite()
- test()
- setup()
- teardown()
下面代码是测试计数器是否加1。
```javascript
suite('Counter', function() {
test('tick increases count to 1', function() {
var counter = new Counter();
counter.tick();
assert.equal(counter.count, 1);
});
});
```
### BDD
BDD是“行为驱动的开发”(Behavior-Driven Development)的简称,指的是写出优秀测试的最佳实践的总称。
BDD认为,不应该针对代码的实现细节写测试,而是要针对行为写测试。BDD测试的是行为,即软件应该怎样运行。
BDD接口提供以下六个方法。
- describe()
- it()
- before()
- after()
- beforeEach()
- afterEach()
下面是测试计数器是否加1的BDD写法。
```javascript
describe('Counter', function() {
it('should increase count by 1 after calling tick', function() {
var counter = new Counter();
var expectedCount = counter.count + 1;
counter.tick();
assert.equal(counter.count, expectedCount);
});
});
```
下面是一个BDD开发的示例。现在,需要开发一个`Foo`类,该类的实例有一个`sayHi`方法,会对类参数说“Hi”。这就是`Foo`类的规格,根据这个规格,我们可以写出测试用例文件`foo.spec.js`。
```javascript
describe('Simple object', function() {
var foo;
beforeEach(function() {
foo = new Foo('John');
});
it('should say hi', function() {
expect(foo.sayHi()).toEqual('John says hi!');
});
});
```
有了测试用例以后,我们再写出实际的脚本文件`foo.js`。
```javascript
function Foo(name) {
this.name = name;
}
Foo.prototype.sayHi = function() {
return this.name + ' says hi!';
};
```
为了把测试用例与脚本文件分开,我们通常把测试用例放在`test`子目录之中。然后,我们就可以使用Mocha、Jasmine等测试框架,执行测试用例,看看脚本文件是否通过测试。
### BDD术语
(1)测试套件
测试套件(test suite)指的是,一组针对软件规格的某个方面的测试用例。也可以看作,对软件的某个方面的描述(describe)。
测试套件由一个`describe`函数构成,它接受两个参数:第一个参数是字符串,表示测试套件的名字或标题,表示将要测试什么;第二个参数是函数,用来实现这个测试套件。
```javascript
describe("A suite", function() {
// ...
});
```
(2)测试用例
测试用例(test case)指的是,针对软件一个功能点的测试,是软件测试的最基本单位。一组相关的测试用例,构成一个测试套件。测试用例由`it`函数构成,它与`describe`函数一样,接受两个参数:第一个参数是字符串,表示测试用例的标题;第二个参数是函数,用来实现这个测试用例。
```javascript
describe("A suite", function() {
it("contains spec with an expectation", function() {
// ...
});
});
```
(3)断言
断言(assert)指的是对代码行为的预期。一个测试用例内部,包含一个或多个断言(assert)。
断言会返回一个布尔值,表示代码行为是否符合预期。测试用例之中,只要有一个断言为false,这个测试用例就会失败,只有所有断言都为`true`,测试用例才会通过。
```javascript
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
```
## 断言
断言是判断实际值与预期值是否相等的工具。
断言有assert、expext、should三种风格,或者称为三种写法。
```javascript
// assert风格
assert.equal(event.detail.item, '(item)‘);
// expect风格
expect(event.detail.item).to.equal('(item)');
// should风格
event.detail.item.should.equal('(item)');
```
Chai.js是一个很流行的断言库,同时支持上面三种风格。
(1) assert风格
```javascript
var assert = require('chai').assert;
var foo = 'bar';
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
assert.typeOf(foo, 'string', 'foo is a string');
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');
```
上面代码中,assert方法的最后一个参数是错误提示信息,只有测试没有通过时,才会显示。
(2)expect风格
```javascript
var expect = require('chai').expect;
var foo = 'bar';
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(beverages).to.have.property('tea').with.length(3);
```
(3)should风格
```javascript
var should = require('chai').should();
var foo = 'bar';
var beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
beverages.should.have.property('tea').with.length(3);
```
## Mocha.js
### 概述
Mocha(发音“摩卡”)是现在最流行的前端测试框架之一,此外常用的测试框架还有[Jasmine](http://jasmine.github.io/)、[Tape](https://github.com/substack/tape/)、[zuul](https://github.com/defunctzombie/zuul/)等。所谓“测试框架”,就是运行测试的工具。
Mocha使用下面的命令安装。
```bash
# 全局安装
$ npm install -g mocha chai
# 项目内安装
$ npm i -D mocha chai
```
上面代码中,除了安装Mocha以外,还安装了断言库`chai`,这是因为Mocha自身不带断言库,必须安装外部断言库。
测试套件文件一般放在`test`子目录下面,配置文件`mocha.opts`也放在这个目录里面。
### 浏览器测试
使用浏览器测试时,先用`mocha init`命令在指定目录生成初始化文件。
```bash
$ mocha init <path>
```
运行上面命令,就会在该目录下生成一个`index.html`文件,以及配套的脚本和样式表。
```html
<!DOCTYPE html>
<html>
<head>
<title>Unit.js tests in the browser with Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="mocha.css" />
</head>
<body>
<h1>Unit.js tests in the browser with Mocha</h1>
<div id="mocha"></div>
<script src="mocha.js"></script>
<script>
mocha.setup('bdd');
</script>
<script src="tests.js"></script>
<script>
mocha.run();
</script>
</body>
</html>
```
然后在该文件中,加入你要测试的文件(比如`app.js`)、测试脚本(`app.spec.js`)和断言库(`chai.js`)。
```html
<script src="app.js"></script>
<script src="http://chaijs.com/chai.js"></script>
<script src="app.spec.js"></script>
```
各个文件的内容如下。
```javascript
// app.js
function add(x, y){
return x + y;
}
// app.spec.js
var expect = chai.expect;
describe('测试add函数', function () {
it('1加1应该等于2', function () {
expect(add(1, 1)).to.equal(2);
});
});
```
### 命令行测试
Mocha除了在浏览器运行,还可以在命令行运行。
还是使用上面的文件,作为例子,但是要改成CommonJS格式。
```javascript
// app.js
function add(x, y){
return x + y;
}
module.exports = add;
// app.spec.js
var expect = require('chai').expect;
var add = require('../app');
describe('测试add函数', function () {
it('1加1应该等于2', function () {
expect(add(1, 1)).to.equal(2);
});
});
```
然后,在命令行下执行`mocha`,就会执行测试。
```bash
$ mocha
```
上面的命令等同于下面的形式。
```bash
$ mocha test --reporter spec --recursive --growl
```
### mocha.opts
所有Mocha的命令行参数,都可以写在`test`目录下的配置文件`mocha.opts`之中。
下面是一个典型的配置文件。
```javascript
--reporter spec
--recursive
--growl
```
上面三个设置的含义如下。
- 使用spec报告模板
- 包括子目录
- 打开桌面通知插件growl
如果希望测试非存放于test子目录的测试用例,可以在`mocha.opts`写入以下内容。
```bash
server-tests
--recursive
```
上面代码指定运行`server-tests`目录及其子目录之中的测试脚本。
### 生成规格文件
Mocha支持从测试用例生成规格文件。
```bash
$ mocha test/app.spec.js -R markdown > spec.md
```
上面命令生成单个`app.spec.js`规格。
生成HTML格式的报告,使用下面的命令。
```bash
$ mocha test/app.spec.js -R doc > spec.html
```
如果要生成整个`test`目录,对应的规格文件,使用下面的命令。
```bash
$ mocha test -R markdown > spec.md --recursive
```
只要提供测试脚本的路径,Mocha就可以运行这个测试脚本。
```javascript
$ mocha -w src/index.test.js
```
上面命令运行测试脚本`src/index.test.js`,参数`-w`表示watch,即当这个脚本一有变动,就会运行。
指定测试脚本时,可以使用通配符,同时指定多个文件。
```bash
$ mocha --reporter spec spec/{my,awesome}.js
$ mocha --ui tdd test/unit/*.js etc
```
上面代码中,参数`--reporter`指定生成的报告格式(上面代码是spec格式),`-ui`指定采用哪一种测试模式(上面代码是tdd模式)。
除了使用shell通配符,还可以使用node通配符。
```bash
$ mocha --compilers js:babel-core/register 'test/**/*.@(js|jsx)'
```
上面代码指定运行`test`目录下面任何子目录中,文件后缀名为`js`或`jsx`的测试脚本。注意,Node的通配符要放在单引号之中,因为否则星号(`*`)会先被shell解释。
如果要改用shell通配符,执行`test`目录下面任何子目录的测试脚本,要写成下面这样。
```bash
$ mocha test/**.js
```
如果测试脚本不止一个,最好将它们放在专门的目录当中。Mocha默认执行`test`目录的测试脚本,所以可以将所有测试脚本放在`test`子目录。`--recursive`参数可以指定运行子目录之中的测试脚本。
```bash
$ mocha --recursive
```
上面命令会运行`test`子目录之中的所有测试脚本。
`--grep`参数用于搜索测试用例的名称(即it方法的第一个参数),然后只执行匹配的测试用例。
```bash
$ mocha --reporter spec --grep "Fnord:" server-test/*.js
```
上面代码只测试名称中包含“Fnord:”的测试用例。
`--invert`参数表示只运行不符合条件的测试脚本。
```bash
$ mocha --grep auth --invert
```
如果测试脚本用到了ES6语法,还需要用`--compiler`参数指定babel进行转码。
```bash
$ mocha --compilers js:babel/register --recursive
```
上面命令会在运行测试脚本之前,先用Babel进行转码。`--compilers`参数的值是用冒号分隔的一个字符串,冒号左边是文件的后缀名,右边是用来处理这一类文件的模块名。上面代码表示,运行测试之前,先用`babel/register`模块,处理一下JS文件。
`--require`参数指定测试脚本默认包含的文件。下面是一个`test_helper.js`文件。
```javascript
// test/test_helper.js
import chai from 'chai';
```
使用`--require`参数,将上面这个脚本包含进所有测试脚本。
```bash
$ mocha --compilers js:babel/register --require ./test/test_helper.js --recursive
```
### 测试脚本的写法
测试脚本中,describe方法和it方法都允许调用only方法,表示只运行某个测试套件或测试用例。
```javascript
// 例一
describe('Array', function(){
describe.only('#indexOf()', function(){
...
});
});
// 例二
describe("using only", function() {
it.only("this is the only test to be run", function() {
});
it("this is not run", function() {
});
});
```
上面代码中,只有带有`only`方法的测试套件或测试用例会运行。
describe方法和it方法还可以调用skip方法,表示跳过指定的测试套件或测试用例。
```javascript
// 例一
describe.skip('Article', function() {
// ...
});
// 例二
describe("using only", function() {
it.skip("this is the only test to be run", function() {
});
it("this is not run", function() {
});
});
```
上面代码中,带有`skip`方法的测试套件或测试用例会被忽略。
如果测试用例包含异步操作,可以done方法显式指定测试用例的运行结束时间。
```javascript
it('logs a', function(done) {
var f = function(){
console.log('logs a');
done();
};
setTimeout(f, 500);
});
```
上面代码中,正常情况下,函数f还没有执行,Mocha就已经结束运行了。为了保证Mocha等到测试用例跑完再结束运行,可以手动调用done方法
## Promise的测试
对于异步的测试,测试用例之中,通常必须调用`done`方法,显式表明异步操作的结束。
```javascript
var expect = require('chai').expect;
it('should do something with promises', function(done) {
var result = asyncTest();
result.then(function(data) {
expect(data).to.equal('foobar');
done();
}, function(error) {
assert.fail(error);
done();
});
});
```
上面代码之中,Promise对象的`then`方法之中,必须指定`reject`时的回调函数,并且使用`assert.fail`方法抛出错误,否则这个错误就不会被外界感知。
```javascript
result.then(function(data) {
expect(data).to.equal(blah);
done();
});
```
上面代码之中,如果Promise被`reject`,是不会被捕获的,因为Promise之中的错误,不会”泄漏“到外界。
Mocha内置了对Promise的支持。
```javascript
it('should fail the test', function() {
var p = Promise.reject('Promise被reject');
return p;
});
```
上面代码中,Mocha能够捕获`reject`的Promise。
因此,使用Mocha时,Promise的测试可以简化成下面的写法。
```javascript
var expect = require('chai').expect;
it('should do something with promises', function() {
var result = asyncTest();
return result.then(function(data) {
expect(data).to.equal('foobar');
});
});
```
## 模拟数据
单元测试时,很多时候,测试的代码会请求HTTP服务器。这时,我们就需要模拟服务器的回应,不能在单元测试时去请求真实服务器数据,否则就不叫单元测试了,而是连同服务器一起测试了。
一些工具库可以模拟服务器回应。
- [nock](https://github.com/pgte/nock)
- [sinon](http://sinonjs.org/docs/#server)
- [faux-jax](https://github.com/algolia/faux-jax)
- [MITM](https://github.com/moll/node-mitm)
## 覆盖率
测试的覆盖率需要安装istanbul模块。
```bash
$ npm i -D istanbul
```
然后,在package.json设置运行覆盖率检查的命令。
```javascript
"scripts": {
"test:cover": "istanbul cover -x *.test.js _mocha -- -R spec src/index.test.js",
"check-coverage": "istanbul check-coverage --statements 100 --branches 100 --functions 100 --lines 100"
}
```
上面代码中,`test:cover`是生成覆盖率报告,`check-coverage`是设置覆盖率通过的门槛。
然后,将`coverage`目录写入`.gitignore`防止连这个目录一起提交。
如果希望在`git commit`提交之前,先运行一次测试,可以安装ghooks模块,配置`pre-commit`钩子。
安装ghooks。
```bash
$ npm i -D ghooks
```
在package.json之中,配置`pre-commit`钩子。
```javascript
"config": {
"ghooks": {
"pre-commit": "npm run test:cover && npm run check-coverage"
}
}
```
还可以把覆盖率检查,加入`.travis.yml`文件。
```bash
script:
- npm run test:cover
- npm run check-coverage
```
如果测试脚本使用ES6,`scripts`字段还需要加入Babel转码。
```javascript
"scripts": {
"test": "mocha src/index.test.js -w --compilers js:babel/register",
"test:cover": "istanbul cover -x *.test.js _mocha -- -R spec src/index.test.js --compilers js:babel/register"
}
```
覆盖率报告可以上传到[codecov.io](https://codecov.io/)。先安装这个模块。
```bash
$ npm i -D codecov.io
```
然后在package.json增加一个字段。
```javascript
"scripts": {
"report-coverage": "cat ./coverage/lcov.info | codecov"
}
```
最后,在CI的配置文件`.travis.yml`之中,增加运行这个命令。
```
after_success:
- npm run report-coverage
- npm run semantic-release
```
## WebDriver
WebDriver是一个浏览器的自动化框架。它在各种浏览器的基础上,提供一个统一接口,将接收到的指令转为浏览器的原生指令,驱动浏览器。
WebDriver由Selenium项目演变而来。Selenium是一个测试自动化框架,它的1.0版叫做Selenium RC,通过一个代理服务器,将测试脚本转为JavaScript脚本,注入不同的浏览器,再由浏览器执行这些脚本后返回结果。WebDriver就是Selenium 2.0,它对每个浏览器提供一个驱动,测试脚本通过驱动转换为浏览器原生命令,在浏览器中执行。
### 定制测试环境
DesiredCapabilities对象用于定制测试环境。
- 定制DesiredCapabilities对象的各个属性
- 创建DesiredCapabilities实例
- 将DesiredCapabilities实例作为参数,新建一个WebDriver实例
### 操作浏览器的方法
WebDriver提供以下方法操作浏览器。
close():退出或关闭当前浏览器窗口。
```javascript
driver.close();
```
quit():关闭所有浏览器窗口,中止当前浏览器driver和session。
```javascript
driver.quit();
```
getTitle():返回当前网页的标题。
```javascript
driver.getTitle();
```
getCurrentUrl():返回当前网页的网址。
```javascript
driver.getCurrentUrl();
```
getPageSource():返回当前网页的源码。
```javascript
// 断言是否含有指定文本
assert(driver.getPageSource().contains("Hello World"),
"预期含有文本Hello World");
```
click():模拟鼠标点击。
```javascript
// 例一
driver.findElement(By.locatorType("path"))
.click();
// 例二
driver.get("https://www.google.com");
driver.findElement(By.name("q"))
.sendKeys("webDriver");
driver.findElement(By.id("sblsbb"))
.click();
```
clear():清空文本输入框。
```javascript
// 例一
driver.findElement(By.locatorType("path")).clear();
// 例二
driver.get("https://www.google.com");
driver.findElement(By.name("q"))
.sendKeys("webDriver");
driver.findElement(By.name("q"))
.clear();
driver.findElement(By.name("q"))
.sendKeys("testing");
```
sendKeys():在文本输入框输入文本。
```javascript
driver.findElement(By.locatorType("path"))
.sendKeys("your text");
```
submit():提交表单,或者用来模拟按下回车键。
```javascript
// 例一
driver.findElement(By.locatorType("path"))
.submit();
// 例二
driver.get("https://www.google.com");
driver.findElement(By.name("q"))
.sendKeys("webdriver");
element.submit();
```
findElement():返回选中的第一个元素。
```javascript
driver.get("https://www.google.com");
driver.findElement(By.id("lst-ib"));
```
findElements():返回选中的所有元素(0个或多个)。
```javascript
// 例一
driver.findElement(By.id("searchbox"))
.sendKeys("webdriver");
driver.findElements(By.xpath("//div[3]/ul/li"))
.get(0)
.click();
// 例二
driver.findElements(By.tagName("select"))
.get(0)
.findElements(By.tagName("option"))
.get(3)
.click()
.get(4)
.click()
.get(5)
.click();
// 例三:获取页面所有链接
var links = driver
.get("https://www.google.com")
.findElements(By.tagName("a"));
var linkSize = links.size();
var linksSrc = [];
console.log(linkSize);
for(var i=0;i<linkSize;i++) {
linksSrc[i] = links.get(i).getAttribute("href");
}
for(int i=0;i<linkSize;i++){
driver.navigate().to(linksSrc[i]);
Thread.sleep(3000);
}
```
可以使用`size()`,查看到底选中了多少个元素。
### 网页元素的定位
WebDriver提供8种定位器,用于定位网页元素。
- By.id:HTML元素的id属性
- By.name:HTML元素的name属性
- By.xpath:使用XPath语法选中HTML元素
- By.cssSelector:使用CSS选择器语法
- By.className:HTML元素的class属性
- By.linkText:链接文本(只用于选中链接)
- By.tagName:HTML元素的标签名
- By.partialLinkText:部分链接文本(只用于选中链接)
下面是一个使用id定位器,选中网页元素的例子。
```javascript
driver.findElement(By.id("sblsbb")).click();
```
### 网页元素的方法
以下方法属于网页元素的方法,而不是webDriver实例的方法。需要注意的是,有些方法是某些元素特有的,比如只有文本框才能输入文字。如果在网页元素上调用不支持的方法,WebDriver不会报错,也不会给出给出任何提示,只会静静地忽略。
getAttribute():返回网页元素指定属性的值。
```javascript
driver.get("https://www.google.com");
driver.findElement(By.xpath("//div[@id='lst-ib']"))
.getAttribute("class");
```
getText():返回网页元素的内部文本。
```javascript
driver.findElement(By.locatorType("path")).getText();
```
getTagName():返回指定元素的标签名。
```javascript
driver.get("https://www.google.com");
driver.findElement(By.xpath("//div[@class='sbib_b']"))
.getTagName();
```
isDisplayed():返回一个布尔值,表示元素是否可见。
```javascript
driver.get("https://www.google.com");
assert(driver.findElement(By.name("q"))
.isDisplayed(),
'搜索框应该可选择');
```
isEnabled():返回一个布尔值,表示文本框是否可编辑。
```javascript
driver.get("https://www.google.com");
var Element = driver.findElement(By.name("q"));
if (Element.isEnabled()) {
driver.findElement(By.name("q"))
.sendKeys("Selenium Essentials");
} else {
throw new Error();
}
```
isSelected():返回一个布尔值,表示一个元素是否可选择。
```javascript
driver.findElement(By.xpath("//select[@name='jump']/option[1]"))
.isSelected()
```
getSize():返回一个网页元素的宽度和高度。
```javascript
var dimensions=driver.findElement(By.locatorType("path"))
.getSize();
dimensions.width;
dimensions.height;
```
getLocation():返回网页元素左上角的x坐标和y坐标。
```javascript
var point = driver.findElement(By.locatorType("path")).getLocation();
point.x; // 等同于 point.getX();
point.y; // 等同于 point.getY();
```
getCssValue():返回网页元素指定的CSS属性的值。
```javascript
driver.get("https://www.google.com");
var element = driver.findElement(By.xpath("//div[@id='hplogo']"));
console.log(element.getCssValue("font-size"));
console.log(element.getCssValue("font-weight"));
console.log(element.getCssValue("color"));
console.log(element.getCssValue("background-size"));
```
### 页面跳转的方法
以下方法用来跳转到某一个页面。
get():要求浏览器跳到某个网址。
```javascript
driver.get("URL");
```
navigate().back():浏览器回退。
```javascript
driver.navigate().back();
```
navigate().forward():浏览器前进。
```javascript
driver.navigate().forward();
```
navigate().to():跳转到浏览器历史中的某个页面。
```javascript
driver.navigate().to("URL");
```
navigate().refresh():刷新当前页面。
```javascript
driver.navigate().refresh();
// 等同于
driver.navigate()
.to(driver.getCurrentUrl());
// 等同于
driver.findElement(By.locatorType("path"))
.sendKeys(Keys.F5);
```
### cookie的方法
getCookies():获取cookie
```javascript
driver.get("https://www.google.com");
driver.manage().getCookies();
```
getCookieNamed() :返回指定名称的cookie。
```javascript
driver.get("https://www.google.com");
console.log(driver.manage().getCookieNamed("NID"));
```
addCookie():将cookie加入当前页面。
```javascript
driver.get("https://www.google.com");
driver.manage().addCookie(cookie0);
```
deleteCookie():删除指定的cookie。
```javascript
driver.get("https://www.google.co.in");
driver.manage().deleteCookieNamed("NID");
```
### 浏览器窗口的方法
maximize():最大化浏览器窗口。
```javascript
var driver = new FirefoxDriver();
driver.manage().window().maximize();
```
getSize():返回浏览器窗口、图像、网页元素的宽和高。
```javascript
driver.manage().window().getSize();
```
getPosition():返回浏览器窗口左上角的x坐标和y坐标。
```javascript
console.log("Position X: " + driver.manage().window().getPosition().x);
console.log("Position Y: " + driver.manage().window().getPosition().y);
console.log("Position X: " + driver.manage().window().getPosition().getX());
console.log("Position Y: " + driver.manage().window().getPosition().getY());
```
setSize():定制浏览器窗口的大小。
```javascript
var d = new Dimension(320, 480);
driver.manage().window().setSize(d);
driver.manage().window().setSize(new Dimension(320, 480));
```
setPosition():移动浏览器左上角到指定位置。
```javascript
var p = new Point(200, 200);
driver.manage().window().setPosition(p);
driver.manage().window().setPosition(new Point(300, 150));
```
getWindowHandle():返回当前浏览器窗口。
```javascript
var parentwindow = driver.getWindowHandle();
driver.switchTo().window(parentwindow);
```
getWindowHandles():返回所有浏览器窗口。
```javascript
var childwindows = driver.getWindowHandles();
driver.switchTo().window(childwindow);
```
switchTo.window():在浏览器窗口之间切换。
```javascript
driver.SwitchTo().Window(childwindow);
driver.close();
driver.SwitchTo().Window(parentWindow);
```
### 弹出窗口
以下方法处理浏览器的弹出窗口。
dismiss() :关闭弹出窗口。
```javascript
var alert = driver.switchTo().alert();
alert.dismiss();
```
accept():接受弹出窗口,相当于按下接受OK按钮。
```javascript
var alert = driver.switchTo().alert();
alert.accept();
```
getText():返回弹出窗口的文本值。
```javascript
var alert = driver.switchTo().alert();
alert.getText();
```
sendKeys():向弹出窗口发送文本字符串。
```javascript
var alert = driver.switchTo().alert();
alert.sendKeys("Text to be passed");
```
authenticateUsing():处理HTTP认证。
```javascript
var user = new UserAndPassword("USERNAME", "PASSWORD");
alert.authenticateUsing(user);
```
### 鼠标和键盘的方法
以下方法模拟鼠标和键盘的动作。
- click():鼠标在当前位置点击。
- clickAndHold():按下鼠标不放
- contextClick():右击鼠标
- doubleClick():双击鼠标
- dragAndDrop():鼠标拖放到目标元素
- dragAndDropBy():鼠标拖放到目标坐标
- keyDown():按下某个键
- keyUp():从按下状态释放某个键
- moveByOffset():移动鼠标到另一个坐标位置
- moveToElement():移动鼠标到另一个网页元素
- release():释放拖拉的元素
- sendKeys():控制键盘输出