讀古今文學網 > Vue2實踐揭秘 > 第2章 工程化的Vue.js開發 >

第2章 工程化的Vue.js開發

我們在上一章中只是用了一個非常簡單的例子,嘗試對Vue的一些基礎概念進行大面積的瞭解,而這個例子也只能跑在開發環境中,它的運行需要有NPM和NPM上的各種依賴包,所以我們沒有辦法直接將它放到某個服務器上就能運行,而且它的質量不高。因為在開發過程中我們只是通過視覺主觀地判斷它「能運行」,其中是否潛藏著缺陷我們並不可知。

我認識很多年輕的程序員,其中不乏前端和後端的開發能手,也有入行一兩年的新手,我很喜歡與他們交流,因為他們才是現今的開發主力,他們有激情和各種標新立異的想法。在各種的交流活動中我卻發現,大多數的前端程序員並不太喜歡談論測試與部署的話題,多數人認為測試很重要但沒有時間寫,因為項目時間太短了,某些人認為前端程序的部署不過就是個文件複製的過程,只是將本機上的代碼複製到服務器上行了,讓運維人員做就夠了,沒有必要浪費本就不多的開發時間。

幾年前,當jQuery還在大行其道之時,前端開發的工作只是負責美化頁面樣式,修改一些頁面的佈局,又或者使用一些jQuery插件來增強用戶界面的交互能力以提升使用體驗。可見,處在這種開發時代下的前端開發者可以說在團隊中是沒有什麼地位的,即使有再強的前端開發能力,充其量也只是個打下手的角色。幸運的是,AngularJS的出現在短短幾年間就掀起了一波瘋狂的前端技術革命,直到今天這場革命仍然在如火如荼地進行,繼而出現了各種形式的響應式編程框架,例如Polymer、Ember、Knockout、React。呈現著你方唱罷我方上台的格局,當然其中少不了本書的主角Vue。這波前端的革命浪潮將原本由後端技術所主導的交互頁面開發大量地被前端化,前端開發人員的地位也隨之水漲船高。能力越大責任越大,這種角色與職責的轉化帶來的是對前端開發體系、工具甚至是開發方法的一系列改變。在這樣的背景下,怎麼才能算是一名合格的前端工程師,怎樣才能成為一名優秀的前端工程師而不會成為這場風風火火的前端革命中的犧牲者呢?Vue給了我們一個很好的選擇。

選對了前端框架只是第一步,既然前端開發「搶奪」了大量本由後端處理的工作,那麼就意味著我們在前端開發過程中要融入一些與後端類似的流程,這樣才能真正地達至所謂的「前端開發工程化」。

除了開發,測試與部署可以說是我們在項目開發中的必經階段,下圖很好地詮釋了這三者之間的關係。

前端開發與後端開發的不同之處是開放性,換句話說,前端開發具有極強的選擇性,工具與開發框架的組合可以說是多得讓人眼花繚亂不可勝數。這種開放性帶來的多樣性選擇對於入門者而言無疑像面前橫亙了一條深不見底的鴻溝。如果你曾參與過基於AngularJS的開發項目,那你必然會體驗到搭建一個多人協作的開發、測試和部署環境如身陷泥潭般難以前行;當你進入到React項目也會有類似的感覺,不同的只是從工具的複雜性陷阱跳入了第三方依賴包所構建的陷阱。

Vue作為AngularJS和React的繼承者和改良者,它不單單從編碼的特色與開發框架本身的使用上進行大面積的改良與優化。而更實用之處也是它的優秀之處,在於它提供了一整套簡化開發、測試與部署的方案。作為它的前端開發者,不再需要花大量的時間去學習理解框架的使用概念,以及耗費大量的精力去建立複雜的自動化環境。

在此我特意以一章的篇幅講述Vue如何為我們建立開發、測試與部署的自動化環境,我們又將如何在它們的基礎上有針對性地進行定制與改良。

2.1 腳手架vue-cli

當我們使用Vue構建一個原型的時候,需要做的通常就是通過<script>把Vue.js引入進來,然後就可以在頁面上直接進行編碼了。這種情況僅僅作為一個實驗性的嘗試完全是可以的,但是真實的開發卻不能這樣做。

真正在前端開發時,不可避免地要用到一大堆的工具,例如模塊化的包管理工具,代碼運行前的預處理器,程序熱加載模塊,代碼校驗,還有各種的測試環境與框架支持工具等,這些工具對於一個需要長期維護或不斷地迭代演進的應用來說都是必需的。從項目初始化開始,安裝配置這些不同開發場景下的支撐工具是家常便飯,但卻又是非常讓人感到痛苦的。這些開發環境的支持工具很多,而且配置方式各不相同,如果沒有一定的使用經驗,在項目初始化時就配置一個具有良好擴展性的工具環境,這種安裝配置的工作就還會不斷地重複出現。

我很喜歡Vue的一個重要原因就是因為它的vue-cli,這個工具可以讓一個簡單的命令行工具來幫助我快速地構建一個足以支撐實際項目開發的Vue環境,並不像Angular和React那樣要在Yoman上找適合自己的第三方腳手架。vue-cli的存在將項目環境的初始化工作與複雜度降到了最低。

安裝vue-cli

vue-cli是一個npm的安裝包,我們希望它能在本機的任意目錄下創建項目,那麼就得將它安裝到node.js的全局運行目錄下:

    $ npm i vue-cli -g  

安裝成功後,我們就可以使用vue-cli來初始化Vue項目了。

使用vue-cli初始化項目

vue-cli是一個很簡單的指令,先打開它的幫助文件看看它的具體用法:

      用法: vue <命令> [選項]

      命令:

         init         從指定模板中生成一個新的項目
         list         列出所有的可用的官方模板
         help [cmd]   顯示所有[cmd](命令)的幫助

      選項:

         -h, --help       輸出用法信息
         -V, --version    輸出版本號  

先用list指令來看看有哪些官方模板可用:

    $ vue list  

輸出結果如下圖所示。

這些官方模板存在的意義在於提供強大的項目構建能力,用戶可以盡可能快地進行開發。然而能否真正地發揮作用還在於用戶如何組織代碼和使用的其他庫。

將list指令的輸出結果翻譯一下,就可以清楚地瞭解這些官方模板應用於哪些使用場景:

● browserify——擁有高級功能的Browserify + vueify用於正式開發;

● browserify-simple——擁有基礎功能的Browserify + vueify用於快速原型開發;

● simple——適用於單頁應用開發的最小化配置;

● webpack——擁有高級功能的webpack + vue-loader用於正式開發;

● webpack-simple——擁有基礎功能的webpack + vue-loader用於快速原型開發。

browserify的模板做得比較簡陋,就算是用於正式開發還是會有些不足,配置的是Karma+Jasmine的單元測試框架,而browserify屬於比較老舊的構建工具,估計官方提供這兩個模板頁是出於對經常使用browserify的開發人員提供一個熟悉環境的考慮。到了正式的項目開發時,我們還是會走上webpack的道路。

