Vue2-秘籍-三-

55 阅读35分钟

Vue2 秘籍(三)

原文:zh.annas-archive.org/md5/dd7447834c754d87cebc9999e0cff7f3

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:单页应用程序

在本章中,将涵盖以下内容:

  • 使用 vue-router 创建 SPA

  • 在切换路由之前获取数据

  • 使用命名动态路由

  • 在页面中使用多个 router-view

  • 按层次组织路由

  • 使用路由别名

  • 在路由之间添加过渡效果

  • 管理路由错误

  • 在加载页面时添加进度条

  • 如何重定向到另一个路由

  • 在返回时保存滚动位置

介绍

许多现代应用程序都基于 SPA(单页应用程序)模型。从用户的角度来看,这意味着整个网站在单个页面上看起来类似于一个应用程序。

这是好的,因为如果做得正确,它可以增强用户体验,主要是减少等待时间,因为没有新页面需要加载-整个网站都在一个页面上。这就是 Facebook、Medium、Google 和许多其他网站的工作方式。

URL 不再指向 HTML 页面,而是指向应用程序的特定状态(通常看起来像不同的页面)。在实践中,在服务器上,假设您的应用程序位于index.html页面中,这是通过将请求“关于我”的用户重定向到index.html来实现的。

后者页面将采用 URL 的后缀,并将其解释为路由,而路由将创建一个类似页面的组件,其中包含个人简介信息。

使用 vue-router 创建 SPA

Vue.js 通过其核心插件 vue-router 实现了 SPA 模式。对于 vue-router,每个路由 URL 对应一个组件。这意味着我们将告诉 vue-router 当用户访问特定 URL 时如何行为,以其组件为基础。换句话说,在这个新系统中,每个组件都是旧系统中的一个页面。

准备工作

对于这个示例,您只需要安装 vue-router 并对 Vue 组件有一些了解。

要安装 vue-router,请按照以下说明操作:router.vuejs.org/en/installation.html

如果您正在使用 JSFiddle 进行跟随,可以添加类似于unpkg.com/vue-router/dist/vue-router.js的链接。

操作步骤

我们正在为一家餐厅准备一个现代化的网站,并将使用 SPA 模式。

该网站将包括三个页面:主页、餐厅菜单和酒吧菜单。

整个 HTML 代码将如下所示:

<div id="app">

  <h1>Choppy's Restaurant</h1>

  <ul>

    <li>Home</li>

    <li>Menu</li>

    <li>Bar</li>

  </ul>

  <router-view></router-view>

</div>

<router-view>组件是 vue-router 的入口点。它是组件显示为页面的地方。

列表元素将变成链接。目前,它们只是列表元素;要将它们转换为链接,我们可以使用两种不同的语法。将第一个链接包装在以下行中:

<li><router-link to="/">

Home</router-link>

</li>

另一个示例如下:

<li><router-link to="/menu">

Menu</router-link>

</li>

我们可以使用的另一种语法是以下内容(用于 Bar 链接):

<li>

  <router-link

    tag="li" to="/bar"

      :event="['mousedown', 'touchstart']"

    >

    <a>Bar</a>

  </router-link>

</li>

这种更冗长但更明确的语法可以用于将自定义事件绑定到特定的路由。

要告诉 Vue 我们要使用 vue-router 插件,请在 JavaScript 中写入以下内容:

Vue.use(VueRouter)

我们在开始时列出的三个页面的部分将由这三个虚拟组件扮演(将它们添加到 JavaScript 中):

const Home = { template: '<div>Welcome to Choppy's</div>' }

const Menu = { template: '<div>Today we have cookies</div>' }

const Bar = { template: '<div>We serve cocktails</div>' }

现在,您终于可以创建路由器了。其代码如下:

const router = new VueRouter({})

这个路由器没有做太多事情;我们必须添加路由(对应于 URL)及其关联的组件:

const router = new VueRouter({

 routes: [ 

 { path: '/', component: Home }, 

 { path: '/menu', component: Menu }, 

 { path: '/bar', component: Bar } 

 ] 

})

现在我们的应用程序几乎完成了;我们只需要声明一个简单的Vue实例:

new Vue({

  router,

  el: '#app'

})

我们的应用程序现在可以工作了;在启动之前,添加此 CSS 规则以获得稍微更好的反馈:

a.router-link-active, li.router-link-active>a {

  background-color: gainsboro;

}

当您打开应用程序并点击 Bar 链接时,您应该看到类似以下屏幕截图的内容:

它是如何工作的...

您的程序首先要做的是将 vue-router 注册为插件。vue-router 反过来注册路由(它们是 URL 的一部分)并将组件连接到每个路由。

当我们首次访问应用程序时,浏览器上的 URL(您无法在 JSFiddle 中看到它在更改,因为它在 iframe 中)将以index.html/#/结尾。井号后面的所有内容都是 vue-router 的路由。在这种情况下,它只是一个斜杠(/),因此它与第一个主页路由匹配。

当我们点击链接时,<router-view>的内容会根据我们与该路由关联的组件而更改。

还有更多...

敏锐的读者肯定会发现可以解释为错误的问题-在运行应用程序之前,我们添加了一些 CSS 样式。.router-link-active类会在<router-link>组件中自动注入,每当页面对应于实际指向的链接时。

当我们点击“菜单”和“栏目”时,背景颜色会发生变化,但似乎仍然会停留在“主页”链接上。这是因为<router-link>组件执行的匹配不是精确的。换句话说,/bar/menu包含/字符串,因此/总是匹配的。

一个快速修复的方法是添加与第一个<router-link>完全相同的属性:

<li><router-link to="/" exact

>Home</router-link></li>

现在,只有当路由完全匹配主页链接时,“主页”链接才会被突出显示。

还要注意的一点是规则本身:

a.router-link-active, li.router-link-active>a {

  background-color: gainsboro;

}

为什么我们要匹配两个不同的东西?这取决于你如何编写路由链接。

<li><router-link to="/" exact>Home</router-link></li>

前面的代码将被翻译为以下 DOM 部分:

<li><a href="#/" class="router-link-active"

>

Home</a></li>

而:

<router-link tag="li" to="/" exact>Home</router-link>

变成:

<li class="router-link-active"

>Home</li>

请注意,在第一种情况下,类被应用于子锚点元素;在第二种情况下,它被应用于父元素。

在切换路由之前获取数据

在 Vue 的早期版本中,我们有一个专门的方法来从互联网上获取数据,然后再切换路由。在 Vue 2 中,我们有一个更通用的方法来处理这个问题,以及在切换路由之前可能需要处理的其他事情。

准备工作

为了完成这个示例,您需要已经了解 vue-router 的基础知识以及如何进行 AJAX 请求(在最后一章中会详细介绍)。

如何做到这一点…

我们将编写一个由两个页面组成的简单网页作品集:一个主页和一个关于我页面。

为了完成这个示例,我们需要将 Axios 添加为依赖项。

基本布局可以从以下 HTML 代码中清楚地看出:

<div id="app">

  <h1>My Portfolio</h1>

  <ul>

    <li><router-link to="/" exact>Home</router-link></li>

    <li><router-link to="/aboutme">About Me</router-link></li>

  </ul>

  <router-view></router-view>

</div>

在 JavaScript 中,您可以开始构建您的AboutMe组件:

const AboutMe = {

  template: `<div>Name:{{name}}<br>Phone:{{phone}}</div>`

}

它只显示一个姓名和一个电话号码。让我们在组件的data选项中声明这两个变量,如下所示:

data () {

  return {

    name: undefined,

    phone: undefined  

  } 

}

在实际加载组件到场景之前,vue-router 将在我们的对象中查找一个名为beforeRouteEnter的选项;我们将使用它来从服务器加载姓名和电话号码。我们使用的服务器将提供一些虚假数据,仅用于显示,如下所示:

beforeRouteEnter (to, from, next) {

  axios.post('https://schematic-ipsum.herokuapp.com/', {

    "type": "object",

    "properties": {

      "name": {

        "type": "string",

        "ipsum": "name"

      },

      "phone": {

        type": "string",

        "format": "phone"

      }

    }

  }).then(response => {

    next(vm => {

      vm.name = response.data.name

      vm.phone = response.data.phone 

    })

  })

}

对于另一个组件,主页,我们只需编写一个小组件作为占位符:

const Home = { template: '<div>This is my home page</div>' }

接下来,您需要注册router及其paths

Vue.use(VueRouter)

const router = new VueRouter({

  routes: [

    { path: '/', component: Home },

    { path: '/aboutme', component: AboutMe },  

  ] 

})

当然,您还需要注册一个Vue根实例,如下所示:

new Vue({

  router,

  el: '#app'

})

当您启动应用程序并点击“关于我”链接时,您应该看到类似于以下内容的东西:

请注意,当您点击链接时,页面不会重新加载,但显示个人简介仍然需要一些时间。这是因为它正在从互联网获取数据。

工作原理如下:

beforeRouteEnter钩子函数接受三个参数:

  • to:这是一个表示用户请求的路由的Route对象。

  • from:这也是一个表示当前路由的Route对象。在出现错误时,用户将保留在该路由上。

  • next:这是一个函数,当我们准备继续切换路由时可以使用它。如果使用 false 调用此函数,将阻止路由的更改,在出现错误时非常有用。

当调用上述函数时,我们使用 Axios 调用了一个 Web 服务,该服务提供了一个名称字符串和一个电话号码字符串。

当我们在此钩子函数内部时,重要的是要记住我们无法访问this。这是因为此钩子函数在组件实际实例化之前运行,因此没有this可供引用。

当服务器响应时,我们在then函数内部,并且希望将服务器返回的名称和电话赋值给变量,但是正如前面所说,我们无法访问this。next 函数接收到我们的组件的引用作为参数。我们使用它来将变量设置为接收到的值:

...

}).then(response => {

  next(vm => {

    vm.name = response.data.name

    vm.phone = response.data.phone

  })

})

使用命名动态路由

手动注册所有路由可能会耗费时间,并且当路由事先未知时,这是不可能的。vue-router 允许您使用参数注册路由,以便您可以为数据库中的所有对象创建链接,并覆盖其他用户选择路由的用例,遵循某种模式,这将导致需要手动注册太多的路由。

准备工作

除了 vue-router 的基础知识(参考“使用 vue-router 创建单页应用程序”配方),您不需要任何其他信息来完成此配方。

操作步骤如下:

我们将开设一个在线餐厅,提供十种不同的菜肴。我们将为每道菜创建一个路由。

