# Vue-cli 中使用路由
# 1. 改造『老』Demo
在之前的 vue.js 中我们已经演示了路由的基本概念和使用,现在我们在 vue-cli 中将 vue.js 的路由 demo 改造成多模块形式。
创建独立的 LoginForm.vue 和 RegisterForm.vue 文件
<template> <div> <h2>登录页</h2> <label>用户名:<input type="text"></label><br/> <label>密 码:<input type="password"></label><br/> </div> </template> <script> export default { name: "LoginForm" } </script>
和
<template> <div> <h2>注册页</h2> <label>用 户 名:<input type="text"></label><br/> <label>密  码:<input type="password"></label><br/> <label>确认密码:<input type="password"></label><br/> </div> </template> <script> export default { name: "RegisterForm" } </script>
在 App.vue 中引用上述组件,并添加
router-link
和router-view
<div id="app"> <router-link to="/login">登录</router-link> <router-link to="/register">注册</router-link> <hr/> <div> <login-form></login-form> <register-form></register-form> </div> <router-view/> </div>
在 router 中添加路由信息
import Vue from 'vue' import VueRouter from 'vue-router' /* 引入 Vue compentents 组件页面。@ 表示相对于 src 的路径 */ import LoginForm from "@/components/LoginForm"; // import RegisterForm from "@/components/RegisterForm"; 第二种写法不需要导入 Vue.use(VueRouter) const routes = [ /* 请求重定向:/ => 映射到 /#/login */ { path: "/", redirect: '/login' }, /* 请求方式为 /#/login => 映射到 LoginForm 组件页面 */ { path: "/login", name: 'LoginForm', component: LoginForm }, /* 第二种加载页面组件的写法 */ { path: "/register", name: 'RegisterForm', component: () => import('@/components/RegisterForm') } ]
最后:在 main.js 引入我们的 router.js 文件。这部分代码 vue-cli 会自动生成。
# 2. 路由重定向
上例中已经演示了路由重定向的效果。
路由重定向指的是,当用户访问 A-URI
时,强制跳转至 B-URI
,此时,用户在 <router-view> 看到的自然就是 B URI 对应的组件。
说明
逻辑上,路由重定向就是让多个 URI 对应同一个组件。
路由重定向是通过 routers 中的 redirect 属性实现的。
routes: [
{ path: "/", redirect: '/login' },
{ path: "/login", name: 'LoginForm', component: LoginForm },
...
]
上述规则中,当访问 /
路径时,会重定向到 /login
,从而在页面的 <router-view> 中展现 LoginForm 组件。
逻辑上,无论时访问 /
,还是访问 /login
,看到的都是 LoginForm 组件。
# 3. 嵌套路由
在设计项目的 URI 时,我们通常会将相关功能的 URI 归类到同一个 URI 路径下,例如:
- /about/xxx
- /about/yyy
- /about/zzz
- /about/...
对于这样的情况,我们可以使用『嵌套路由』来实现这样的功能。
一级组件/首页(<router-view>)
│
│── 二级组件-1
│
└── 二级组件-2(<router-view>)
│
│── 三级组件-A
│
└── 三级组件-B
嵌套路由的页面方面的设计,就是将组件之间的关系组织成如上形式。二级组件和三级组件之间的关系,就是一级组件(或首页)和二级组件之间的关系的“重现”。
在这里,我们利用 vue-cli 自动生成的 Home
和 About
来演示。
我们可以将原用于 App.vue 中的 LoginForm 和 RegisterForm 整体『搬家』到 Home.vue 中:
<div class="home">
<router-link to="/login">登录</router-link>
<router-link to="/register">注册</router-link>
<hr/>
<div>
<login-form></login-form>
<register-form></register-form>
</div>
<router-view/>
</div>
然后将 App.vue 中替换为 Home 和 About:
<div id="app">
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
<hr/>
<router-view/>
</div>
这样,它们几个就组成了类似于如下的层次结构:
App(<router-view>)
│── About
└── Home(<router-view>)
│── LoginForm
└── RegisterForm
最后,我们去做路由配置。在这里,嵌套路由的设置,需要利用上一级路由设置的 children 属性:
const routes = [
// {path: "/", redirect: '/home'},
{
path: "/home", component: () => import('@/views/Home'), children: [
// 使用的是绝对路径
{path: "/login", name: 'LoginForm', component: LoginForm},
{path: "/register", name: 'RegisterForm', component: () => import('@/components/RegisterForm')}
]
},
{path: "/about", component: () => import('@/views/About')},
]
绝对路径和相对路径:
- 如果,你在子路由中使用的是绝对路径(以
/
开头),那么,你是访问/login
和/register
来控制两个子路由的显示; - 如果,你在子路由中使用的是相对路径,那么,子路由的访问路径是父路由路径 + 子路由路径,即,
/home/login
和/home/register
来控制两个子路由的显示。
无论是绝对路径和相对路径,父路由都不用以 /
结尾。
# 4. 动态路由匹配
有时(特别是在 RESTful 风格的项目中),我们会在代表查询的 GET 请求 URI 中嵌入 ID,以表示查询某个人/物的相关信息。例如:
- /user/1
- /user/2
- /user/3
- ...
但是问题是,如果系统中有 10086 个用户信息,那岂不是我们在路由配置中,要写 10086 个配置项?很显然,不可能是这样。对于这种情况,vue-router 提出了『动态路由匹配』。
# 基本使用
在路由配置中,我们可以采用下面的写法(来顶替 10086 个配置项):
var router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: '/user/:id', component: User },
...
]
})
说明
这里是不是和 Spring MVC 的 @PathVariable 注解的使用场景很像?
这样配置以后,无论 URI 是 /user/1
,还是 /user/2
,亦或者是 /user/10086
,都会是在 <router-view> 处显示 User 组件 。
而在 User 组件中,如果你需要用到『嵌』在 URI 中的那个 ID 值,你可以像下面这样取到它:
<div class="user">
<!--路由组件中通过 $route.params 获取路由参数 -->
User: {{ $route.params.id }}
</div>
当 URI 路径是
/user/1
时,User 组件中取到并显示就是 1 ;当 URI 路径是
/user/2
时,User 组件中取到并显示就是 2 ;当 URI 路径是
/user/10096
时,User 组件中取到并显示就是 10086 ;
这里的
id
和路由配置中的:id
是遥相呼应的。如果路由配置中是xxx
,那么这里自然也要使用xxx
。
# 动态路由的参数解耦
上一章节中,User 组件使用了 $route.params.id
来获取 URI 路径中『嵌』着的 id 。这种方式虽然可行,但是它让 User 组件和路由配置耦合在了一起。
当然,你也可能不在乎这个耦合的问题,那么这一章就没什么好说的了。
你可以利用 props 将组件和路由解耦。
# props 的简单使用
首先,你需要在路由配置中开启 props 功能:
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true }
]
})
当 props 被设置为 true,之前你所见到并使用的 route.params
将会被设置为组件属性。
然后,你需要在 User 组件中声明对应的 props :
export default {
name: "User",
props: ['id'] // 使用 props 接收路由参数
}
取值时,你也不需要再啰里吧嗦地写那么长了。
<div class="user">
User {{ id }}
</div>
# props 的值是对象
路由设置
const router = new VueRouter({ routes: [ // 如果 props 是一个对象,它会被按原样设置为组件属性 { path: '/user/:id', component: User, props: { uname: 'lisi', age: 12 }} ] })
取值
<template> <div class="user"> User: {{ uname + ', ' + age }} </div> </template> <script> export default { name: "User", props: ['uname', 'age'] } </script>
# props 的值是函数
const router = new VueRouter({
routes: [
// 如果 props 是一个函数,则这个函数接收 route 对象为自己的形参
{
path: '/user/:id',
component: User,
props: route => ({ uname: 'tom', age: 20, id: route.params.id })
}
]
})
<template>
<div class="user">
User: {{ uname + ', ' + age + ', ' + id}}
</div>
</template>
<script>
export default {
name: "User",
props: ['uname', 'age', 'id']
}
</script>
# 动态路由和 beforeRouteUpdated 守卫
当 /user/1234 与 /user/4567 相互切换时,其中相同的组件会被重用,于是 Vue 的生命周期钩子,例如 mounted ,是不会被调用的。那如何让两个页面显示不同的数据呢?
这里可以使用 beforeRouteUpdated 导航守卫,在 URL 动态部分发生变化时,运行一些代码(这些代码发起 ajax 请求去后台读取数据)。
具体实现思路在《路由守卫》章节专项讲解。
# 5. 为路由命名
为了更方便地表示路由的路径,可以给路由规则起一个别名,即为 命名路由 。
routes: [
{ path: '/user/:id', name: 'user', component: User },
...
]
这么干的好处在于,你可以在 <router-link> ,以及未来的 router.push() 中,用 name 来代替 path 。
<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>
router.push({ name: 'user', params: { id: 123 }})
# 6. 编程式导航
页面的导航方式有 2 种:
声明式导航:通过 点击链接 实现导航。
vue 中的 <router-link> 就会被渲染成 <a> 元素。
编程式导航:通过执行 JavaScript 代码,调用 BOM 的 API 实现导航。
本质上就是去改变 location 的 href 属性。
Vue 中编程式导航的常用的核心 API 有 2 个:
this.$router.push('hash地址')
this.$router.go(n)
<template>
<div class="user">
<p>
<button @click="goRegister">跳转到 About 页面</button>
</p>
</div>
</template>
<script>
export default {
name: "User",
methods: {
goRegister: function(){
// 用编程的方式控制路由跳转
this.$router.push('/about');
}
}
}
</script>
router.push()
方法的常见参数形式有:
// 字符串(路径名称)
router.push('/home')
// 对象
router.push({ path: '/home' })
// 命名的路由,变成 /user/123
router.push({ name: '/user', params: { userId: 123 }})
// 带查询参数,变成 /register?uname=lisi
router.push({ path: '/register', query: { uname: 'lisi' }})
# 7. 解决 Vue-router 报 NavigationDuplicated 的三种方法
有时候,你会发现控制台会报 [NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated"}]
错误信息。其原因在于 Vue-router 在 3.1 之后把 $router.push()
方法改为了 Promise 。所以假如没有回调函数,错误信息就会交给全局的路由错误处理,因此就会报上述的错误。
vue-router 是先报了一个 Uncaught (in promise) 的错误(因为 push 没加回调),然后再点击路由的时候才会触发 NavigationDuplicated 的错误(路由出现的错误,全局错误处理打印了出来)。
方案一:降低版本,固定 vue-router 版本到 3.0.7 以下。
npm install [email protected] -S yarn add [email protected] -S
方案二:禁止全局路由错误处理打印。这是最常见的解决方案。
在引入 vue-router 的文件中增加以下代码,对 Router 原型链上的 push、replace 方法进行重写,这样就不用每次调用方法都要加上 catch:
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const originalPush = VueRouter.prototype.push VueRouter.prototype.push = function push(location, onResolve, onReject) { if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject) return originalPush.call(this, location).catch(err => err) }
按照 vue-router 的要求,老老实实为每一个 push 方法的调用增加回调函数。
// 路由导航守卫
router.beforeEach(((to, from, next) => {
if (to.path === '/login') {
return next()
}
// 如果访问地址不为 /login 检验本地存储 token 值是否过期,不存在跳转到 login 页面,存在则放行
const tokenStr = window.sessionStorage.getItem('token')
if (!tokenStr) {
return next('/login')
} else {
next()
}
}));
路由守卫 →