所以我建議初學者可以跳過browserify的兩個模板,直接使用webpack的兩個模板。首先webpack-simple正如其名,配置了最簡單的可直接支持ES6的Vue.js編譯環境,可以應對那些要求時間短,結構相對簡單的小型應用。如果對所有環境工具都非常熟悉,開發者也可以由這個模板入手,為項目底板定制更適應自身開發要求的環境。

其次,webpack模板是一個非常讚的腳手架,將其分析透徹之後,就會知道Vue的官方開發團隊在其中花了很大的功夫,將上文所敘述的開發、測試與生產環境做了非常完善的配置,從最大程度上簡化了由於工具而引入項目的複雜度,也降低了開發人員對工具的學習成本,這個模板也將是本書中講述的重點。

創建項目

接下來先看看這個vue-cli如何為我們創建項目。創建項目使用的是init命令,它會為我們自動創建一個新的文件夾,並將所需的文件、目錄、配置和依賴都準備好,具體做法如下:

    $ vue init webpack my-project  

init命令執行後會出一系列的交互式問題讓我們選擇,運行結果如下所示。

完成以後直接按提示進入項目,安裝npm的依賴包後就可以開始開發。

2.2 深入vue-cli的工程模板

vue-cli提供的腳手架只是一個最基礎的,也可以說是Vue團隊認為的工程結構的一種最佳實踐。對於初學者或者以前曾從事AngularJS/React開發的用戶來說,可能對開發環境有自已習慣性用法和熟悉的工具,但我建議用Vue來開發的話還是先按照官方推薦的來做,待我們掌握了Vue官方推薦的環境配置後再按照實際情況進行相應的調整,這樣會少走一些彎路,節省不少時間。

我們下面要討論的工程結構都是圍繞webpack-simple與webpack展開的,browserify也只是在這兩個模板的基礎上移植的一個版本,所以就不過多地贅述。

webpack和webpack-simple這兩個模板從文件結構上看幾乎是一致的,只是一個是簡化版,另一個是完全版。其實不然,webpack-simple是基於[email protected]進行配置的版本,而webpack模板則是基於Webpack ^1.3.2配置的。這兩個版本暫時是互相不兼容的,而且使用的依賴包的版本也不一樣,所以不要將webpack模板創建的項目文件結構複製到webpack-simple中進行直接的取代升級,而是需要將node_modules內安裝的所有的依賴包刪除,然後重新安裝才有可能遷移成功,這一點是需要注意的。

2.2.1 webpack-simple模板

以下為webpack-simple模板構建的項目的工程目錄結構:

    .
    ├── README.md
    ├── index.html
    ├── package.json
    ├── src
    │    ├── App.vue
    │    ├── assets
    │    │    └── logo.png
    │    └── main.js
    └── webpack.config.js  

webpack-simple只配置了Babel和Vue的編譯器,其他的一無所有。這個模板值得一提的就是src目錄,所有的Vue代碼源程序都放置在這個目錄中,五個模板構建出來的這個src目錄都是一樣的,只是在webpack模板中多了components目錄用於存放公用組件。這個目錄的結構與文件的組織應在開發前就進行約定,對於多人協作式項目,目錄的使用與文件的命名都顯得尤為重要。

具體約定如下:

(1)公共組件、指令、過濾器(多於三個文件以上的引用)將分別存放於src目錄下的

● components;

● directives;

● filters。

(2)以使用場景命名Vue的頁面文件。

(3)當頁面文件具有私有組件、指令和過濾器時,則建立一個與頁面同名的目錄,頁面文件更名為index.vue,將頁面與相關的依賴文件放在一起。

(4)目錄由全小寫的名詞、動名詞或分詞命名,由兩個以上的詞組成,以「-」進行分隔。

(5)Vue文件統一以大駝峰命名法命名,僅入口文件index.vue採用小寫。

(6)測試文件一律以測試目標文件名.spec.js命名。

(7)資源文件一律以小寫字符命名,由兩個以上的詞組成,以「-」進行分隔。

例如:

    src
    ├── README.md
    ├── assets                 // 全局資源目錄
    │    ├── images           // 圖片
    │    ├── less             // less 樣式表
    │    ├── css              // CSS 樣式表
    │    └── fonts            // 自定義字體文件
    ├── components             // 公共組件目錄
    │    ├── ImageInput.vue
    │    ├── Slider.vue
    │    └── ...
    ├── directives.js          // 公共指令
    ├── filters.js             // 公共過濾器
    ├── login                  // 場景:登錄
    │    ├── index.vue        // 入口文件
    │    ├── LoginForm.vue    // 登錄場景私有表單組件
    │    └── SocialLogin.vue
    ├── cart
    │    ├── index.vue
    │    ├── ItemList.vue
    │    └── CheckoutForm.vue
    ├── Discover.vue           // 場景入口文件
    ├── App.vue                // 默認程序入口
    └── main.js  

前端開發的文件非常零碎而且會隨著項目增多,組件化程度的增加使得文件越來越多,如果從一開始就沒有約定目錄與文件的使用與命名規範,項目越往後發展,要找到某個文件就越困難,各種古怪的名字也會隨之顯現,所以從項目一開始就確立工程的命名規範與使用約定是很有必要的。

2.2.2 webpack模板

webpack模板的工程目錄結構如下:

    .
    ├── README.md
    ├── build
    │    ├── build.js
    │    ├── check-versions.js
    │    ├── dev-client.js
    │    ├── dev-server.js
    │    ├── utils.js
    │    ├── webpack.base.conf.js
    │    ├── webpack.dev.conf.js
    │    └── webpack.prod.conf.js
    ├── config
    │    ├── dev.env.js
    │    ├── index.js
    │    ├── prod.env.js
    │    └── test.env.js
    ├── index.html
    ├── package.json
    ├── src
    │    ├── App.vue
    │    ├── assets
    │    │    └── logo.png
    │    ├── components
    │    │    └── Hello.vue
    │    └── main.js
    ├── static
    └── test
          ├── e2e
          │    ├── custom-assertions
          │    │    └── elementCount.js
          │    ├── nightwatch.conf.js
          │    ├── runner.js
          │    └── specs
          │          └── test.js
          └── unit
                ├── index.js
                ├── karma.conf.js
                └── specs
                      └── Hello.spec.js  

是不是覺得這個工程結構非常複雜?第一次使用的時候我也頓生此感,但這個webpack模板的結構是非常合理的,而且配置的工具也相當豐富,當投入真正的項目開發時會覺得模板的實用性很強。

所以我們很有必要花些時間將這個模板的結構以及它所提供的工具配置瞭解清楚,掌握Vue官方團隊對項目開發的環境配置與使用思路,以便於我們能結合自己的實際情況進行適當的配置與調整。

在上文中我們已經提過src目錄的用法與約定,此處就不再贅述。在項目的根目錄下多了4個目錄,它們的作用分別如下:

● build——存放用於編譯用的webpack配置與相關的輔助工具代碼;

● config——存放三大環境配置文件,用於設定環境變量和必要的路徑信息;

● test——存放E2E測試與單元測試文件以及相關的配置文件;