我们网站的 HTML 布局如下:

<div id="app">

  <h1>Online Restaurant</h1>

  <ul>

    <li>

      <router-link :to="{ name: 'home' }" exact>

        Home

      </router-link>

    </li>

    <li v-for="i in 10">

      <router-link :to="{ name: 'menu', params: { id: i } }">

        Menu {{i}}

      </router-link>

    </li>

    </ul>

  <router-view class="view"></router-view>

</div>

这将创建 11 个链接,一个用于主页,十个用于菜肴。

在 JavaScript 部分注册VueRouter之后,代码如下:

Vue.use(VueRouter)

创建两个组件;一个将作为主页的占位符:

const Home = { template: `

  <div>

    Welcome to Online Restaurant

  </div>

` }

其他路由将连接到一个Menu组件:

const Menu = { template: `

  <div>

    You just ordered

    <img :src="'http://lorempixel.com/200/200/food/' + $route.params.id">

  </div>

` }

在前面的组件中,我们使用$route引用全局路由对象,并从 URL 中获取id参数。Lorempixel.com是一个提供示例图片的网站。我们为每个id连接不同的图片。

最后,使用以下代码创建路由本身:

const router = new VueRouter({

  routes: [

    { path: '/', name:'home', component: Home }, 

    { path: '/menu/:id', name: 'menu', component: Menu },

  ]

})

你可以看到菜单的路径包含/:id,这是一个占位符,用于表示 URL 中的id参数。

最后,编写一个根Vue实例:

new Vue({

  router,

  el: '#app'

})

现在你可以启动应用程序,应该能够看到所有的菜单项。点击其中任何一个应该会点菜:

工作原理...

代码中有两个主要部分贡献于创建不同菜品的路由。

首先,我们使用冒号语法注册了一个通用路由,并为其指定了一个名称,代码如下:

{ path: '/menu/:id', name: 'menu', component: Menu }

这意味着我们可以有一个以/menu/82结尾的 URL,Menu组件将显示,并且$route.params.id变量将设置为82。所以,以下行应该根据以下内容进行更改:

<img :src="'http://lorempixel.com/200/200/food/' + $route.params.id">

在渲染的 DOM 中,上述行将被以下行替换:

<img src="'http://lorempixel.com/200/200/food/82">

不要在现实生活中寻找这样的图片。

请注意,我们还为此路由指定了一个名称。这并不是必需的,但它使我们能够编写代码的第二个主要部分,如下所示:

<router-link :to="{ name: 'menu', params: { id: i } }

">

  Menu {{i}}

</router-link>

我们可以传递一个对象给to属性,而不是写一个字符串,并指定params。在我们的例子中,参数由v-for包装给出。这意味着,例如,在v-for的第四个循环中:

<router-link :to="{ name: 'menu', params: { id: 4} }">

  Menu 4

</router-link>

这将导致 DOM 如下所示:

<a href="#/menu/4" class="">Menu 4</a>

在页面中有多个<router-view>

拥有多个<router-view>可以让您拥有可以使用更复杂布局组织的页面。例如,您可以拥有侧边栏和主视图。本篇介绍了这方面的内容。

准备工作

本篇不使用任何高级概念。但建议您熟悉 vue-router 并学习如何安装它。请参阅本章的第一篇文章以了解更多信息。

操作步骤

本篇将使用大量代码来说明问题。但是,机制非常简单。

我们将构建一个二手硬件商店。我们将拥有一个主视图和一个侧边栏;这些将是我们的router-view。侧边栏将包含我们的购物清单,以便我们始终知道我们要购买的物品,并且没有干扰。

整个 HTML 代码非常简短,因为它只包含一个标题和两个router-view组件:

<div id="app">

  <h1>Second-Hand Hardware</h1>

    <router-view name="list"></router-view>

    <router-view></router-view>

</div>

在这种情况下,列表被命名为router-view。第二个没有名称,因此默认命名为Vue

在 JavaScript 中注册vue-router

Vue.use(VueRouter)

之后,注册路由:

const router = new VueRouter({

  routes: [

    { path: '/',

      components: {

        default: Parts,

        list: List

      }

    },

    { path: '/computer',

      components: {

        default: ComputerDetail,

        list: List

      }

    }

  ]

})

组件不再是一个单独的对象,而是一个包含两个组件的对象:一个用于list,另一个用于默认的router-view

按照示例编写list组件,放在路由代码之前:

const List = { template: `

  <div>

    <h2>Shopping List</h2>

      <ul>

        <li>Computer</li>

      </ul>

  </div>

` }

这将只显示计算机作为我们应该记得购买的项目。

部件组件如下所示;在路由代码之前编写它:

const Parts = { template: `

  <div>

    <h2>Computer Parts</h2>

    <ul>

      <li><router-link to="/computer">Computer</router-link></li>

      <li>CD-ROM</li>

    </ul>

  </div>

` }

它包含一个链接,用于查看有关正在销售的计算机的更多信息;下一个组件绑定到该页面,因此在路由代码之前编写它:

const ComputerDetail = { template: `

  <div>

    <h2>Computer Detail</h2>

    <p>Pentium 120Mhz, CDs sold separately</p>

  </div>

` }

当然,不要忘记添加Vue实例:

new Vue({

  router,

  el: '#app'

})

启动应用程序时,您应该看到两个路由视图一个在另一个上方。如果您希望它们并排显示,可以添加一些 CSS 样式:

工作原理...

在将<router-view>组件添加到页面时,您只需记住为其添加一个名称以便在路由注册期间引用:

<router-view name="view1"></router-view>

<router-view name="view2"></router-view>

<router-view></router-view>

如果不指定名称,路由将被称为默认路由:

routes: [

  { path: '/',

    components: {  

      default: DefaultComponent,

      view1: Component1,

      view2: Component2

    }

  }

]

这样,组件将显示在各自的router-view元素中。

如果未为命名视图指定一个或多个组件,则与该名称关联的router-view将为空。

按层次组织您的路由

在许多情况下,您的网站的组织树可能很复杂。在某些情况下,可能存在明确的分层组织,您可以遵循并使用嵌套路由,vue-routes 可以帮助您保持一切井然有序。最好的情况是,URL 的组织方式与组件的嵌套方式完全对应。

准备工作

在本示例中,您将使用 Vue 的组件和其他基本功能。您还将使用动态路由。请参阅使用命名动态路由示例以了解更多信息。

操作步骤...

在本示例中,您将为一个虚构的世界构建一个在线会计网站。我们将有两个用户-StarkLannister-我们将能够看到这两个用户拥有多少黄金和士兵。

我们网站的 HTML 布局如下:

<div id="app">

  <h1>Kindoms Encyclopedia</h1>

  <router-link to="/user/Stark/">Stark</router-link>

  <router-link to="/user/Lannister/">Lannister</router-link>

  <router-view></router-view>

</div>

我们有一个标题和两个链接-一个用于Stark,一个用于Lannister-最后是router-view元素。

我们将VueRouter添加到插件中:

Vue.use(VueRouter)

然后,我们注册路由:

const router = new VueRouter({

  routes: [

    { path: '/user/:id', component: User,

      children: [ 

        {

          path: 'soldiers',

          component: Soldiers

        },

        {

          path: 'gold',

          component: Gold

        }

      ]

    }

  ]

})

我们所说的是注册一个动态路由/user/:id,在User组件内部,将有另一个router-view,其中包含金币和士兵的嵌套路径。

刚才提到的三个组件按照所示编写,在路由代码之前添加它们:

const User = { template: `

  <div class="user">

    <h1>Kindoms Encyclopedia</h1>

    User {{$route.params.id}}

    <router-link to="gold">Gold</router-link>

    <router-link to="soldiers">Soldiers</router-link>

    <router-view></router-view>

  </div>

`}

正如预期的那样,在User组件内部还有另一个router-view入口,其中包含嵌套的routes组件。

然后,在路由代码之前编写SoldiersGold组件:

const Soldiers = { template: `

  <div class="soldiers">

    <span v-for="soldier in $root[$route.params.id].soldiers"> 

    </span>

  </div>

`}

const Gold = { template: `

   div class="gold">

    <span v-for="coin in $root[$route.params.id].gold">

    </span>

  </div>

`}

这些组件将根据 Vue 根实例数据选项中的金币或士兵变量显示相应数量的表情符号。

这是 Vue 根实例的样子:

new Vue({

  router,

  el: '#app',

  data: {

    Stark: {

      soldiers: 100,

      gold: 50  

    },

    Lannister: {

      soldiers: 50,

      gold: 100

    }

  }

})

启动应用程序将使您能够直观地表示两个用户的金币和士兵数量:

工作原理...

为了更好地理解嵌套路由的工作原理,可以看一下以下图表:

我们的示例中只有两个级别。第一个级别是顶级,由大的包裹矩形表示,对应于/user/:id路由,意味着每个可能匹配的 ID 都在同一级别上。

而内部矩形则是一个嵌套路由和嵌套组件。它对应于金币路由和 Gold 组件。

当嵌套路由对应于嵌套组件时,这是正确的选择。还有两种其他情况需要考虑。

当我们有嵌套组件但没有嵌套路由时,我们只需在嵌套路由前加上斜杠/。这将使其表现得像顶级路由。

例如,考虑将我们的代码更改为以下内容:

const router = new VueRouter({

  routes: [

    { path: '/user/:id', component: User,

      children: [

        {

          path: 'soldiers',

          component: Soldiers

        },

        {

          path: '/gold'

,

          component: Gold

        }

      ] 

    }

  ]

})

/gold路由前加上斜杠将使Gold组件在我们将浏览器指向/goldURL 时出现,而不是/user/Lannister/gold(在这种情况下将导致错误和空白页面,因为未指定用户)。

另一种相反的情况是有嵌套路由但没有相同级别的组件。在这种情况下,只需使用常规语法注册路由。

使用路由别名

有时需要有多个指向同一页的 URL。这可能是因为页面更改了名称,或者因为页面在站点的不同部分中有不同的引用方式。

特别是当页面更改名称时,也非常重要在许多设置中保留以前的名称。链接可能会断开,页面可能无法从网站的某些部分访问。在这个示例中,您将防止出现这种情况。

准备工作

对于这个示例,您只需要对 vue-router 组件有一些了解(如何安装和基本操作)。有关 vue-router 的更多信息将从“使用 vue-router 创建 SPA”示例开始。

操作步骤

假设我们有一个时尚网站,负责给服装命名的员工 Lisa 为两件衣服创建了两个新链接:

