企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
<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():控制键盘输出