● static——存放項目所需要的其他靜態資源文件;

● dist——存放運行npm run build指令後的生產環境輸出文件,可直接部署到服務器對應的靜態資源文件夾內,該文件夾只有在運行build之後才會生成。

可見,這些目錄的存在是依賴於模板內配置的開發工具的,webpack模板配置以下的工具。

2.2.3 構建工具

由於開發、測試與生產三大運行環境都需要進行構建,而且針對不同的環境要求,它的配置會有一定的區別,本書後面的章節中我們會對具體的配置進行一些定制與修改,我們應該清楚地瞭解webpack模板是如何進行構建的。

1.編譯開發環境

在開發環境下通過以下指令加載運行Vue項目:

    $ npm run dev  

這個指令的配置是在package.json的script屬性中設置的,實質上它是由npm來引導執行入口程序dev-server.js完成以下的加載過程:

加載環境變量

該環節從config目錄加載index.js和dev.env.js兩個模塊,準備開發調試環境所必需的一些目錄和全局變量。

合併webpack配置

在build目錄下一共有三個webpack的配置文件:

● webpack.base.conf.js——公用的基本webpack配置;

● webpack.dev.conf.js——開發環境專用的webpack配置項;

● webpack.prod.conf.js——生產環境專用的webpack配置項。

這裡使用了一個叫webpack-merge的包來進行兩個webpack配置之間的合併,這個環節就是通過這個包將webpack.base.conf.js和webpack.dev.conf.js合併成最終的webpack配置。

請記住這幾個配置文件,在下面的章節中我們會對這些配置的內容進行調整。

配置熱加載

熱加載是一個非常棒的功能,這個功能啟用後的效果就是:當開發環境被啟動並進入調試模式後,一旦我們修改了任意地方的源代碼,瀏覽器中對應的內容就會被自動刷新,而無須手工對瀏覽器進行刷新的操作,這個配置將是我們做頁面佈局或者功能調整時的一大臂助。

上一個環境中合併的webpack配置也是通過這個環節被動態加載的,當代碼文件發生變化,熱加載就會啟動webpack進行重新編譯,然後將最新的編譯文件重新加載到瀏覽器中。

配置代理服務器

這個環境是為我們的代碼增加一個模擬的服務端做準備,有了它的存在,我們就可以在沒有後端程序支持的情況下,直接模擬遠程服務器執行的一些請求的效果。例如,向服務器發出一個HTTP GET/api/books/的請求,那麼我們就可以利用代理服務器將這一請求截獲下來,然後返回一組這個API應該執行成功的返回結果,這樣我們的前端程序運行起來的效果就與接入了服務端後的效果是一致的了。我們將這一技術稱為服務模擬,在後面的章節中會具體介紹這一技術。

配置靜態資源

將圖片、字體、樣式表和編譯後的JS腳本等,生成對應的一些印記(Footprint)並存放到由開發服務器托管的一個static虛目錄中,使得我們在瀏覽器中可以正常訪問到這些資源。每個生成的文件Footprint是一些哈希代碼,當文件內容發生變化時這些哈希代碼就會發生改變,使用Footprint是將靜態文件發佈到CDN或者進行離線緩衝時通知瀏覽器文件是否發生改變的重要依據。

加載開發服務器

啟動一個Express的Web服務器,將上述各個環境中配置好的模塊進行加載,並使程序能通過瀏覽器進行訪問。

以上就是npm run dev的完整執行思路。

2.編譯生產環境

當項目準備發佈時,在命令行鍵入:

    $ npm run build  

執行效果如下:

生產環境的構建過程比較簡單,首先是對必要的資源文件進行打包加上FootPrint,然後是對腳本進行編譯、壓縮和包大小的分割。

2.3 Vue工程的webpack配置與基本用法

我們在真實的Vue項目開發過程中,會因為很多不同的實際運用需求不斷地對webpack配置進行修改,在此之前,我們需要對webpack有一個基本的認識,瞭解它到底能為我們做些什麼。

webpack是一個模塊打包的工具,它的作用是把互相依賴的模塊處理成靜態資源,如下圖所示。

現有的模塊打包工具不適合大型項目(大型的SPA)的開發。當然最重要的還是因為缺少代碼分割功能,以及靜態資源需要通過模塊化來無縫銜接。webpack的作者曾經試圖對原有的打包工具進行擴展,但是沒能成功。webpack的目標:

● 把依賴樹按需分割;

● 把初始加載時間控制在較低的水平;

● 每個靜態資源都應該成為一個模塊;

● 能把第三方庫集成到項目裡成為一個模塊;

● 能定制模塊打包器的每個部分;

● 能適用於大型項目。

2.3.1 webpack的特點

代碼分割

在webpack的依賴樹裡有兩種類型的依賴:同步依賴和異步依賴。異步依賴會成為一個代碼分割點,並且組成一個新的代碼塊。在代碼塊組成的樹被優化之後,每個代碼塊都會保存在一個單獨的文件裡。

加載器

webpack原生是只能處理JavaScript的,而加載器的作用是把其他的代碼轉換成JavaScript代碼,這樣一來所有種類的代碼都能組成一個模塊,也就是說,我們可以在代碼內通過import將webpack打包的資源以模塊的方式引入到程序中。

以下是Vue項目中常用到的加載器(它們都是以NPM庫形式提供的):

● vue-loader——用於加載與編譯*.vue文件;

● vue-style-loader——用於加載*.vue文件中的樣式;

● style-loader——用於將樣式直接插入到頁面的<style>內;

● css-loader——用於加載*.css樣式表文件;

● less-loader——用於編譯與加載*.less文件(需要依賴於less庫);

● babel-loader——用於將ES6編譯成為瀏覽器兼容的ES5;

● file-loader——用於直接加載文件;

● url-loader——用於加載URL指定的文件,多用於字體與圖片的加載;

● json-loader——用於加載*.json文件為JS實例。

智能解析

webpack的智能解析器能處理幾乎所有的第三方庫,它甚至允許依賴裡出現這樣的表達式:

    require("./components/"+ name + ".vue")  

這一點恰恰是browserify不能做到的。

它能處理大多數的模塊系統,比如說CommonJS和AMD。

插件系統

webpack有豐富的插件系統,大多數內部的功能都是基於這個插件系統的。這也使得我們可以定制webpack,把它打造成能滿足我們需求的工具,並且把自己做的插件開源出去。

2.3.2 基本用法

webpack的打包依賴於它的一個重要配置文件webpack.config.js,在這個配置文件中就可以指定所有在源代碼編譯過程中的工作了!對,就一個配置就可以與冗長的Gruntfile或者Gulpfile說再見了。

下面就可直接完成打包的工作了,通過這樣精簡化的配置是不是感覺已經瞭解了webpack?當然一個完整的工程項目中的webpack的配置遠遠沒有這麼簡單,隨著工程的構建要求的增加,webpack.config.js內的配置項目也會隨之增加,webpack還有許許多多的選項提供給我們進行靈活配置,但這不是本書最重要的內容,它只是一個構建工具,我們只需要瞭解在Vue項目中它基本能為我們做到的工作、最小化的配置是如何的就足夠了,在以後需要對它進行擴展與優化時,帶著問題去查官方文檔也是非常容易的事。