<router-link to="/green-dress-01/">Valentino</router-link>

<router-link to="/green-purse-A2/">Prada</router-link>

开发人员在 vue-router 中创建了相应的路由:

const router = new VueRouter({

  routes: [

    {

      path: '/green-dress-01',

      component: Valentino01

    },

    {

      path: '/green-purse-A2',

      component: PradaA2

    }

  ]

})

后来发现这两件衣服不是绿色的,而是红色的。Lisa 并不怪罪,因为她是色盲。

现在您负责更改所有链接以反映列表的真实颜色。首先要做的是更改链接本身。在您编辑后,HTML 布局如下所示:

<div id="app">

  <h1>Clothes Shop</h1>

  <router-link to="/red-dress-01/">Valentino</router-link>

  <router-link to="/red-purse-A2/">Prada</router-link>

  <router-view></router-view>

</div>

您将VueRouter插件添加到Vue中:

Vue.use(VueRouter)

然后,注册新的routes以及旧路由的别名:

const router = new VueRouter({

  routes: [

    {

      path: '/red-dress-01',

      component: Valentino01,

      alias: '/green-dress-01'

    },

    {

      path: '/red-purse-A2',

      component: PradaA2,

      alias: '/green-purse-A2'

    }

  ]

})

以下是所提到的组件的样子:

