<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                <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():控制鍵盤輸出
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看