樣式表引用

某些頁面或者組件可能具有特定的樣式定義,這些樣式對於其他頁面來說是冗余的,我們只希望這些組件在應用時才自動加載這些特定的樣式,此時用webpack我們就能在源代碼中加入以下代碼來動態加載CSS:

    import Vue from 'vue'
    // ... 省略
    // 引用指定的樣式源文件
    import './app/assets/less/dark.less'
    

export default {
    // ... 省略
    }  

此時我們只需要在webpack的配置中加入less-loader,那麼webpack在打包的時候就會自動將less轉換為CSS,並將CSS的動態代碼生成到JS文件中。當Vue組件被加載到頁面並實例化後,將在DOM內插入這個特定的行內樣式<style>以實現動態樣式的應用。

對於*.css文件同樣也是適用的,例如導入某個第三方庫中必需的樣式表:

    import 'uikit/dist/css/components/tabs.css'  
字體的引用

假設在dark.less內加入對自定義字體文件的樣式定義:

    @font-face {
       font-family: 'Darkenstone';
       src: url('./Darkenstone.eot');
       src: url('./Darkenstone.eot?#iefix') format('embedded-opentype'),
           url('./Darkenstone.woff2') format('woff2'),
           url('./Darkenstone.woff') format('woff'),
           url('./Darkenstone.ttf') format('truetype'),
           url('./Darkenstone.svg#Darkenstone') format('svg');
           font-weight: normal;
           font-style: normal;
    }

    .header
    {
       display: flex;
       flex-flow: row nowrap;

       & > h1 {
          font: 16pt 'Darkenstone';
       }}  

這裡.header>h1指定了一個Darkenstone的自定義字體,這個字體瀏覽器一定是不能識別的,以前我們在樣式表中先定義這個字體樣式並指定加載位置(如上文@font-face的定義),然後在頁面中引用這個樣式表,這是多麼麻煩的一件事,不是嗎?

如果用了webpack後,我們只是在配置文件內加入了一個url-loader:

    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      

loader: 'url'
    }  

我們並不需要在源代碼中做任何改變,因為之前已經引用過樣式表dark.less,而字體是在樣式表中的,webpack將在打包的時候為我們識別並在代碼中引入字體的動態加載。這樣一來極大地解決了我們對資源引用的依賴問題!

vue-cli的webpack模板已經為我們配置好了絕大多數常用的loader,在實際運用中我們只需要瞭解它們是怎麼來的,應該怎麼用,需要的時候如何修改就夠了。

2.3.3 用別名取代路徑引用

在項目開發過程中有可能有許多包是沒有放在npm上的,有一些較老的可能還依然只存在於bower上,某些甚至在bower與npm上都找不到,而不得不通過下載的方式在項目內引用,這樣一來我們的代碼可能通過require就得在代碼內引用一段很長的文件路徑,如下所示。

    import Selector from '../../bower_components/bootstrap-select/dist/js/select'  

這種包的引用方式明顯違反了CommonJS的編程規範,對於這些長路徑,甚至還具有「../..」這些相對路徑搜索的定義,我們可以通過webpack的resolve配置項來解決。就以select這個組件為例,在webpack.base.config.js中加入以下的這個別名的定義:

    module.exports = {
       entry:{ ... },
       output: { ... },
       module:{ ... },
       resolve: {
          extensions:['','.js'],
          alias:{
             'bs-select':
'bower_components/bootstrap-select/dist/js/select.js'
          }
       }
    }  

有了這個定義以後,我們就可以將上面那個長引用改為下面的寫法:

    import Selector from 'bs-select';  

絕對不要讓路徑引用進入到我們的代碼,因為這是代碼的「癌症」,一旦開始植入並生長起來,以前的代碼將難以維護!

2.3.4 配置多入口程序

多數情況下我們的程序入口不單單只有一個,舉一個最簡單的例子,前台提供給最終用戶使用(http://www.domain.com/index),後台提供給登錄用戶使用(http://www.domain.com/admin/),那麼自然需要多個與main.js類似的程序入口了。

首先在build/webpack.base.conf.js配置文件中的entry配置屬性上加上新的入口文件:

    module.exports = {
      entry: {
        app: './src/main.js',
        admin : './src/admin-main.js'
      },
      // ... 省略
    }  

這是用於告訴webpack哪幾個是入口文件,這些文件需要被生成到啟動頁的<script>內。

vue-cli的webpack模板使用HtmlWebpackPlugin插件,生成HTML入口頁面並自動將生成後的JS文件和CSS文件的引用地址寫入到頁內的<script>中。

這裡就需要在build/webpack.dev.config.js文件內的plugins配置項內多配置一個HtmlWebpackPlugin插件,用於生成admin.html入口頁。

    plugins:[
      // ... 省略

      // 這是原有的配置項,用於匹配注入app.js的輸出腳本
      new HtmlWebpackPlugin({
        filename: process.env.NODE_ENV === 'testing'
          ? 'index.html'
          : config.build.index,
        template: 'index.html',
        chunks: ['app'], // 與原配置的不同的是要用chunks指定對應的entry
        inject: true,
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeAttributeQuotes: true
        },
        chunksSortMode: 'dependency'
      }),
      

// 這是新增項,用於匹配注入admin.js的輸出腳本
      new HtmlWebpackPlugin({
        filename: process.env.NODE_ENV === 'testing'
          ? 'admin.html'
          : config.build.admin,
        template: 'index.html',
        chunks: ['admin'],
        inject: true,
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeAttributeQuotes: true
        },
        chunksSortMode: 'dependency'
      }),
    ]  

需要強調一點的是,這裡的HtmlWebpackPlugin配置必須用chunks指定在上文entry內對應的入口文件的別名。

關於HtmlWebpackPlugin更多配置內容可以參考:https://github.com/kangax/html-minifier#options-quick-reference。

還有就是得將同樣的配置加入到生產環境專用的webpack配置文件webpack.prod.conf.js中,否則當我們運行npm run build時是不會輸出admin.js和admin.html這兩個入口文件的(由於配置內容相同這裡就不再重複了)。

最後,如果使用了vue-router就得對connect-history-api-fallback插件的配置進行修改,否則原有的默認配置只會將所有的請求轉發給index.html,這樣就會導致History API沒有辦法正確地將請求指向admin.html,導致熱加載失敗,具體做法如下所述。

打開dev-server.js文件,將app.use(require('connect-history-api-fallback'))配置改為以下的方式:

    // handle fallback for HTML5 history API
    var history = require('connect-history-api-fallback')
    // app.use(require('connect-history-api-fallback'))

    app.use(history({
      rewrites: [
        { from: /^\/admin\/.*$/, to: '/admin.html' }
      ]
    

}));  

新入口需要有明確區分的路由規則,否則還是會產生熱加載失敗的情況,這樣就非常不便於開發了。