const Valentino01 = { template: '<div class="emoji">

</div>' }

const PradaA2 = { template: '<div class="emoji">

</div>' }

在启动应用程序之前,请记得实例化一个Vue实例:

new Vue({

  router,

  el: '#app'

})

您可以添加一个 CSS 规则,使表情符号看起来像图片,如下面的屏幕截图所示:

.emoji {

  font-size: 3em;

}

工作原理

即使我们更改了所有链接,我们也无法控制其他实体如何链接到我们的页面。对于搜索引擎(如 Google),没有办法告诉它们删除对旧页面的链接并使用新页面。这意味着如果我们不使用别名,我们可能会遇到大量的破损链接和 404 页面,甚至可能来自我们支付费用链接到不存在页面的广告商的负面宣传。

在路由之间添加过渡效果

我们在“第三章”中详细探讨了过渡效果和动画。在这里,我们将在更改路由时使用它们,而不是更改元素或组件。同样的观察也适用于这里。

准备工作

在尝试这个示例之前,我强烈建议您完成“第三章”中的一些示例,即“过渡和动画”,以及本示例。这个示例是到目前为止学到的概念的混合体。

操作步骤

在这个教程中,我们将为一个餐厅的网站建立一个鬼魂餐厅的网站。除了页面必须淡出而不是立即出现的要求外,它与普通餐厅的网站没有太大的区别。

让我们先来布置一些 HTML 布局:

<div id="app">

  <h1>Ghost's Restaurant</h1>

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li><router-link to="/menu">Menu</router-link></li>  

  </ul>

  <transition mode="out-in">

  <router-view></router-view>

  </transition>

</div>

注意我们如何用transition标签包裹了主路由显示端口。设置out-in模式是因为我们希望消失的组件的动画在其他组件出现之前完成。如果我们没有设置这个,两个淡出的组件会在短暂的时间内叠加在一起。有关更详细的讨论,您可以参考在过渡中让元素在进入阶段之前离开的教程。

现在,让我们创建两个页面/组件:

const Home = { template: '<div>Welcome to Ghost's</div>' }

const Menu = { template: '<div>Today: invisible cookies</div>' }

现在,让我们注册routes

Vue.use(VueRouter)

const router = new VueRouter({

  routes: [

    { path: '/', component: Home },

    { path: '/menu', component: Menu }

  ]

})

在启动应用程序之前,实例化一个Vue对象:

new Vue({

  router,

  el: '#app'

})

为了使过渡效果生效,您需要添加一些 CSS 规则:

.v-enter-active, .v-leave-active {

  transition: opacity .5s;

}

.v-enter, .v-leave-active {

  opacity: 0

}

现在启动您的应用程序。您成功地在页面切换之间添加了淡入淡出的过渡效果。

工作原理...

将整个<router-view>包装在一个过渡标签中将为所有组件执行相同的过渡效果。

如果我们想为每个组件设置不同的过渡效果,我们有另一种选择:我们必须将各个组件自己包装在过渡中。

例如,假设我们有两个过渡效果:spooky 和 delicious。我们希望在Home组件出现时应用第一个过渡效果,在Menu组件出现时应用第二个过渡效果。

我们需要修改我们的组件,如下所示:

const Home = { template: `

  <transition name="spooky">

    <div>Welcome to Ghost's</div>

  </transition>

` }

const Menu = { template: `

  <transition name="delicious">

    <div>Today: insisible cookies!</div>

  </transition>

` }

管理路由错误

如果我们访问的页面不存在或无法正常工作,那么前往链接就没有太多意义。传统上,当发生这种情况时,我们会看到一个错误页面。在 SPA 中,我们更强大,我们可以完全阻止用户前往那里,并显示一个礼貌的消息,说明页面不可用。这极大地增强了用户体验,因为用户可以立即采取其他操作,而无需返回。

准备工作

为了跟上,您应该完成在切换路由之前获取数据的教程。

本教程将在此基础上进行构建,并假设您已经将所有相关代码放置在适当的位置。

如何操作...

正如前面所说,我们将编辑在切换路由之前获取数据教程的结果代码来管理错误。只是为了提醒您,当我们转到/aboutme页面时,我们正在从互联网上加载信息。我们希望在信息不可用的情况下避免转到该页面。

对于这个示例,像之前的示例一样将 Axios 添加为依赖项。

首先,使用下面的代码来丰富 HTML 布局:

<div id="app">

  <h1>My Portfolio</h1>

  <ul>

    <li><router-link to="/" exact>Home</router-link></li>

    <li><router-link to="/aboutme">About Me</router-link></li>

  </ul>

  <router-view></router-view>

 <div class="toast" v-show="showError"> 

 There was an error 

 </div> 

</div>

这是一个弹出消息,每当出现错误时都会显示在屏幕上。使用以下 CSS 规则为其添加一些样式:

div.toast {

  width: 15em;

  height: 1em;

  position: fixed;

  bottom: 1em;

  background-color: red;

  color: white;

  padding: 1em;

  text-align: center;

}

接下来要做的是有一个全局机制将showError设置为true。在 JavaScript 代码的顶部,声明vm变量:

let vm

然后,将我们的Vue根实例分配给它:

vm =

 new Vue({

  router,

  el: '#app',

 data: { 

 showError: false 

 } 

})

我们还将showError变量添加到数据选项中。

最后要做的是在显示个人简介信息之前,实际上管理数据检索时的错误。

将下面的代码添加到beforeRouteEnter钩子中:

beforeRouteEnter (to, from, next) {

  axios.post('http://example.com/

', {

    "type": "object",

    "properties": {

      "name": {

        "type": "string",

        "ipsum": "name"

      },

      "phone": {

        "type": "string",

        "format": "phone"

      }

    }

  }).then(response => {

  next(vm => {

    vm.name = response.data.name

    vm.phone = response.data.phone

  })

}).catch(error => { 

 vm.showError = true 

 next(false) 

 }) 

}

接下来的(false)命令将使用户停留在原地,我们还将端点编辑为example.com,这将在POST请求上返回错误代码:

工作原理...

Axios 将从example.com接收到一个错误,这将触发对我们调用 post 时创建的 promise 的拒绝。promise 的拒绝将反过来触发传递给 catch 的函数。

值得注意的是,在代码的这一点上,vm指的是根Vue实例;这是因为该代码总是在Vue实例初始化并分配给vm之后执行的。

为页面添加进度条以加载页面

确实,使用 SPA,用户不必等待新页面加载,但他仍然需要等待数据加载。在在切换路由之前获取数据示例中,我们在点击按钮进入/aboutme页面后还需要等待一段时间。没有任何提示数据正在加载,然后突然页面出现了。如果用户至少有一些反馈页面正在加载,那不是很好吗?

准备工作

为了跟上,您应该完成在切换路由之前获取数据示例。

本示例将在此基础上构建,并假设您已经有了所有相关的代码。

如何操作...

如前所述,我假设您已经有了从在切换路由之前获取数据示例中得到的所有代码,并且已经正常工作。

对于这个示例,我们将使用一个额外的依赖项--NProgress,一个在屏幕顶部显示加载进度条的小工具。

将以下两行代码添加到页面的头部或 JSFiddle 的依赖项列表中(也有一个 npm 包):

<link rel="stylesheet" href="https://cdn.bootcss.com/nprogress/X/nprogress.css">

<script src="https://cdn.bootcss.com/nprogress/X/nprogress.js"></script>

在这里,XNProgress的版本。在编写时,版本号为 0.2.0,但你可以在网上查找。

完成这一步后,下一步是定义我们希望进度条具有的行为。

首先,我们希望在点击链接后立即显示进度条。为此,我们可以为点击事件添加一个事件监听器,但如果有一百个链接,这将是一个很差的设计。一个更可持续和清晰的方法是通过为路由创建一个新的钩子,并将进度条的出现与路由的切换连接起来。这也将具有提供一致的应用体验的优势:

router.beforeEach((to, from, next) => {

  NProgress.start()

  next()

})

类似地,我们希望在加载成功后进度条消失。这意味着我们希望在回调函数中执行这个操作:

beforeRouteEnter (to, from, next) {

  axios.post('http://schematic-ipsum.herokuapp.com/', {

    "type": "object",

    "properties": {

      "name": {

        "type": "string",

        "ipsum": "name"

      },

      "phone": {

        "type": "string",

        "format": "phone"

      }

    }

  }).then(response => {

 NProgress.done() 

    next(vm => {

      vm.name = response.data.name

      vm.phone = response.data.phone

    })

  })

}

现在你可以启动应用程序,你的进度条应该已经工作了:

它是如何工作的...

这个示例还证明了利用外部库并不难,只要它们易于安装。

由于NProgress组件如此简单且有用,我在这里报告它的 API 作为参考:

  • NProgress.start():显示进度条

  • NProgress.set(0.4):设置进度条的百分比

  • NProgress.inc():稍微增加进度条

  • NProgress.done():完成进度

我们使用了前面两个函数。

作为预防措施,我还建议不要依赖于各个组件调用done()函数。我们在then函数中调用它,但如果下一个开发人员忘记了呢?毕竟,我们在任何路由切换之前都会启动进度条。

最好是在router中添加一个新的钩子:

router.afterEach((to, from) => {

  NProgress.done()

})

由于done函数是幂等的,我们可以随意调用它多次。因此,这不会修改我们应用程序的行为,并且将确保即使将来的开发人员忘记关闭进度条,它也会在路由改变后自动消失。

如何重定向到另一个路由

你可能有无数个原因希望重定向用户。你可能希望用户在访问页面之前先登录,或者页面已经移动,你希望用户注意到新的链接。在这个示例中,你将重定向用户到一个新的主页,作为快速修改网站的一种方式。

准备工作

这个示例只会使用关于 vue-router 的基本知识。如果你已经完成了使用 vue-router 创建单页应用的示例,你就可以开始了。

如何做到这一点...

假设我们有一个在线服装店。

这将是网站的 HTML 布局:

<div id="app">

  <h1>Clothes for Humans</h1>

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li><router-link to="/clothes">Clothes</router-link></li>

  </ul>

  <router-view></router-view>

</div>

它只是一个带有指向服装列表的链接的页面。

让我们注册VueRouter

Vue.use(VueRouter)

我们的网站有三个页面,分别由以下组件表示:

const Home = { template: '<div>Welcome to Clothes for Humans</div>' }

const Clothes = { template: '<div>Today we have shoes</div>' }

const Sales = { template: '<div>Up to 50% discounts! Buy!</div>' }

它们代表主页、服装列表和去年使用的带有一些折扣的页面。

让我们注册一些routes

const router = new VueRouter({

  routes: [

    { path: '/', component: Home }

    { path: '/clothes', component: Clothes },

    { path: '/last-year-sales', component: Sales }

  ]

})

最后,我们添加一个根Vue实例:

new Vue({

  router,

  el: '#app'

})

您可以启动应用程序,它应该可以正常工作,没有任何问题。

黑色星期五明天就要到了,我们忘记了这是全球时尚界最大的活动。我们没有时间重写主页,但去年销售的那个页面可以解决问题。我们将把访问我们主页的用户重定向到那个页面。

为了实现这一点,我们需要修改我们注册的routes

const router = new VueRouter({

  routes: [

    { path: '/', component: Home, redirect: '/last-year-sales'

 },

    { path: '/clothes', component: Clothes },

    { path: '/last-year-sales', component: Sales }

  ]

})

只需添加该重定向,我们就能挽救这一天。现在,每当您访问主页时,都会显示销售页面。

它是如何工作的...

当匹配到根路由时,Home组件将不会被加载。而是匹配到/last-year-sales路径。我们也可以完全省略组件,因为它永远不会被加载:

{ path: '/', redirect: '/last-year-sales' }

还有更多...

在 vue-router 中,重定向比我们刚才看到的更强大。在这里,我将尝试通过重定向为刚刚创建的应用程序增加更多功能。

重定向到 404 页面

通过在最后一个路由中添加一个捕获所有路由,可以重定向找不到的页面。它将匹配到其他路由未匹配到的所有内容:

...

{ path: '/404', component: NotFound },

{ path: '*', redirect: '/404' }

命名重定向

重定向可以与命名路由结合使用(参考“使用命名动态路由”配方)。我们可以通过名称指定目标:

...

{ path: '/clothes', name: 'listing', component: Clothes },

{ path: '/shoes', redirect: { name: 'listing' }}

带参数的重定向

在重定向时,您还可以保留参数:

...

{ path: '/de/Schuh/:size', redirect: '/en/shoe/:size' },

{ path: '/en/shoe/:size', component: Shoe }

动态重定向

这是最终的重定向。您可以访问用户尝试访问的路由,并决定要将其重定向到何处(但无法取消重定向):

...

{ path: '/air', component: Air },

{ path: '/bags', name: 'bags', component: Bags },

{ path: '/super-shirt/:size', component: SuperShirt },

{ path: '/shirt/:size?', component: Shirt},

{ path: '/shirts/:size?',

  redirect: to => {

    const { hash, params, query } = to

    if (query.colour === 'transparent') {

      return { path: '/air', query: null }

    }

    if (hash === '#prada') {

      return { name: 'bags', hash: '' }

    }

    if (params.size > 10) {

      return '/super-shirt/:size'

    } else {

      return '/shirt/:size?'

    }

  }

}

在返回时保存滚动位置

在 vue-router 中,有两种导航模式:hashhistory。默认模式和前面的示例中使用的模式是history。传统上,当您访问一个网站,向下滚动一点并点击链接到另一个页面时,新页面从顶部显示。当您点击浏览器的返回按钮时,页面从先前滚动的高度显示,并且您刚刚点击的链接可见。

当你在 SPA(单页应用)中时,这并不是真实的,或者至少不是自动的。vue-router 的历史模式可以让你模拟这一点,甚至更好的是,可以对滚动行为进行精细控制。

准备中

为了完成这个步骤,我们需要切换到历史模式。历史模式只在应用程序在正确配置的服务器上运行时才有效。如何为 SPA 配置服务器超出了本书的范围(但原则是每个路由都从服务器端重定向到index.html)。

我们将使用一个 npm 程序来启动一个小型服务器;我们期望您已经安装了 npm(您可以查看“选择开发环境”这个教程来了解更多关于 npm 的信息)。

如何做到这一点...

首先,您将安装一个紧凑的服务器用于单页应用程序,以便历史模式能够正常工作。

在您喜欢的命令行中,进入包含您的应用程序的目录。然后,输入以下命令:

    npm install -g history-server

    history-server .

运行服务器后,您需要将浏览器指向http://localhost:8080,如果您的目录中有一个名为index.html的文件,它将被显示出来;否则您将看不到太多内容。

创建一个名为index.html的文件,并填写一些样板内容,就像在“选择开发环境”一节中所示。我们希望得到一个只有Vuevue-router作为依赖项的空白页面。我们的空白画布应该如下所示:

<!DOCTYPE html>

<html>

<head>

  <script src="https://unpkg.com/vue/dist/vue.js"></script>

  <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

</head>

<body>

  <div id="app">

  </div>

  <script>

    new Vue({

      router,

      el: '#app'

    })

  </script>

</body>

</html>

作为 HTML 布局,将以下内容放在body中:

<div id="app">

  <h1>News Portal</h1>

    <ul>

      <li><router-link to="/">Home</router-link></li>

      <li><router-link to="/sports">Sports</router-link></li>

      <li><router-link to="/fashion">Fashion</router-link></li>

    </ul>

  <router-view></router-view>

</div>

我们有一个带有三个链接和一个router-view入口点的标题。我们将为体育和时尚页面创建两个长页面:

const Sports = { template: `

  <div>

    <p v-for="i in 30">

      Sample text about sports {{i}}.

    </p>

    <router-link to="/fashion">Go to Fashion</router-link>

    <p v-for="i in 30">

      Sample text about sports {{i + 30}}.

    </p>

  </div>

` }

const Fashion = { template: `

  <div>

    <p v-for="i in 30">

      Sample text about fashion {{i}}.

    </p>

    <router-link to="/sports">Go to Sports</router-link>

    <p v-for="i in 30">

      Sample text about fashion {{i + 30}}.

    </p>

  </div>

` }

对于主页组件,我们只需要一个存根:

const Home = { template: '<div>Welcome to BBCCN</div>' }

为这个新闻网站编写一个合理的路由器:

Vue.use(VueRouter)

const router = new VueRouter({

  routes: [

    { path: '/', component: Home },

    { path: '/sports', component: Sports },

    { path: '/fashion', component: Fashion } 

  ]

})

如果您现在使用浏览器访问之前指定的地址,您应该能看到网站正在运行。

转到体育页面,向下滚动直到看到链接,然后点击它。

注意您正在访问的页面不是从开始显示的。这在传统网站中不会发生,也是不可取的。

点击返回按钮,注意我们回到了上次离开页面的位置;我们希望保留这种行为。

最后,注意页面的 URL 看起来不太自然,而是带有哈希符号;我们希望 URL 看起来更好:

为了实现这一点,让我们将路由器代码修改为以下内容:

const router = new VueRouter({

 mode: 'history',

  routes: [

    { path: '/', component: Home },

    { path: '/sports', component: Sports },

    { path: '/fashion', component: Fashion }

  ],

 scrollBehavior (to, from, savedPosition) { 

 if (savedPosition) { 

 return savedPosition 

 } else { 

 return { x: 0, y: 0 } 

 } 

 } 

})

我们添加了一行指定新模式为 history(链接中没有哈希),并定义了scrollBehavior函数以返回到上次位置(如果存在);如果是新页面,它应该滚动到左上角。

您可以通过刷新浏览器并返回到主页来尝试此操作。

打开体育页面,并点击页面中间的链接。新页面现在从开始显示。

点击返回,savedPosition被恢复。

注意现在 URL 看起来更好了:

工作原理...

当您在浏览器中使用包含哈希符号的 URL 时,浏览器将发送一个不包含哈希后缀的 URL 的请求,也就是说,当您在页面内部有一个事件,该事件转到相同页面但具有不同的哈希后缀时:

http://example.com#/page1

 on  http://example.com#/page2

浏览器不会重新加载页面;这就是为什么当用户点击一个只修改哈希而不重新加载页面的链接时,vue-router 可以修改页面的内容。

当您将模式从hash更改为history时,vue-router 将删除哈希符号,并利用“history.pushState()”函数。

这个函数添加了另一个虚拟页面并将 URL 更改为其他内容:

http://example.com/page1

 =pushState=> http://example.com/page2

浏览器不会发送 GET 请求来查找page2;实际上,它什么都不会做。

当您按下返回按钮时,浏览器会恢复 URL,并且 vue-router 会接收到一个事件。然后它将读取 URL(现在是“page1”)并匹配相关的路由。

我们紧凑的历史服务器的作用是将每个 GET 请求重定向到index.html页面。这就是为什么当我们尝试直接访问http://localhost:8080/fashion时,我们不会得到 404 错误的原因。

第六章:单元测试和端到端测试

本章将介绍以下内容:

  • 使用 Jasmine 进行 Vue 测试

  • 将 Karma 添加到工作流程中

  • 测试应用程序的状态和方法

  • 测试 DOM

  • 测试 DOM 异步更新

  • 使用 nightwatch 进行端到端测试

  • 在 nightwatch 中模拟双击

  • 不同风格的单元测试

  • 使用 Sinon.JS 对外部 API 调用进行存根

  • 测量代码的覆盖率

介绍

测试是真正区分专业软件和业余软件的关键。根据行业经验和研究,发现软件成本的很大一部分在于在软件投入生产时纠正错误。测试软件可以减少生产中的错误,并使纠正这些错误的成本大大降低。

在本章中,您将学习如何设置测试工具和编写单元测试和集成测试,以加快应用程序开发速度,并使其在复杂性增加时不留下错误。

在完成这些示例后,您将熟悉最流行的测试框架和术语;您将能够自信地发布按预期工作的软件。

使用 Jasmine 进行 Vue 测试

Jasmine 是一个用于测试的库,非常易于使用,并且能够直接在浏览器中显示测试结果。在这个示例中,您将构建一个简单的 Vue 应用程序,并使用 Jasmine 进行测试。

准备工作

希望您不是从这个示例开始学习 Vue,因为我将假设,就像本章的其他部分一样,您已经了解了在 Vue 中构建简单应用程序的基础知识。

您还应该能够在互联网上找到四个文件。我将在写作时提供链接,但是当然,它们可能会发生变化:

您可以方便地从cdnjs.com/libraries/jasmine页面复制粘贴所有链接。

这些文件彼此依赖,因此添加它们的顺序很重要!特别是,boot.js 依赖于 jasmine-html.js,而 jasmine-html.js 又依赖于 jasmine.js

如何做到这一点...

Jasmine 是一个由各种模块组成的库。为了使其工作,您需要安装一些与 Jasmine 相关的依赖项。我假设您正在使用 JSFiddle 进行操作。如果您使用的是 npm 或其他方法,您应该能够根据原则简单地推导出需要更改的内容。

要在您的应用程序中安装 Jasmine,您将需要四个不同的依赖项,其中一个仅用于 CSS 样式。

这四个文件的顺序(按依赖关系排序)是:

  • jasmine.css

  • jasmine.js 是一个用于 JavaScript 测试的开源框架。它提供了一套简洁的语法和功能,用于编写和执行单元测试和集成测试。jasmine.js 可以帮助开发人员轻松地编写可靠的测试用例,以确保代码的质量和稳定性。无论是在前端还是后端开发中,jasmine.js 都是一个非常有用的工具。

  • jasmine-html.js(依赖于前面的 js 文件)

  • boot.js(依赖于前面的 js 文件)

你应该能够在 CDNJS 或其他 CDN 上找到所有这些文件。按照显示的顺序安装它们,否则它们将无法正常工作。

当你把所有文件放好后,写下以下 HTML 代码:

<div id="app">

  <p>{{greeting}}</p>

</div>

然后,将以下脚本添加为 JavaScript 部分:

new Vue({

  el: '#app',

  data: {

    greeting: 'Hello World!'

  }

})

现在可以启动应用程序了,正如预期的那样,屏幕上会出现Hello World的消息。

我们希望在对应用程序进行修改和添加新功能时,能够确保我们的应用程序始终显示这条消息。

在这方面,Jasmine 将帮助我们。在 Vue 实例之后,我们编写以下 JavaScript 代码:

describe('my app', () => {

  it('should say Hello World', () => {

    expect(document.querySelector('p').innerText)

      .toContain('Hello World')

  })

})

为了使其在 JSFiddle 中工作,需要将 Load Type 设置为 No wrap - in 。如果保持默认的 Load Type onLoad,它将在 Vue 有机会启动之前加载 Jasmine。

现在尝试启动应用程序。您将在页面末尾看到 Jasmine 的详细报告,告诉您应用程序是否有问题。

如果一切如预期,您应该会看到一个快乐的绿色条,如下所示:

工作原理...

您为 Vue 应用程序编写了第一个单元测试。如果您已经编写了单元测试,那么一切都应该很清楚,因为我们没有使用任何 Vue 特有的功能来编写测试。

无论如何,让我们花点时间分析我们编写的代码;之后,我将提供一些关于在编写真实应用程序时何时编写类似测试的考虑事项。

我们编写的测试在网页上读作“我的应用程序应该说 Hello World”。

这是一条相当通用的消息;然而,让我们仔细看一下代码:

expect(document.querySelector('p').innerText)

  .toContain('Hello World')

将其作为一个英语短语来阅读-我们期望文档中的<p>元素包含文本Hello World

document.querySelector('p')代码选择页面内的第一个p元素,确切地说。innerText查找 HTML 元素内部并返回可读的文本。然后,我们验证该文本是否包含Hello World

在实际应用中,你不会将测试代码直接写在网页下方。测试对于开发者来说非常重要,可以在每次代码更改后自动验证每个功能是否正常工作,而无需手动验证。另一方面,你不希望用户看到测试结果。

通常情况下,您将拥有一个专门的页面,只有开发人员可以访问,该页面会为您运行所有的测试。

还有更多...

在软件开发中有一种广泛的实践叫做TDD或者测试驱动开发。它鼓励你将软件的功能视为测试。这样一来,你可以通过测试本身的工作来确保软件中的功能正常运行。

在这一部分中,我们将使用 TDD 为我们的食谱添加一个功能。我们希望页面上有一个标题,上面写着“欢迎”。

首先,在 hello world 测试之后,我们将为describe函数内的功能编写一个(失败的)测试。

it('should have an header that says `Welcome`', () => {

  expect(document.querySelector('h1').innerText)

    .toContain('Welcome')

})

当我们启动测试时,我们应该看到它失败:

现在,不要太关注堆栈跟踪。你应该注意的重要事情是,我们有一个测试失败的名称(另一个测试仍然有效)。

在实现功能本身之前,编写测试并确保它失败是很重要的。要理解为什么,试着想象一下,我们在实现功能之前编写了测试,然后我们启动它,然后它成功了。这意味着测试实际上并没有起作用,因为我们从一开始就没有实现功能。

如果你认为这只是奇怪和不可能的,请再次思考。在实践中,经常发生这样的情况,一个看起来完全正常的测试实际上并没有测试任何东西,并且无论功能是否损坏,它总是成功的。

在这一点上,我们已经准备好实际实现功能了。我们编辑 HTML 布局,像这样:

<div id="app">

  <h1>Welcome</h1>

  <p>{{greeting}}</p>

</div>

当我们启动页面时,结果应该类似于这样:

为你的工作流添加一些 Karma

Karma 是一个 JavaScript 测试运行器。这意味着它将为您运行测试。软件往往会迅速增长,Karma 为您提供了一种同时运行所有单元测试的方法。它还为您提供了添加监视测试覆盖率和代码质量的工具的能力。

Karma 在 Vue 项目中传统上被使用,并且作为一个工具存在于官方 Vue 模板中。学习 Karma 是您 JavaScript 工具箱的一个很好的补充,即使您不使用 Vue 也是如此。

准备工作

我认为已经完成了“使用 Jasmine 测试 Vue”的先决条件。由于 Karma 是一个测试运行器,所以您应该首先能够编写测试。

在这个教程中,我们将使用 npm,所以在继续之前,请确保已经安装了它,并阅读有关如何使用它的基础知识。

如何操作...

对于这个教程,我们将需要命令行和 npm,所以在继续之前,请确保已经安装了它。

在一个新的文件夹中,创建一个名为package.json的文件,并在其中写入以下内容:

{

  "name": "my-vue-project",

  "version": "1.0.0"

}

只要将此文件放在您的文件夹中,就会创建一个新的 npm 项目。我们稍后会编辑这个文件。

在命令行中,进入项目所在的目录,并在其中输入以下命令来安装必要的依赖项:

npm install --save-dev vue karma jasmine karma-jasmine karma-chrome-launcher

这将安装 Vue 以及 Karma、Jasmine 和 Karma 的一些插件作为我们项目的依赖项。

如果您现在查看package.json,您会看到它已相应地更改。

下一个命令将创建一个名为karma.conf.js的文件,其中包含 Karma 的配置:

./node_modules/karma/bin/karma init

这将询问您一些问题,除了询问源文件和测试文件的位置时,其他问题都选择默认值。对于该问题,只需写入*.js。完成后,您应该能够在目录中看到karma.conf.js文件。打开它并快速查看您通过回答问题设置的所有设置。

由于 Karma 不知道 Vue,您需要进行一些小的修改,将 Vue 添加为 Karma 的依赖项。有几种方法可以做到这一点;最快的方法可能是在要加载的文件列表中添加一行。在karma.conf.js文件中,在files数组中添加以下行:

...    

    // list of files / patterns to load in the browser 

files:

 [ 

'node_modules/vue/dist/vue.js'

 **,** 

'*.js' 

 ],

... 

请注意,当您回答问题时,您也可以直接添加该行。

下一步是编写我们要测试的应用程序。

在您的文件夹中,创建一个名为myApp.js的文件;在其中写入以下内容:

const myApp = {

  template: `

    <div>

      <p>{{greetings}}</p>

    </div>

  `,

  data: {

    greetings: 'Hello World'

  }

}

我们分配给myApp的对象只是一个简单的 Vue 实例。

接下来,我们将为其创建一个测试。具体来说,我们将检查组件中是否包含Hello World文本。

创建一个名为test.js的文件,并在其中写入以下内容:

describe('my app', () => {

  beforeEach(() => {

    document.body.innerHTML = `

      <div id="app"></div>

    `

    new Vue(myApp)

      .$mount('#app')

  })

  it('should say Hello World', () => {

    expect(document.querySelector('p').innerText)

      .toContain('Hello World')

  })

})

beforeEach块将在每个测试之前运行(现在我们只有一个测试),在检查其他功能之前重置我们的 Vue 应用程序的状态。

现在,我们可以运行我们的测试了。在终端中输入以下命令:

./node_modules/karma/bin/karma start

你应该看到 Chrome 启动,如果你回到命令行,你应该收到类似于以下消息:

这意味着你的测试成功了。

工作原理...

在你的配方完成后,你应该注意你的应用程序的一般结构。你有应用程序本身在myApp.js中,然后你有你的测试在test.js中。你有一些配置文件,如karma.conf.jspackage.json,你的库在node_modules目录中。所有这些文件一起工作,使你的应用程序可测试。

在一个真实的应用程序中,你可能会有更多的源代码和测试文件,而配置文件通常增长得更慢。

在整个设置中,你可能会想知道如何启动应用程序本身。毕竟,没有 HTML,我们只启动了测试;我们从来没有见过这个Hello World程序。

实际上,你是对的;这里没有要启动的程序。事实上,我们必须在测试的beforeEach中为 HTML 布局编写一个固定装置:

beforeEach(() => {

  document.body.innerHTML = `

    <div id="app"></div>

  `

  new Vue(window.myApp)

    .$mount('#app')

})

在上述代码中,我们注入了 HTML,它只包含一个<div>元素(其余的布局在myApp.js中)在页面中。

然后,我们创建一个新的 Vue 实例,传递在myApp.js中定义的myApp变量中包含的选项对象;然后我们使用$mount('#app') Vue API,在我们刚刚注入的<div>元素中实际地实现应用程序。

还有更多...

每次从node_modules目录中调用 Karma 可能会很烦人。有两种方法可以使这更愉快:我们可以全局安装 Karma,或者我们可以将 Karma 添加到我们的 npm 脚本中;我们将两者都做。

首先,让我们将 Karma 添加到我们的 npm 脚本中。进入package.json文件,并添加以下代码块:

...

"version": "1.0.0",

  "scripts": {

    "test": "./node_modules/karma/bin/karma start"

  },

"devDependencies": {

...

现在,你可以输入npm run test,Karma 将自动启动。接下来,我们可以使用以下命令全局安装 Karma:

npm install -g karma

现在我们可以编写诸如karma initkarma start之类的命令,并且它们将被识别。我们还可以编辑我们的package.json,像这样:

...

"version": "1.0.0",

  "scripts": {

    "test": "karma start"

  },

"devDependencies": {

...

测试应用程序的状态和方法

在这个示例中,我们将编写一个单元测试来直接触摸和检查我们的 Vue 实例的状态。测试组件状态的优势是我们不必等待 DOM 更新,即使 HTML 布局发生变化,状态变化也会慢得多,从而减少了测试所需的维护量。

准备工作

在尝试这个示例之前,您应该完成将一些 Karma 添加到您的工作流程,因为我们将描述如何编写测试,但我们不会提及测试环境的设置。

如何做...

假设我们有一个应用程序,它用Hello World!来问候您,但它还有一个按钮可以将问候语翻译成意大利语,即Ciao Mondo!

为此,您需要在一个新文件夹中创建一个新的 npm 项目。在那里,您可以使用以下命令安装此示例所需的依赖项:

npm install --save-dev vue karma jasmine karma-jasmine karma-chrome-

   launcher

要设置 Karma,就像在前一个示例中一样,运行以下命令:

./node_modules/karma/bin/karma init

除了问题您的源文件和测试文件的位置是什么?,保留默认答案;对于这个问题,您应该用以下两行回答:

  • node_modules/vue/dist/vue.js

  • *.js

创建一个名为test.js的文件,并在其中编写一个beforeEach,以便将应用程序恢复到其起始状态,以便可以独立于其他测试进行测试:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue({

      template: `

        <div>

          <p>{{greetings}}</p>

          <button @click="toItalian">

            Translate to Italian

          </button>

        </div>

      `,

      data: {

        greetings: 'Hello World!'

      },

      methods: {

        toItalian () {

          this.greetings = 'Ciao Mondo!'

        }

      } 

    }).$mount()

  })

})

