我们在上一章中只是用了一个非常简单的例子,尝试对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的详细配置项说明:
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 MacOS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80Safari/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 });