2.4 基於Karma+Phantom+Mocha+Sinon+Chai的單元測試環境

一般來說,Vue項目的單元測試用得最多的就是組件功能測試了。具體如何來寫這些組件測試將在第5章講解。在此,我們先從開發流程的角度來看待測試的問題以及深入瞭解vue-cli webpack模板為我們建立的單元測試環境的功能與運作機理。

首先,在有具體的組件測試目標時的Vue-TDD的開發流程如下圖所示。

● 編寫組件測試——在單元測試中將設計組件的名稱、屬性接口、事件接口,用斷言工具確定衡量這個組件正確的標準。

● 編寫組件代碼——以單元測試作為引導程序,編寫組件真實的實現代碼,讓測試通過。

● 運行測試,並看到測試通過。

● 重構。

然後如此循環直至所有的組件單元測試都通過為止。與運行npm run dev指令將代碼加載到瀏覽器中運行不同的是,這個過程並不需要我們打開瀏覽器用眼睛判斷組件的輸出是否符合要求,用調試模式來觀察變量是否正確,因為這一切都應該是自動執行的!vue-cli的webpack模板就為我們配置了這樣一個全自動化的測試環境,作為高質量Vue組件開發的最大助力。

前端開發的單元測試環境會比後端開發的單元測試環境複雜,這是由於前端開發的工具碎片化比較嚴重所致的,所以要配置一個良好的單元測試環境需要有長期的實戰經驗以及熟悉各種各樣的工具,vue-cli的webpack模板給我們配置的單元測試環境其實也相當複雜,下圖描繪了這些工具是如何進行協作的。

接下來我們就一個一個地瞭解這些工具的作用,以便我們在做單元測試的時候知道可以對哪些環節進行調整和優化。

Karma