请注意,您在开始时声明了vm变量来引用我们的 Vue 实例。

beforeEach之后(但在describe内部),添加以下(目前为空)测试:

it(`should greet in Italian after

  toItalian is called`, () => {

})

在测试的第一部分中,您将使组件达到所需的状态(在调用toItalian之后):

it(`should greet in Italian after

    toItalian is called`, () => {

 vm.toItalian()

})

现在,我们想要检查问候语是否已更改:

it(`should greet in Italian after

    toItalian is called`, () => {

  vm.toItalian()

 expect(vm.greetings).toContain('Ciao Mondo')

})

现在,为了证明每个测试之前状态都被重置了,添加以下内容:

it('should greet in English', () => {

  expect(vm.greetings).toContain('Hello World')

})

如果状态真的被重置了,它应该包含英文问候语,如果你启动测试(使用./node_modules/karma/bin/karma start命令),你会发现(如果没有错误的话)确实是这样的。

工作原理...

由于我们有 Vue 实例本身的引用,我们可以直接在测试中访问方法和状态变量。

我希望你花一些时间欣赏测试的名称。第一个标题为should greet in Italian after toItalian is called。它没有提到页面或图形,并且没有对前提条件做任何假设。请注意,按钮从未被点击过,事实上,在测试标题中也没有提到按钮。

