本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26
服务端路由:从服务器请求并渲染页面,匹配路径(/
)来响应相应的路由。弊端是浏览器每次加载新文档(HTML
)时,当前应用程序的状态值就会丢失,需要使用 cookie、本地存储以及URL里的查询参数来保存页面间的状态值,使开发变得复杂。
客户端路由:页面内容直接在客户端渲染,无须向服务端发新请求。当页面点击链接跳转页面的时候,客户端路由将阻止浏览器向服务器发送请求,作为替代,客户端路由会更改URL而不会导致页面的重新加载,并且页面将会使用对应的内容进行渲染,这里的关键点就是页面内容更新,但页面仍保留了与之前相同的 state。
VueRouter 是 Vue 客户端路由库,VueRouter 将 URL 的路径与其应渲染的组件配对,你可以使用一个路由数组来配置 VueRouter 要匹配的路径,一个路由配置对象有多个属性,其中最重要的是 path 和 component 属性,path 属性用来与 URL 的当前路径进行匹配,而 component 属性就是路径匹配后应该渲染的组件。
默认情况下, VueRouter 每次只会匹配一个路由,你需要渲染一个 RouterView 组件 (VueRouter 的内置组件)来渲染当前路由匹配到的组件,你可以将其理解为占位符,如果当前的路由是 /about
并且匹配到了对应的 About
组件,那么 RouterView 组件将渲染为 About
组件。
<!-- App.vue -->
<template>
<router-view />
</template>
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import About from './components/About.vue';
import router from './router';
Vue.use(VueRouter);
const routes = [
{
path: '/',
component: App
},
{
path: '/about',
component: About
}
]
new Vue({
router,
el: '#app', // 对应 index.html模板
render: h => h(App)
})
VueRouter 可以借助 RouterLink 在页面进行跳转,RouterLink 默认使用 <a>
元素进行渲染,当点击该元素的时候, VueRouter会阻止浏览器重新加载页面,并更新 URL,重新渲染 RouterView。
<router-link to="/about" /> 关于我们 <router-link>
动态路由匹配通常会使用路由的一部分来生成页面的内容,比如路径 /product/1
,这里路径的第一段就是产品的ID,在渲染的时候就可以根据对应的ID生成对应的内容。
你可以在路径中定义动态路径参数,并使用 :
进行标示。
const routes = [
{
path: '/product/:id',
component: ProductShow,
},
{
path: '/:type(top|new)', // 匹配 /top 或 /new,可以使用 $router.params.type 访问到
component: news,
},
{
path: '/:type(top|new)/:page',
// 可以匹配 /top/1 或 /new/20
// 使用 $router.params.type 访问到 top 还是 new
// 使用 $router.params.page 匹配到 1 或 20
component: moreNews,
},
{
path: '/',
redirect: '/top' // 根路径直接重定向到/top,使用 redirect 则不需要使用component属性
}
]
在实例中可以获取匹配到的动态参数路径 $route.params.id
,如果想要达到精准的匹配效果,你可以在动态路径中使用正则表达式。
VueRouter 的配置对象包含两个属性: mode 和 routes,mode 用来设置 VueRouter 以哪种方式控制 URL,history 模式会告诉 VueRouter 使用底层的 window.history.pushState
方法来设置 URL,而不触发页面的重新加载。 routes 是一个路由数组,它定义了哪些路由应该被匹配到。
当 VueRouter 被安装到 Vue 上时,它会添加两个实例属性:$route
和 $router
。 $route
属性包含了有关当前匹配路由的所有信息,其中包括路由参数中的任何动态字段。$router
是你在入口文件传递给 Vue 的路由实例,包含了可以控制 VueRouter 的方法,比如更改路由完成跳转。
1、测试 $route 属性
test('route', () => {
const wrapper = shallowMount(TestComponent, {
mocks: {
$route: {
params: {
id: 123
}
}
}
})
expect(wrapper.text()).toContain('123');
// 断言 TestComponent 能够根据 $route 的属性,正确渲染传入的id:123
})
更复杂一点的例子:
test('', () => {
expert.assertions(1);
const store = createStore(); // 参考上一章
store.dispatch = jest.fn(() => Promise.resolve());
const type = 'top';
const mocks = {
$route: {
params: { type }
}
}
createWrapper({ store, mocks }); // 参考上一章
await flushPromises();
expect(store.dispath).toHaveBeenCalledWith('fetchListData', { type });
})
// 这就要求组件实现是
this.$store.dispatch('fetchListData' {
type: this.$route.params.type
})
总结,如果你正在测试的组件需要访问 $route 和 $router 上的属性和方法,而不需要在测试中使它们的值受控,那么可以使用 localVue 安装 VueRouter,因为使用 Vue 实例安装 VueRouter 的时候会把 $route 和 $router 的值设置为只读,因此你就无法在测试中使它们的值受控。
如果你要控制 $route 和 $router 对象中包含的数据,你需要使用 mocks 挂载选项,mocks 挂载选项使得属性在每个挂载组件内都可用。
2、测试 $router 属性
上面的例子,所有传递给路由的参数都是我们模拟的有效值,但是因为用户可以通过更改 URL 来控制页面参数,就可能访问到了未存在的路由,你应该添加一些代码来处理无效值并重定向到有效页。也可能传递不符合规范的参数,比如page/id之类的传值一定要准确,所以要掌握如何测试 $router 属性。
由于 $router 是一个实例属性,因此你可以使用 Vue Test Utils 的 mocks 挂载项在测试中控制 $router 的值。
test('当页码参数超出了最大值重定向到第一页', () => {
expert.assertions(1);
const store = createStore({
getters: {
maxPage: () => 5
}
})
const mocks = {
$route: {
params: { page: 1000 }
},
$router: {
replace: jest.fn(),
}
}
createWrapper({ mocks, store });
await flushPromises();
expect(mocks.$router.replace).toHaveBeenCalledWith('/product/1'); // 断言会被重定向到第一页
})
// 所以代码应有这样一段逻辑
if (this.$route.params.page > this.$router.getters.maxPage) {
this.$router.replace('/product/1');
}
与 Vuex 一样,VueRouter 作为插件,应该避免直接在 Vue 基础构造函数上安装,应该使用 localVue 构造函数。
要测试链接到其他页的 RouterLink 组件,你需要断言 RouterLink 组件接收到了正确的 to prop,这样它才能在点击后正常的跳转,问题是,VueRouter 并没有到处 RouterLink 和 RouterView 组件,因此无法把 RounterLink 组件当做选择器 wrapper.findComponent(Component)
这样调用。
解决方法是控制渲染成 RouterLink 的组件,把这个受控组件用作选择器,你可以使用 Vue Test Utils 控制已渲染组件,当 Vue 父组件渲染子组件时, Vue 会试图在父组件实例上对子组件进行解析,你可以使用 Vue Test Utils 的 stubs 选项覆盖这个过程。
const wrapper = shallowMount(TestComponent, {
stubs: {
RouterLink: 'div',
}
})
// 这里的意思 设置所有的 RouterLink 组件以 <div> 元素渲染
// RouterLink 属性名还可以写为 router-link \ routerLink
Vue Test Utils 可以输出一个表现就像 RouterLink 的 RouterLinkStub 组件,你可以存根所有的 RouterLink 组件,把它们解析为 RouterLinkStub 组件,使用 RouterLinkStub 作为选择器。
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
test('renders RouterLink', () => {
const store = createStore({
getters: {
maxPage: () => 3,
}
})
const mocks = {
$route: {
params: { page: '1' }
}
}
const wrapper = createWrapper({
store,
mocks,
stubs: {
RouterLink: RouterLinkStub,
}
})
expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/product/1');
expect(wrapper.findComponent(RouterLinkStub).text()).toBe('下一页');
// 最大页数是3页,当前在第1页,所以可以正常渲染出下一页的 RouterLink
})
// 测试 router-link 是否渲染
test('测试当没有正确传参的时候,RouterLink不会被渲染,只渲染一个a标签', () => {
const wrapper = createWrapper(); // 没有传参 $route.params.page
expect(wrapper.find('a').attributes().href).toBe(undefined);
expect(wrapper.find('a').text()).toBe('上一页');
})
对应的 RouterLink 的 template 大概如下:
<template>
<div>
<router-link
v-if="$route.params.page > 1"
:to="'/' + $route.params.type + '/' + ($route.params.page - 1) "
>
上一页
</router-link>
<!-- 如果上一页不存在 渲染一个a标签 没有herf 即不可点击跳转 -->
<a v-else> 上一页 </a>
<span> {{ $route.params.page || 1 }} / {{ $store.getters.maxPage }} </span>
<router-link
v-if="($route.params.page || 1) < $store.getters.maxPage "
:to="'/' + $route.params.type + '/' + ($route.params.page + 1) "
>
下一页
</router-link>
<a v-else> 下一页 </a>
</div>
</template>
当你安装 VueRouter 时,VueRouter 组件会注册为全局组件,只有在 Vue 实例被实例化之前,VueRouter 被安装到 Vue 构造函数上时(调用了Vue.use(VueRouter)
),组件才能渲染它们。
如果你挂载了一个包含了 RouterLink 或 RouterView 的自定义组件,而没有对它们(RouterLink/RouterView)进行存根或者没有在 localVue 上安装 VueRouter,测试输出中会报告出警告信息,如下示例:
[Vue warn]: Unknown custom element: <router-link> - did you register the component correctly? For recursive components, make sure to provide the “name” option.
推荐在之前一直提到的 createWrapper
创建包装器的工厂函数里使用 stubs 挂载选项来存根这些组件,而不是在 localVue 构造函数中安装 VueRouter,以便在需要时对 VueRouter 属性进行覆盖。
在 Vuex store中使用 VueRouter 的属性可能会很使用,你可以使用 vuex-router-sync 库同步 Vuex 和 VueRouter, 使 route 对象在 store 也可以被获取到。
import { sync } from 'vuex-router-sync'; // npm i -S vuex-router-sync
sync(store, route);
现在 store 中会包含一个 route 对象, 该对象与组件实例中的 $route 值相同。例如,可以使用 $store.route.params.page
访问对应的 route 传参。
所以在测试中可以这样模拟使用:
test('vuex router sync', () => {
const items = Array(40).fill().map((v, i) => i);
const store = {
items,
route: {
params: {
page: '2'
}
}
}
const relust = getters.displayItems(store); // getters 接收 store 作为参数
const expectdResult = items.slice(20, 40);
expect(result).toEqual(expectdResult);
})
// 应该有对应的 getters 如下
const store = {
// ...
getters: {
displayItems() {
const page = Number(state.route.params.page) || 1;
const start = (page - 1) * 20;
const end = page * 20;
return state.items.slice(start, end);
}
}
}
// 需要注意的是,需要在 createStore 的默认选项中这样设置
// 因为关于 displayItem getters 的测试中没有 router对象
// 导致访问 state.route 为 undefined 从而出错的的情况
const state = {
// ...
route: {
params: {}
}
}