Karma是一個著名的測試加載器(https://karma-runner.github.io/),它能完成許多測試環境加載任務。

Karma作為自動化測試程序的入口,它可以執行以下這些任務:

● 為測試程序注入指定依賴包;

● 可同時在一個或多個瀏覽器宿主中執行測試,滿足兼容性測試需要;

● 執行代碼覆蓋性測試;

● 輸出測試報告;

● 執行自動化測試。

Karma就是這樣一個開發環境,開發者指定需要測試的腳本/測試文件,需要運行的瀏覽器等信息,Karma會在後台自動監控文件的修改,並啟動一個瀏覽器與Karma的服務器連接,這樣當源代碼或者測試發生修改後,Karma會自動運行測試。

開發者可以指定不同的瀏覽器,甚至可以跨設備。由於Karma只是一個運行器,所以要配置一些測試框架如Mocha、Jasmine等作為單元測試的代碼支撐,甚至還可以自定義適配器來支持自己的測試框架。

Karma擁有獨立的CLI,可以通過以下方式安裝到全局環境中:

    $ npm karma i -g  

然後就可以在命令行直接使用Karma指令了。

我們需要知道的Karma常用命令有兩個,第一個就是初始化Karma環境:

    $ karma init  

karma init指令運行後就會出現一個嚮導型的終端交互界面來生成一個karma.conf.js的全局配置文件,具體效果如下:

如果正在使用vue-cli webpack模板就不需要手工來做這一步,因為模板在創建工程時就生成了這個配置文件,在~/test/unit/karma.conf.js中就可以找到它。

第二個命令就是karma start,這個命令就是啟動Karma,讓它按照karma.conf.js的配置項執行自動化測試。vue-cli webpack模板將這個指令在package.json內進行了包裝定義,所以在工程目錄下只要運行:

    $ npm run unit  

就可以直接啟動Karma。

另外,Karma還能很好地與WebStorm集成在一起,只要在WebStorm的「Run/Edit Configurations」菜單中打開運行器對話框,然後增加一個Karam的運行項,在「Configuratio File」選擇框內找到當前工程的karma.conf.js,就可以在WebStrom內直接運行Karma了, 配置如下圖所示。

在WebStorm內直接運行Karma,WebStorm會將單元測試項集成在IDE內,而不單單只是在終端輸出Karma的測試結果報告:

Karma 的插件系統

在整個單元測試環境中,Karma承擔的是一個調度員的職責,通過調配不同的工具讓其有條不紊地互相協作。之所以能與如此多的外部工具協同工作,是由於它自身強大的插件系統和豐富的插件庫。在我們的單元測試環境中,Karam通過karma-webpack啟動webpack編譯測試文件與源代碼文件。然後通過karma-phantomjs-lanucher啟動PhantomJS瀏覽器,將編譯後的代碼嵌入到網頁內。接著通過karma-mocha啟動Mocha,將karma-sinon-chai加載到Mocha之中並運行當前頁面加載的單元測試代碼,最後將測試的結果輸出到終端。

Karma在我們引入TDD方法開發Vue組件後,它將是一個運行頻次很高的工具。如果感覺運行速度慢的話,可以將test/unit/karma.conf.js內的代碼覆蓋性報告插件(coverage)暫時刪除掉,因為生成這份報告並不是每次運行測試都必需的,更何況它是一個慢速插件。具體做法如下:

    config.set({
       // ... 省略
       reporters: ['spec']
    })  

瞭解完Karma運行原理和作用,接下來我們瞭解一下每個工具使用的方法和具體完成的任務。

PhantomJS

PhantomJS(http://phantomjs.org/)是一個無界面的、可腳本編程的WebKit瀏覽器引擎。它原生支持多種Web標準:DOM操作、CSS選擇器、JSON、Canvas以及SVG。

一般來說,在我們的Vue單元測試代碼中很少會直接以編碼方式調用PhantomJS的功能,更多是利用PhantomJS具有高速的運行速度這一特點,優化每一次單元測試的效能,節省等待瀏覽器啟動的漫長等待時間。

Karma會自動加載karma-phantomjs-lanucher來引導PhantomJS啟動,我們甚至不需要改動karma.conf.js內的任何配置。使用PhantomJS會比在Karma中使用Chrome作為宿主要快上好幾倍的啟動時間。

MochaJS

Mocha(http://mochajs.org/)是一個JavaScript測試框架,可以用來運行測試代碼,它沒有內置的Assertion、Mock和Stub功能。一般我們用Chai來為它提供斷言,用Sinon為它提供Mock和Stub功能。Mocha可以用來測試Node.js和瀏覽器的JavaScript代碼。

Mocha與Jasmine的語法非常相似,Mocha配合Sinon可以更好地支持後端服務模擬的能力和異步測試調用,這一點比Jasmine做得更優秀一些。我會在第5章再詳細講述它的用法。

執行單元測試的命令如下:

    $ npm run unit  

在單元測試環境內Mocha起到了兩個作用,首先它提供了單元測試框架與編寫單元測試的規則,如果你是一個Ruby開發者而且使用過RSpec的話,你對此一定不會陌生:

    describe('UkButton', => {
        it('應該輸出uikit按鈕的HTML結構', => {
            // ... 具體測試代碼
        })
    })  

其次就是加載運行這些單元測試代碼的解釋運行器。

Chai

Chai是一個提供BDD風格的代碼斷言庫,由於Mocha的代碼斷言非常簡單,Chai用於彌補Mocha的這一缺陷。例如:

    expect(vm.$el.querySelectorAll('ul')).to.have.lengthOf(2)  

expect和should是BDD風格的,二者使用相同的鏈式語言來組織斷言,但不同之處在於它們初始化斷言的方式:expect使用構造函數來創建斷言對像實例,而should通過為Object.prototype新增方法來實現斷言(所以should不支持IE);expect直接指向chai.expect,而should則是chai.should。

我們在vue-cli webpack模塊建立的Vue工程內編寫單元測試是不需要手工配置Chai的,因為Chai和Sinon被Karma通過karma-sinon-chai插件直接嵌入到單元測試的上下文中,所以不需要import就能直接使用。

更多關於Chai的斷言的資料請參考本書的「附錄A」。

Sinon

當測試的某個方法中,需要去某個接口發送HTTP請求以獲得數據,如果你真實地發送某個請求,那麼當有一天你請求的這個服務器掛掉的時候,你的單元測試就怎麼也跑不過了。其實在測試的時候,我們並不是真的關心這個接口是否存在(甚至是否實現),我們需要模擬一個這樣的接口來返回假的數據,sinonjs就是解決這類問題的一個輔助庫,換個 專業的說法,Sinon就是負責仿真的。

它主要提供方法調用偵測(Spy)、接口仿真(Stub)和對像仿真(Mock)這三個方面的輔助功能。另外,vue-cli的webpack模板所生成的單元測試環境採用sinon-chai這個聯合庫,它基於Sinon和Chai兩個庫直接提供了一套更方便使用代碼的斷言庫,而這些配置早就被腳手架vue-cli配置好在項目裡面等我們了。關於Mocha、Sinon和Chai的具體應用將在「Vue的測試與調試技術」一章中通過具體示例一一講述。

2.5 基於Nightwatch的端到端測試環境

不同公司和組織之間的測試效率迥異。在這個富交互和響應式處理隨處可見的時代,很多組織都使用敏捷的方式來開發應用,因此測試自動化也成為軟件項目的必備部分。測試自動化意味著使用軟件工具來反覆運行項目中的測試,並為回歸測試提供反饋。

端到端測試又簡稱E2E(End-To-End test)測試,它不同於單元測試側重於檢驗函數的輸出結果,端到端測試將盡可能從用戶的視角,對真實系統的訪問行為進行仿真。對於Web應用來說,這意味著需要打開瀏覽器、加載頁面、運行JavaScript,以及進行與DOM交互等操作。簡言之,單元測試的功能只能確保單個組件的質量,無法測試具體的業務流程是否運作正常,而E2E卻正好與之相反,它是一個更高層次的面對組件與組件之間、用戶與真實環境之間的一種集成性測試 。

E2E測試的意義在於可以通過程序固化和仿真用戶操作,對於開發人員而言,基於E2E測試能極大地提高Web的開發效能,節約開發時間。

先來看看如果沒有E2E測試下的一次從開發到手工測試成功的過程:

這個過程還屬於簡化過的,還沒有包括在觀察結果時要打開瀏覽器的調試窗口觀看某些內部的運行變量或者網頁代碼結構。整個過程都是純人工操作,人工操作最大的問題是一個程序可能要調試好幾次,同樣的操作就要重複數遍。即使有嚴格的規定,程序員們大多都還是隨便地做「通過」式操作,尤其在輸入樣本數據時,絕大多數的程序員幾乎都是亂輸,出現得最多的就是各種隨意的數字或者是「aaa」、「asd」、「aws」這樣毫無意義的字符。以這種方式開發出來的程序在驗收時產品經理或者客戶會經常說一句話:「我上次試過是沒有問題的!」這樣的失誤歸根結底不在程序員本身,因為這是一種人性!一個人如果重複多次自己都覺得毫無意義的動作時,要不就逃避不做,如果不能逃避就會消極對待。

所以我們應該用更高效、更能彌補人性化缺陷和更有意義的辦法來處理,這就是E2E測試,先來看看如果使用E2E測試後的開發過程將會變成什麼:

從運行測試開始,所有的一切都是自動的!這就是最大的區別,還有更重要的一點是,當我們要寫出E2E測試時就需要對操作需求有深刻的理解,在這一過程中還有很大的機會對用戶的操作進行優化,從而提高用戶體驗。

Nightwatch

vue-cli的webpack模板也為我們準備了一個當下很流行的E2E測試框架——Nightwatch。

Nightwatch是一套新近問世的基於Node.js的驗收測試框架,使用Selenium WebDriver API以將Web應用測試自動化。它提供了簡單的語法,支持使用JavaScript和CSS選擇器來編寫運行在Selenium服務器上的端到端測試。

這個框架在配置好後的具體工作流程如下圖所示。

Nightwatch採用Fluent interface模式(https://en.wikipedia.org/wiki/Fluent_interface)來簡化端到端測試的編寫,語法非常簡潔易懂,正如以下代碼所示。

    this.demoTestGoogle = function (browser) {
      browser
        .url('http://www.google.com')
        

.waitForElementVisible('body', 1000)
        .setValue('input[type=text]', 'nightwatch')
        .waitForElementVisible('button[name=btnG]', 1000)
        .click('button[name=btnG]')
        .pause(1000)
        .assert.containsText('#main', 'The Night Watch')
        .end;
    }  

我們可以從Nightwatch網站找到當前提供特性的列表:

● 簡單但強大的語法。只需要使用JavaScript和CSS選擇器,開發者就能夠非常迅捷地撰寫測試。開發者也不必初始化其他對像和類,只需要編寫測試規範即可。

● 內建命令行測試運行器,允許開發者同時運行全部測試——分組或單個運行。

● 自動管理Selenium服務器;如果Selenium運行在另一台機器上,那麼也可以禁用此特性。

● 支持持續集成:內建JUnit XML報表,因此開發者可以在構建過程中,將自己的測試與系統(例如Hudson或Teamcity等)集成。

● 使用CSS選擇器或Xpath,定位並驗證頁面中的元素或是執行命令。

● 易於擴展,便於開發者根據需要,實現與自己應用相關的命令。

配置 Nightwatch

要瞭解Nightwatch的配置和用法,與前文介紹Mocha一樣,應該先從工程結構入手。

工程結構
    .
    └── test
          └── e2e
                ├── custom-assertions     // 自定義斷言
                │    └── elementCount.js
                ├── page-objects          // 頁面對像文件夾
                ├── reports               // 輸出報表文件夾
                ├── screenshots           // 自動截屏
                ├── nightwatch.conf.js    // nightwatch 運行配置
                ├── runner.js             // 運行器
                └── specs                 // 測試文件
                      └── test.spec.js  

以上是vue-cli為我們自動創建的Nightwatch工程結構,specs是測試文件存放的文件夾,nightwatch.conf.js是Nightwatch的運行配置文件。其他的目錄將會在具體的章節逐一地進行講述。

基本配置

Nightwatch的配置項都集中在nightwatch.conf.js中,其實這個配置也可以是一個JSON格式,採用JSON格式只需要簡單地對配置項寫入一些常量即可。但使用模塊的方式進行配置可以執行一些額外的配置代碼,這樣則顯得更為靈活。以下是我調整過的nightwatch.conf.js文件內容:

    require('babel-register');
    var config = require('../../config');
    var seleniumServer = require('selenium-server');
    var phantomjs = require('phantomjs-prebuilt');

    module.exports = {
      "src_folders": ["test/e2e/specs"],
      "output_folder": "test/e2e/reports",
      "custom_assertions_path": ["test/e2e/custom-assertions"],
      "page_objects_path": "test/e2e/page-objects",
      "selenium": {
        "start_process": true,
        "server_path": seleniumServer.path,
        "port": 4444,
        "cli_args": {
          "webdriver.chrome.driver": require('chromedriver').path
        }
      },
      "test_settings": {
        "default": {
          "selenium_port": 4444,
          "selenium_host": "localhost",
          "silent": true,
          launch_url:"http://localhost:" + (process.env.PORT || config.dev.port),
          "globals": {
          }
        },
        "chrome": {
          "desiredCapabilities": {
            "browserName": "chrome",
            "javascriptEnabled": true,
            "acceptSslCerts": true
          }
        },
        "firefox": {
          "desiredCapabilities": {
            "browserName": "firefox",
            "javascriptEnabled": true,
            

"acceptSslCerts": true
          }
        }
      }
    }  

Nightwatch的配置分為以下三類:

● 基本配置;

● Selenium配置;

● 測試環境配置。

在配置模塊中的所有根元素配置項都屬於基本配置,用於控制Nightwatch的全局性運行的需要。下表為Nightwatch的基本配置項的詳細說明。

Selenium 配置

Selenium是一組軟件工具集,每一個工具都有不同的方法來支持測試自動化。大多數使用Selenium的QA工程師只關注一兩個最能滿足他們項目需求的工具。然而,學習所有的工具你將有更多選擇來解決不同類型的測試自動化問題。這一整套工具具備豐富的測試功能,很好地契合了測試各種類型的網站應用的需要。這些操作非常靈活,有多種選擇來定位UI元素,同時將預期的測試結果和實際的行為進行比較。Selenium一個最關鍵的特性是支持在多瀏覽器平台上進行測試。

Selenium誕生於2004年,當在ThoughtWorks工作的Jason Huggins在測試一個內部應 用時,作為一個聰明的傢伙,他意識到相對於每次改動都需要手工進行測試,他的時間應該用得更有價值。他開發了一個可以驅動頁面進行交互的JavaScript庫,能讓多瀏覽器自動返回測試結果。那個庫最終變成了Selenium的核心,它是Selenium RC(遠程控制)和Selenium IDE所有功能的基礎。Selenium RC是開拓性的,因為沒有其他產品能讓你使用自己喜歡的語言來控制瀏覽器。

Selenium是一個龐大的工具,所以它也有自己的缺點。由於它使用了基於JavaScript的自動化引擎,而瀏覽器對JavaScript又有很多安全限制,有些事情就難以實現。更糟糕的是,網站應用正變得越來越強大,它們使用了新瀏覽器提供的各種特性,都使得這些限制讓人痛苦不堪。在2006年,一名Google的工程師Simon Stewart開始基於這個項目進行開發,這個項目被命名為WebDriver。此時,Google早已是Selenium的重度用戶,但是測試工程師們不得不繞過它的限制。Simon需要一款能通過瀏覽器和操作系統的本地方法直接和瀏覽器進行通話的測試工具,來解決JavaScript環境沙箱的問題。WebDriver項目的目標就是要解決Selenium的痛點。

Selenium 1 (又叫Selenium RC或Remote Control)在很長一段時間內,Selenium RC都是最主要的Selenium項目,直到WebDriver和Selenium合併而產生了最新且最強大的Selenium 2。Seleinum 1仍然被活躍地支持著(更多是維護),並且提供一些Selenium 2短時間內可能不會支持的特性,包括對多種語言的支持(Java、JavaScript、Ruby、PHP、Python、Perl和C#)和對大多數瀏覽器的支持。

Selenium 2 (又叫Selenium WebDriver)代表了這個項目未來的方向,也是最新被添加到Selenium工具集中的。這個全新的自動化工具提供了很多了不起的特性,包括更內聚和面向對象的API,並且解決了舊版本限制。Selenium和WebDriver的作者都贊同兩者各具優勢,而兩者的合併使得這個自動化工具更加強健。Selenium 2.0正是於此的產品。它支持WebDriver API及其底層技術,同時也在WebDriver API底下通過Selenium 1技術為移植測試代碼提供極大的靈活性。此外,為了向後兼容,Selenium 2仍然使用Selenium 1的Selenium RC接口。

你可以到http://selenium-release.storage.googleapis.com/index.html下載Selenium的各個穩定版本。

在Vue項目中如果使用vue-cli,那麼Nightwatch將不需要進行任何的附加配置,否則你需要在命令行內安裝Selenium的包裝類庫:

    $ npm i selenium-server -D  

Nightwatch能引導Selenium的啟動,實際上我們並沒有必要去修改Selenium服務器的默認運行配置,在nightwatch.conf.js配置文件中只需要聲明Selenium服務器的二進制執行 文件的具體路徑即可,這個可以從selenium-server包提供的Selenium包裝對象的path屬性中獲取,而無須將本機的物理路徑寫死到配置文件內。

    var seleniumServer = require('selenium-server');

    module.exports= {
      "selenium": {
        "start_process": true,
        "server_path": seleniumServer.path,
        "port": 4444,
        "cli_args": {
          "webdriver.chrome.driver": require('chromedriver').path
        }
      },
      // ... 省略
    }  

以下是Selenium的詳細配置項說明:

配置項 類 型 默認值 說 明 start_process boolean false 配置是否自動管理Selenium進程 start_session boolean true 配置是否自動啟用Selenium會話。當不需要與Selenium進行交互時可設置為false server_path string none 指定Selenium jar運行文件目錄 log_path string none Selenium日誌輸出目錄(默認為當前目錄) port integer 4444 Selenium服務器啟動時佔用的端口 cli_args object none Selenium命令行參數列表(詳見下文)
cli_args 的配置

● webdriver.firefox.profile:Selenium默認為每個會話創建一個獨立的Firefox配置方案。如果你希望使用新的驅動配置可以在此進行聲明。

● webdriver.chrome.driver:Nightwatch同樣可以使用Chrome瀏覽器加載測試,當然你要先下載一個ChromeDriver的二進制運行庫對此進行支持。此配置項用於指明ChromeDriver的安裝位置。除此之外,還需要在test_settings配置內使用desiredCapabilities對像為Chrome建立配置方案。

● webdriver.ie.driver:Nightwatch也支持IE,其作用與用法與Chrome相同,此處則不過多贅述。

測試環境配置

test_settings內的項目將應用於所有的測試實例,在E2E測試中我們可以通過Nightwatch提供的默認實例對像browser獲取這些配置值,vue-cli為我們創建了default、firefox和chrome三個環境配置項,default配置是應用於所有環境的基礎配置選項,其他的配置項會自動覆蓋與default相同的配置值。

firefox和chrome這兩個配置項是對兩種瀏覽器的驅動進行描述和配置。對於其他語言或框架而言它們也是常客,但由於性能太低,在實戰中通常只是個擺設,下文中我將會介紹一種實戰效率更高的無頭瀏覽器PhantomJS,對其取而代之。

不要被vue-cli創建默認配置所迷惑,test_settings並不單單只是對瀏覽器的一些基本運行參數的配置,它正確的用法是對E2E測試環境的配置。單元測試只能運行於開發環境內,而E2E卻可以運行於本地環境與網絡環境,更準確地說是開發環境與生產環境。所以這個配置項可以用以下的方式進行設置:

    "test_settings": {
        "default": {
          "selenium_port": 4444,
          "selenium_host": "localhost",
          "silent": true,
          launch_url:"http://localhost:" + (process.env.PORT || config.dev.port),
          "globals": {}
        },
        "dev": {
          "desiredCapabilities": {
            "browserName": "chrome",
            "javascriptEnabled": true,
            "acceptSslCerts": true
          }
        },
        "production": {
          "launch_url":"http://www.your-domain.com"
          "desiredCapabilities": {
            "browserName": "firefox",
            "javascriptEnabled": true,
            "acceptSslCerts": true
          }
        }
      }  

雖然與原有的配置只是在用詞上做了一點點改變,但用詞的改變將會徹底地改變我們對其的認知與思路!

下表是測試環境配置項的詳細說明:

執行 E2E 測試

vue-cli已經在package.json中配置了運行測試的指令:

    $ npm run e2e  

這個指令是默認啟用Chrome運行環境的,如果指定運行環境可使用--env選項:

    $ npm run e2e --env  
使用無頭瀏覽器 PhantomJS

vue-cli webpack腳手架模板非常好用,它將環境的複雜性降低了很多,但是卻沒有很好地詮釋它裡面採用的每個模塊的理由和功能,以及它們的使用特點。這對於入門者來說確實是將門檻降到最低點,但從工程化開發的角度來說,只知道有這些環境或者工具的存在是遠遠不夠的,在Nightwatch中就埋了一個這樣的坑。

我們的開發環境在配置Mocha和Karma時就已經安裝了PhantomJS,但如果你細讀Nightwatch的默認配置會驚奇地發現根本沒有採用PhantomJS,只是配置了Chrome和Firefox!問題何在?一個字:慢!

我曾用一台2013年版標準配置(i5CPU、8GB內存、1TB HDD硬盤)的iMac跑本書下一章中的示例程序,運行一次的實際時間是15秒左右!僅僅一次就得15秒,那可以想像我們開發一個場景最少要做多少次的運行?Chrome的啟動是很慢的,我們做E2E這種自動化測試如果用真實瀏覽器的話只能將性能拖下來,生命不能耗費在毫無意義的等待中!所以我們才會選擇PhantomJS!沒有默認配置PhantomJS作為主瀏覽器是這個環境的最大敗筆。

辦法總比問題多,所以如果沒有,我們還可以自己動手來配置,其實方法也很簡單。打開nightwatch.conf.js,在test_settings配置段的下方加入以下的內容:

    "test_settings": {
      "default": {
        // ...
      }
    },

    "phantom":{
      "desiredCapabilities": {
        "browserName": "phantomjs",
        "javascriptEnabled": true,
        

"acceptSslCerts": true,
        "phantomjs.page.settings.userAgent" : "Mozilla/5.0 (Macintosh; Intel Mac
OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80
Safari/537.36",

"phantomjs.binary.path":"node_modules/phantomjs-prebuilt/bin/phantomjs"
      }
    }  

Nightwatch是通過Selenium加載一個GhostDriver來引導PhantomJS瀏覽器的,上面的內容就相當於告訴Selenium加載一個GhostDriver,可執行程序則指向npm上安裝的phantomjs-prebuilt包,再通過這個包來引導安裝在本機上的PhantomJS啟動。

按上文這樣來引用PhantomJS的二進製程序的地址非常難看,還有原生配置中的Selenium執行程序地址也是一樣的,這裡介紹一個更DRY的方法來處理這些路徑:

    var seleniumServer = require('selenium-server');
    var phantomjs = require('phantomjs-prebuilt');


    module.exports = {
      // ...省略

      "selenium": {
        // ... 省略

        "server_path": seleniumServer.path,
      },

      "test_settings": {
        // ... 省略

        "phantom": {
          "desiredCapabilities": {
            // ... 省略

            "phantomjs.binary.path": phantomjs.path
          }
        }

        // ... 省略
      

}
    }  

做完這個簡單的優化後就可以打開runner.js文件找到:

    if (opts.indexOf('--env') === -1) {
      opts = opts.concat(['--env', 'chrome'])
    }  

將chrome改為phantom就行了:

    if (opts.indexOf('--env') === -1) {
      opts = opts.concat(['--env', 'phantom'])
    }  

重新加載測試程序,在同一台iMac上的運行速度直接降到了5秒,測試運行速度提升了3倍!如果你有配置更好的機器,將硬盤換成SSD之後會有更驚人的速度。

Nightwatch 與 Cucumber

如果你正在開發的項目的業務複雜性不大,可以直接使用Nightwatch推薦的鏈式調用寫法。但是當這種做法真正應用在業務流程較多,或者業務操作相對複雜的應用場景時,你會覺得總有寫不完的E2E測試,因為這麼做E2E測試是沒有辦法一次性覆蓋所有需求的!

E2E測試其實是行為式驅動開發的實現手法,如果跳過了行為式驅動開發的分析部分直接編寫E2E,其結果只能是寫出一堆嚴重碎片化的測試場景,甚至會出現很多根本不應該出現的操作。

幸好Nightwatch具有很好的擴展性與兼容性,能集成最正統的BDD測試框架Cucumber(https://cucumber.io/)。Cucumber是原生於Ruby世界的BDD框架,但它也有很多的語言實現版本,我們可以安裝一套專門為Nightwatch編寫的Cucumber版本——nightwatch-cucumber(https://github.com/mucsi96/nightwatch-cucumber)。本章只介紹關於環境與工具的配置,而關於如何來應用BDD,內容已經超出了本書的知識範圍,如果有興趣的話可以參考《攀登架構之巔》一書中行為式驅動開發的章節內容。

    $ npm i nightwatch-cucumber -D  

然後在~/test/e2e/nightwatch.conf.js文件中加入對Cucumber的配置:

    // ... 省略
    require('babel-register');

    require('nightwatch-cucumber')({
      

nightwatchClientAsParameter: true,
      featureFiles: ['test/e2e/features'],
      stepDefinitions: ['test/e2e/features/step_definitions'],
      jsonReport: 'test/e2e/reports/cucumber.json',
      htmlReport: 'test/e2e/reports/cucumber.html',
      openReport: false
    });