如果我们将测试标题命名为should display 'Ciao Mondo' when Translate button is clicked on,那么我们就会撒谎,因为我们从未检查问候语是否实际显示,并且我们在测试中从未点击按钮。

在真实应用程序中,正确命名测试非常重要,因为当你有成千上万个测试时,如果有一个测试失败,你首先读到的是标题或测试应该检查的内容。如果标题误导了,你将花费很多时间追逐一个错误的线索。

测试 DOM

在这个示例中,您将学习一种技术,可以快速测试 DOM 或网页本身是否符合预期,即使 Vue 组件不在页面中。

准备工作

对于这个示例,您应该已经有一个已经设置好并且工作正常的测试环境;如果您不知道这是什么意思,请完成使用 Jasmine 进行 Vue 测试示例。

我假设您已经安装了 Jasmine 并且可以执行测试。

基本上,您只需要一个网页(JSFiddle 可以)和这四个已安装的依赖项:

  • jasmine.css

  • jasmine.js

  • jasmine-html.js

  • boot.js

如果您正在使用 JSFiddle 或手动添加它们,请记住按指定的顺序添加它们。

在“使用 Jasmine 进行 Vue 测试”配方中找到这些文件的链接。

操作步骤如下:

假设您正在编写一个显示“Hello World!”问候语的组件;您希望测试该问候语是否实际显示,但您正在测试的网页已经足够复杂,您希望在隔离环境中测试您的组件。

事实证明,您不必实际显示组件来证明它的工作。您可以在文档之外显示和测试您的组件。

在您的测试文件或页面的测试部分中,编写以下设置来显示问候语:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue({

      template: '<div>{{greetings}}</div>',

      data: {

        greetings: 'Hello World'

      }

    })

  })

})

为了将我们的 Vue 实例实现为一个文档之外的元素,我们只需要添加$mount() API 调用:

beforeEach(() => {

    vm = new Vue({

      template: '<div>{{greetings}}</div>',

      data: {

        greetings: 'Hello World'

      }

    }).$mount()

  })

由于我们有对vm的引用,我们现在可以测试我们的组件以访问在文档之外渲染的元素:

it('should say Hello World', () => {

  expect(vm.$el.innerText).toContain('Hello World')

})

vm.$el元素代表我们的组件,但无法从正常的 DOM 中访问。

工作原理如下:

在初始化时,Vue 实例会检查是否有el选项。在我们的示例中,我们通常包含一个el选项,但这次我们有一个模板:

vm = new Vue({

  template: '<div>{{greetings}}</div>',

  data: {

    greetings: 'Hello World'

  }

}).$mount()

当 Vue 实例具有el选项时,它会自动挂载到该元素(如果找到);在我们的情况下,Vue 实例等待$mount调用。我们不提供任何参数给函数,因此组件会在文档之外渲染。

此时,从 DOM 中检索它的唯一方法是通过$el属性。一旦组件被挂载,$el属性始终存在,无论组件是手动挂载还是自动挂载。

从那里,我们可以像访问任何普通组件一样访问它,并测试一切是否符合我们的预期。

测试 DOM 的异步更新。

在 Vue 中,当组件的状态发生变化时,DOM 会相应地发生变化;这就是为什么我们称之为响应式状态的原因。唯一需要注意的是,更新不是同步的;我们必须等待额外的时间来实际传播这些变化。

准备中

对于这个配方,我假设你已经完成了“使用 Jasmine 进行 Vue 测试”的配方,并且知道如何编写基本测试。

如何做到这一点...

我们将编写的测试是 Vue 更新机制工作原理的示例。从那里,您将能够自己编写异步测试。

在我们的测试套件的beforeEach函数中,编写以下 Vue 实例:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue({

      template: `

        <div>

          <input id="name" v-model="name">

          <p>Hello from 

            <span id="output">{{name}}</span>

          </p>

        </div>

      `,

      data: {

        name: undefined

      }

    }).$mount()

  })

})

这将创建一个组件,其中包含一个文本框和一个 span 元素,该 span 元素将包含Hello from ...短语以及文本框中输入的任何内容。

我们将如何测试这个组件是,在文本框中编写Herman(通过编程方式,而不是手动),然后等待 DOM 更新。当 DOM 更新后,我们检查是否出现了Hello from Herman这个短语。

让我们从beforeEach函数之后的一个空测试开始:

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  done()

})

前面的测试已经通过了。请注意,我们正在接收done参数,然后将其作为函数调用。只有在调用done()之后,测试才会通过。

<span>元素分配给一个变量以方便操作,然后将文本Herman插入到文本框中。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

 const outputEl = vm.$el.querySelector('#output')

 vm.$el.querySelector('#name').value = 'Herman'

  done()

})

当我们修改状态时,我们必须等待 DOM 更新,但反之则不然;当我们修改了 DOM 时,我们可以立即检查name变量是否已更改。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  const outputEl = vm.$el.querySelector('#output')

  vm.$el.querySelector('#name').value = 'Herman'

 expect(vm.name = 'Herman')

  done()

})

在您编辑测试时,启动它以检查是否正常工作。

接下来,我们将为Vue组件的下一个更新周期安装一个监听器,称为 tick。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  const outputEl = vm.$el.querySelector('#output')

  vm.$el.querySelector('#name').value = 'Herman'

  expect(vm.name = 'Herman')

 vm.$nextTick(() => {

    done()

 })

})

$nextTick块中的所有内容只有在 DOM 更新后才会运行。我们将检查<span>元素的内容是否已更改。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  const outputEl = vm.$el.querySelector('#output')

  vm.$el.querySelector('#name').value = 'Herman'

 expect(outputEl.textContent).not.toContain('Herman')

  expect(vm.name = 'Herman')

  vm.$nextTick(() => {

 expect(outputEl.textContent).toContain('Herman')

    done()

  })

})

请注意,在进行下一次操作之前,我们还会验证 DOM 是否未更改。

工作原理如下...

官方文档中指出:

Vue 以异步方式执行 DOM 更新。每当观察到数据变化时,它将打开一个队列并缓冲在同一事件循环中发生的所有数据变化。

因此,许多测试需要使用$nextTick辅助函数。然而,目前正在努力创建更好的工具来处理测试和同步性,因此,尽管本文档说明了问题,但可能不是处理测试的最新方法。

使用 Nightwatch 进行端到端测试

有时单元测试并不能满足需求。我们可能需要集成两个独立开发的功能,并且尽管每个功能都经过了单元测试并且可以正常工作,但没有简单的方法来同时测试它们。此外,这也违背了单元测试的目的-测试软件的原子单元。在这种情况下,可以进行集成测试和端到端(end-to-end)测试。Nightwatch 是一种模拟用户在网站上点击和输入的软件。这可能是我们想要的最终验证整个系统是否正常工作的方式。

准备工作

在开始进行这个稍微高级的示例之前,您应该已经熟悉命令行和 npm。如果您对它们不熟悉,请查看选择开发环境示例。

操作步骤...

为这个示例创建一个新文件夹,并在其中创建一个名为index.html的新文件。

这个文件将包含我们的 Vue 应用程序,也是我们要测试的内容。在这个文件中写入以下内容:

<!DOCTYPE html>

<html>

<head>

  <title>Nightwatch tests</title>

  <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

  <div id="app">

  </div>

  <script>

  </script>

</body>

</html>

正如您所看到的,这只是一个小型 Vue 应用程序的常规样板。在<div>标签内放置一个标题和一个按钮;当我们点击按钮时,将显示文本Hello Nightwatch!

<div id="app">

  <h2>Welcome to my test page</h2>

  <button @click="show = true">Show</button>

  <p v-show="show">Hello Nightwatch!</p>

</div>

<script>标签内,写入以下 JavaScript 代码使其工作:

<script>

  const vm = new Vue({

    el: '#app',

    data: {

      show: false

    }

  })

</script>

我们的应用程序已经完成;现在我们进入了示例的测试部分。

执行以下命令来安装您的依赖项:

npm install -g selenium-standalone http-server nightwatch

这将安装 Selenium 服务器,这是自动化浏览器操作所必需的,也是使 nightwatch 工作的真正原因。http-server命令将有助于在不必记住长文件路径的情况下提供我们的工作网站。最后,它将安装 nightwatch 本身,它在很大程度上是 Selenium 的包装器和 JavaScript API。

当 npm 完成安装所有这些工具后,创建一个名为nightwatch.json的新文件,其中包含 nightwatch 的配置,并在其中写入以下内容:

{

  "src_folders" : ["tests"],

  "test_settings" : {

    "default" : {

      "desiredCapabilities": {

        "browserName": "chrome"

      }

    }

  }

}

第一个设置表示您将在名为 tests 的文件夹中编写所有测试(我们将创建该文件夹);第二个设置只是将 Chrome 设置为我们运行测试的默认浏览器。

现在,在test目录下创建一个test.js文件。在该文件中,我们将测试应用程序。我们将验证当应用程序启动时,<p>标签是不可见的,并且当我们点击按钮时,它应该出现。

一个空的测试看起来像这样:

module.exports = {

  'Happy scenario' :client => {}

}

在这里,客户端是浏览器(在本例中为 Chrome)。

我们将在http://localhost:8080地址上提供我们的应用程序,所以首先我们希望浏览器转到这个地址。为此,我们将编写以下代码:

module.exports = {

  'Happy scenario' :client => {

    client

 .url('http://localhost:8080')

  }

}

接下来,我们等待页面加载;我们通过等待具有id="app"<div>出现来间接实现这一点:

module.exports = {

  'Happy scenario' :client => {

    client

      .url('http://localhost:8080')

 .waitForElementVisible('#app', 1000)

  }

}

第二个参数是在考虑测试失败之前愿意等待的毫秒数。

接下来,我们希望确保标题也正确显示,并且没有可见的<p>元素:

module.exports = {

  'Happy scenario' :client => {

    client

      .url('http://localhost:8080')

      .waitForElementVisible('#app', 1000)

      .assert.containsText('h2', 'Welcome to')

 .assert.hidden('p')

  }

}

然后,我们点击按钮并断言<p>元素是可见的并且包含单词Nightwatch

module.exports = {

  'Happy scenario' :client => {

    client

      .url('http://localhost:8080')

      .waitForElementVisible('#app', 1000)

      .assert.containsText('h2', 'Welcome to')

      .assert.hidden('p')

      .click('button')

 .waitForElementVisible('p', 1000)

 .assert.containsText('p', 'Nightwatch')

 .end();

  }

}

end()函数将标记测试已成功,因为没有更多需要检查的内容。

要实际运行此测试,您需要运行以下命令:

selenium-standalone install

这将安装 Selenium,然后打开三个不同的命令行。在第一个命令行中,使用以下命令启动 Selenium 服务器:

selenium-standalone start

在第二个命令行中,进入你的食谱文件夹的根目录,即index.html所在的位置,并启动http-server

http-server .

启动后,它会告诉你你的网站在http://localhost:8080上提供服务。这就像我们在测试中写的地址一样。你现在可以导航到该地址查看应用程序的运行情况:

最后,在第三个命令行中,再次进入你的食谱文件夹,并输入以下命令:

nightwatch

如果一切顺利,你会看到浏览器在你眼前闪烁,并在一瞬间(取决于你的计算机速度)显示应用程序,在控制台中,你应该看到类似于这样的内容:

工作原理...

如果这个食谱看起来很费劲,不要灰心,Vue 模板已经在其中解决了所有的设置。你知道所有这些机制是如何工作的,但是当我们在后面的食谱中使用 Webpack 时,你只需要一个命令来运行端到端测试,因为一切都已经设置好了。

注意端到端测试的标题是相当通用的,它指的是一个特定的操作流程,而不是详细描述上下文和期望的内容。这在端到端测试中很常见,因为通常最好先构建用户故事,然后将它们分支,并为每个分支命名一个特定的场景。所以,举个例子,如果我们期望从服务器得到一个响应,但没有返回,我们可以测试一个场景,在这个场景中我们会出现一个错误,并将测试称为服务器错误场景

在 nightwatch 中模拟双击

这个食谱对于那些在 nightwatch 中模拟双击而苦苦挣扎的人来说是一种享受。作为其中之一,我对此表示同情。事实证明,在 nightwatch 中有一个doubleClick函数,但至少在作者的意见中,它并不像预期的那样工作。

准备就绪

这个配方适用于刚开始使用 nightwatch 并且在这个特定问题上遇到困难的开发人员。你想学习如何模拟双击进行测试,但你不了解 nightwatch 吗?回到上一个配方。

我假设你已经设置好了 nightwatch,并且可以启动测试。我还假设你已经安装了前面配方中的所有命令。

工作原理...

假设你有一个 Vue 应用程序,它在一个index.html文件中:

<!DOCTYPE html>

<html>

<head>

  <title>7.6</title>

  <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

  <div id="app">

    <h2>Welcome to my test page</h2>

    <button id="showBtn" @dblclick="show = true">

      Show

    </button>

    <p v-show="show">Hello Nightwatch!</p>

  </div>

</body>

</html>

<div>元素之后,添加以下脚本:

<script>

  const vm = new Vue({

    el: '#app',

    data: {

      show: false

    }

  })

</script>

你可以使用http-server来提供你的应用程序。在浏览器中打开http://localhost:8080,然后尝试双击按钮以使文本出现。

现在,如果我们想要测试这个,我们查看 nightwatch 的 API,发现它有一个名为doubleClick()的函数调用。

然后我们可以编写一个类似于前面配方中的测试:

'Happy scenario' : function (client) {

  client

    .url('http://localhost:8080')

    .waitForElementVisible('#app', 1000)

    .assert.containsText('h2', 'Welcome to')

    .assert.hidden('p')

    .doubleClick('button') // not working

    .waitForElementVisible('p', 1000)

    .assert.containsText('p', 'Nightwatch')

    .end();

 }

除了这个不会按预期工作。正确的方法是:

'Happy scenario' : function (client) {

  client

    .url('http://localhost:8080')

    .waitForElementVisible('#app', 1000)

    .assert.containsText('h2', 'Welcome to')

    .assert.hidden('p')

    .moveToElement('tag name', 'button', 0, 0)

 .doubleClick()

    .waitForElementVisible('p', 1000)

    .assert.containsText('p', 'Nightwatch')

    .end();

 }

只有在你首先移动到你想要双击的元素上时,双击才起作用;只有这样,你才能调用doubleClick而不带任何参数。

工作原理...

moveToElement函数的参数如下:

  • selector:我们使用tag name作为选择器

  • tag / selector:我们寻找button标签;如果我们在这里使用了另一个选择器,我们会使用不同的格式

  • xoffset:这是虚拟鼠标在 x 坐标上的位置;对我们来说,0 是可以的,因为即使在按钮的边缘,点击也是有效的

  • yoffset:这与前面的参数类似,但在 y 轴上

在正确位置后,有一系列的命令可以释放事件。我们使用了doubleClick,但还有其他命令。

不同风格的单元测试

我们在之前的示例中发现并使用了 Jasmine。在这个示例中,我们将探索和比较不同的单元测试风格。这是特别相关的,因为 Vue 模板预装了 Mocha 和 Chai。Chai 使您能够以三种不同的风格编写测试。

准备工作

这个示例不需要任何特定的先前知识,但我强烈建议您完成“使用 Jasmine 进行 Vue 测试”的示例。

操作步骤

为了使这个示例工作,您需要两个依赖项:Mocha 和 Chai。您可以在 Google 上很快找到它们;只需记住,Mocha 有两个不同的文件:mocha.jsmocha.css。如果您希望显示得漂亮,您必须同时添加它们。

如果您正在使用 JSFiddle,请按照通常的方式继续;否则,请确保依赖项中也有 Vue。

我们的 HTML 布局将如下所示:

<div id="app">

  <p>{{greeting}}</p>

</div>

<div id="mocha">

</div>

mocha 部分是显示所有结果的地方。

在 JavaScript 部分,编写最简单的Vue应用程序并将其分配给一个变量:

const vm = new Vue({

  el: '#app',

  data: {

    greeting: 'Hello World!'

  }

})

我们将编写一个测试来查看Hello world文本是否真的被显示出来。

Vue应用程序完成后,写入以下内容:

mocha.setup('bdd')

chai.should()

describe('my app', () => {

  it('should say Hello World', () => {

    vm.$el.innerText.should.contain('Hello World')

  })

})

mocha.run()

上述代码准备了mochachai(通过安装describeitshould函数),然后断言我们组件的内部文本应该包含Hello World。相当易读,不是吗?

您可以启动应用程序,然后您将看到这个:

chai 还允许我们以另外两种方式编写完全相同的测试。第一种方式如下所示:

 vm.$el.innerText.should.contain('Hello World')

要使用第二种方式,您必须在之前添加const expect = chai.expect

expect(vm.$el.innerText).to.contain('Hello World')

最后,在之前添加const assert = chai.assert行:

assert.include(vm.$el.innerText,

  'Hello World',

  'Component innerText include Hello World')

在 assert 风格中,将消息作为附加参数添加是惯用的做法,以使测试在出现问题时更加详细。

它是如何工作的...

Chai 是一个简单的库,它实现了一些函数,并在某些条件不满足时抛出异常。另一方面,Mocha 运行某些代码片段,收集异常,并尝试以友好的方式向用户显示它们。

虽然使用哪种风格主要是品味问题,但这三种风格之间存在一些细微的差别。

  • Should更加雄辩和可读。不幸的是,它扩展了Object,将should函数添加到了所有对象上。如果你不知道如何对待最后一句话,你不应该介意,但正确的行为方式是奔跑并尖叫痛苦;永远不要扩展Object

  • Assert意味着对每个断言编写详细的描述,如果你为每个测试编写多个断言,这通常是很好的。就个人而言,我认为每个测试最多只应该有一个断言,并且应该集中在标题上进行描述。

  • Expect不扩展Object,非常可读且平衡良好,通常我更喜欢使用它而不是其他替代方案。

使用 Sinon.JS 进行外部 API 调用的存根化

通常,在进行端到端测试和集成测试时,您将运行并准备好后端服务器以响应您的请求。我认为有很多情况下这是不可取的。作为前端开发人员,您会抓住每一个机会责怪后端人员。

准备工作

完成这个示例不需要特殊的技能,但您应该将 Jasmine 安装为依赖项;这在使用 Jasmine 进行 Vue 测试示例中有详细说明。

如何操作...

首先,让我们安装一些依赖项。对于这个示例,我们将使用 Jasmine 来运行整个测试;您可以在使用 Jasmine 进行 Vue 测试示例中找到详细的说明(这四个文件分别是jasmine.cssjasmine.jsjasmine-html.jsboot.js,按照这个顺序)。

在继续之前,还要安装 Sinon.JS 和 Axios;您只需要添加与它们相关的js文件。

我们将构建一个在点击按钮时检索帖子的应用程序。在 HTML 部分中,编写以下内容:

<div id="app">

  <button @click="retrieve">Retrieve Post</button>

  <p v-if="post">{{post}}</p>

</div>

相反,JavaScript 部分将如下所示:

const vm = new Vue({

  el: '#app',

  data: {

    post: undefined

  },

  methods: {

  retrieve () {

    axios

      .get('https://jsonplaceholder.typicode.com/posts/1')

      .then(response => {

        console.log('setting post')

        this.post = response.data.body

      })

    }

  }

})

如果您现在启动应用程序,应该能够看到它正在工作:

现在我们想要测试应用程序,但我们不想连接到真实的服务器。这将需要额外的时间,并且不可靠;相反,我们将从服务器获取一个正确的样本响应并使用它。

Sinon.JS 有一个沙盒的概念。这意味着每当一个测试开始时,一些依赖项(如 Axios)都会被覆盖。每个测试结束后,我们可以丢弃沙盒,一切都恢复正常。

使用 Sinon.JS 的空测试如下所示(在Vue实例之后添加):

describe('my app', () => {

  let sandbox

  beforeEach(() => sandbox = sinon.sandbox.create())

  afterEach(() => sandbox.restore())

})

我们想要为 axios 的get函数存根调用:

describe('my app', () => {

  let sandbox

  beforeEach(() => sandbox = sinon.sandbox.create())

  afterEach(() => sandbox.restore())

  it('should save the returned post body', done => {

    const promise = new Promise(resolve => 

 resolve({ data: { body: 'Hello World' } })

 )

 sandbox.stub(axios, 'get').returns(promise)

 ...

 done()

 })

})

我们在这里覆盖了 axios。我们说现在get方法应该返回resolved的 promise:

describe('my app', () => {

  let sandbox

  beforeEach(() => sandbox = sinon.sandbox.create())

  afterEach(() => sandbox.restore())

 it

('

should save the returned post body'

,

 done

 =>

 {

    const promise = new Promise(resolve => 

      resolve({ data: { body: 'Hello World' } })

    )

    sandbox

.

stub

(

axios

,

 'get'

).

returns

(

promise

)

    vm

.

retrieve

()

    promise.then(() => {

      expect

(

vm

.

post

).

toEqual

(

'Hello World'

)

      done

()

    }) 

  }) 

})

由于我们返回了一个 promise(我们需要返回一个 promise,因为retrieve方法正在调用它的then方法),所以我们需要等待它解析。

我们可以启动页面并查看它是否工作:

如果您使用 JSFiddle,请记住将加载类型设置为 No wrap - in <body>,否则 Vue 将没有机会启动。

它是如何工作的...

在我们的案例中,我们使用沙盒来存根其中一个依赖项的方法。这样,axios 的get方法就不会被触发,我们会收到一个类似于后端将给我们的对象。

存根 API 响应将使您与后端及其怪癖隔离开来。如果出现问题,您不会在意,而且您可以在不依赖于后端正确运行的情况下运行测试。

有许多库和技术可以存根 API 调用,不仅与 HTTP 相关。希望这个示例为您提供了一个起步。

测量代码的覆盖率

代码覆盖率是评估软件质量最常用和易于理解的指标之一。如果一个测试执行了特定的代码部分,那么该代码被认为是被覆盖的。这意味着该特定代码部分正常工作且包含错误的可能性较小。

准备就绪

在测量代码覆盖率之前,请确保完成“将一些 Karma 添加到你的工作流程”这个步骤,因为我们将使用 Karma 来帮助我们。

如何操作...

创建一个新目录,并在其中放置一个名为package.json的文件。在其中写入以下内容:

{

 "name": "learning-code-coverage",

 "version": "1.0.0"

}

这将创建一个 npm 项目。在同一目录中,运行以下命令来安装我们的依赖项:

npm install vue karma karma jasmine karma-jasmine karma-coverage karma-chrome-launcher --save-dev

package.json文件会相应地更改。

karma-coverage插件使用底层软件 Istanbul 来测量和显示我们的测试覆盖率。

为了使下一步更容易一些,我们将全局安装 Karma(如果你还没有安装)。运行以下命令:

npm install -g karma

当安装了 Karma 后,在你的目录中运行以下命令;它将创建一个 Karma 配置文件:

karma init

除非它要求你加载文件,否则回答所有问题的默认值;在这种情况下,写下以下两行:

  • node_modules/vue/dist/vue.js

  • *.js

在此之后留一个空行以确认。

这将加载 Vue 和以js扩展名结尾的所有文件到目录的根目录中。

打开 Karma 创建的文件;它应该被称为karma.conf.js,并且应该与其他文件一起在你的目录中。

应该有一个类似以下的部分:

preprocessors: {

},

在 preprocessors 对象中,插入 coverage,如下所示:

preprocessors: {

  'myApp.js': ['coverage']

},

这意味着我们想要使用 coverage 预处理器对myApp.js文件进行预处理。myApp.js文件将包含我们要测试的应用程序。

紧接着,在reporters数组中添加 coverage:

reporters: ['progress', 'coverage'

],

这将使 coverage 报告生成一个包含覆盖率测量的网页。

为了使设置正常工作,您需要在frameworksfiles之间设置另一个属性,称为plugins

plugins: [

 'karma-jasmine',

 'karma-coverage',

 'karma-chrome-launcher'

],

接下来,我们将编写一个简单的 Vue 应用程序进行测试。

创建一个名为myApp.js的文件;我们将创建一个猜数字游戏。

在文件中写入以下内容:

const myApp = {

  template: `

    <div>

      <p>

        I am thinking of a number between 1 and 20.

      </p>

      <input v-model="guess">

      <p v-if="guess">{{output}}</p>

    </div>

  `

}

用户将输入一个数字,输出将显示一个提示或者一个文本来庆祝胜利,如果数字正确。将以下状态添加到myApp对象中:

data: {

  number: getRandomInt(1, 20),

  guess: undefined

}

在文件的顶部,您可以添加一个名为getRandomInt的函数,如下所示:

function getRandomInt(min, max) {

  return Math.floor(Math.random() * (max - min)) + min;

}

我们还需要一个计算属性来显示提示:

computed: {

  output () {

    if (this.guess < this.number) {

      return 'Higher...'

    }

    if (this.guess > this.number) {

      return 'Lower...'

    }

    return 'That's right!'

  }

}

我们的应用程序已经完成。让我们测试一下它是否按预期工作。

在目录的根目录下创建一个名为test.js的文件,并编写以下测试:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue(myApp).$mount()

    vm.number = 5

  })

  it('should output That's right! if guess is 5', () => {

    vm.guess = 5

    expect(vm.output).toBe('That's right!')

  })

})

要运行测试,请使用以下命令:

karma start

如果前面的命令在已经安装了karma-coverage插件的情况下没有要求安装该插件,您可以全局安装插件,或者使用本地安装的 Karma 从./node-modules/karma/bin/karma start运行测试。

如果您的浏览器打开了,请返回控制台,当测试完成时,按下Ctrl + C停止 Karma。

如果一切顺利,您应该会看到一个名为 coverage 的新文件夹,其中包含一个名为 Chrome 的目录。您还应该在其中找到一个名为index.html的文件。打开它,您会看到一个类似于这样的页面:

从一开始,我们就可以看到黄色表示出现了问题。我们测试了 100%的函数,但只测试了 50%的 if 分支。

如果您浏览并打开myApp.js文件的详细信息,您会发现我们没有测试if语句的两个分支:

这些分支内部可能会出现错误,我们甚至可能不知道!

尝试在测试文件中添加以下两个测试:

it('should output Lower... if guess is 6', () => {

  vm.guess = 6

  expect(vm.output).toBe('Lower...')

})

it('should output Higher... if guess is 4', () => {

  vm.guess = 4

  expect(vm.output).toBe('Higher...')

})

现在,如果您运行测试并打开报告,它看起来会更加绿色:

工作原理如下...

我们甚至没有打开应用程序,但我们已经非常确定它能正常工作,这要归功于我们的测试。

此外,我们有一份报告显示我们覆盖了 100%的代码。尽管我们只测试了猜数字游戏的三个数字,但我们覆盖了所有可能的分支。

我们永远无法确定我们的软件没有错误,但这些工具对我们开发人员在添加功能到我们的软件时非常有帮助,而不必担心可能会出现问题。