介绍
Vue Router是Vue.js官方的路由管理器。它和Vue.js的核心功能深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由 / 视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于Vue.js过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5历史模式或hash模式,在IE9中自动降级
- 自定义的滚动条行为
安装
通过执行npm i vue-router -s安装,推荐使用vue add router安装插件(记得提前提交)。
使用Vue-CLI3创建Vue Router项目
在终端输入vue create 项目名
> Manually select features
选择手动选择功能
(*) Babel
>(*) Router
去掉( ) Linter / Formatter,勾选Babel和Router
Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n)
输入y,回车,同意使用历史模式
> In dedicated config files
保存在专用配置文件中
认识Vue Router项目目录
router文件夹下的index.js,在这个文件中需要有5步:
- 导入vue-router
import VueRouter from 'vue-router' - 使用vuerouter
Vue.use(VueRouter) - 创建路由信息配置对象
- 创建路由对象
- 将路由对象抛出
import Vue from 'vue'
//1.导入
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
//2.模块化机制 使用Router
Vue.use(VueRouter)
//3.创建路由信息配置对象
const routes = [{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
//异步组件加载
component: () => import( /* webpackChunkName: "about" */ '../views/About.vue')
}
]
//4.创建路由对象
const router = new VueRouter({
mode: 'history', //history模式可以给我们呈现一个干净的网页地址,前面不会出现`/#`,默认h5模式
base: process.env.BASE_URL,
routes
})
//5.将路由对象抛出
export default router
//6.在main.js中导入
同时我们也可以看到,上面用了两种不一样的组件加载方式,component:()=>import('')属于组件加载的异步加载方式。然后下面还有一个mode: 'history'历史模式,它可以给我们呈现一个干净的网页地址,前面不会出现/#,如果不写的话默认时h5模式。
main.js,需要有2步:
- 导入router
import router from './router' - 将router挂载到vue实例中
import Vue from 'vue'
import App from './App.vue'
//导入
import router from './router'
Vue.config.productionTip = false
new Vue({
//将路由挂载到Vue的实例中
router,
render: h => h(App)
}).$mount('#app')
views文件夹:路由组件全部存放在当前目录下,这些组件会被渲染到App.vue的<router-view />中。
App.vue
<div id="nav">
<!-- router-link默认被渲染成a标签 to属性会被默认渲染成href属性 -->
<router-link to="/">Home</router-link>|
<router-link to="/about">About</router-link>
</div>
<!-- router-view相当于路由组件的出口 -->
<router-view />
其中的router-link标签会被浏览器默认渲染成a标签,to属性就相当于a标签的href属性。router-view就相当于路由组件的出口,所有的路由组件都通过它来渲染。
命名路由
在创建路由信息配置对象的时候,在对象中添加一个name属性,当有了name属性后就可以动态的对路由进行切换,之前在<router-link to="/">Home</router-link>这种通过路径来切换是静态的。当有name属性后我们可以这样<router-link :to="{name:'Home'}">Home</router-link>,给to属性绑定name。
const routes = [{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import( /* webpackChunkName: "about" */ '../views/About.vue')
}
]
动态路由匹配
我们在网页上经常会在地址栏上输入地址,比如http://localhost:8080/user/1,不同的用户对应不同的id,不管是用户1页面还是用户2页面,使用的都是同一个页面,只不过根据他们的id获取各种不同的信息。
首先在views目录下创建一个User组件,然后在index.js中导入import User from '@/views/User.vue',配置路由信息:
{
// /:id就是动态路由匹配
path: '/user/:id',
name: 'User',
component: User
}
其中的/:id就是动态路由匹配,用来接收不同的用户的id,然后在App.vue中:
<router-link :to="{name:'User',params:{id:1}}">User1</router-link>|
<router-link :to="{name:'User',params:{id:2}}">User2</router-link>|
通过在to属性值中配置params:{id:1}传入参数,参数可以有很多,所以这边的params是一个对象。
同时我们可以在User.vue中通过$route.params.id获取id来进行区分。
<template>
<div>用户页面{{$route.params.id}}</div>
</template>
此时我们确实是可以在用户1页面和用户2页面之间进行切换,并且也可以看到各自对应不同的id。
但是我们在User.vue中添加created生命周期函数,并对id进行打印,此时我们可以发现id只会打印一次,也就是说我们只能够获取到第一次加载时的id,第二次进行切换的时候获取不到第二次的id,当前组件被复用了,因为两个路由渲染的是同一个组件,所以created函数不会再被调用,也就拿不到数据了。
//当路由参数变化时 /user/1 切换到 /user/2 ,当前组件实例会被复用,因为两个路由渲染的时同一个组件
created() {
console.log(this.$route.params.id);
},
监听路由对象的两种方式
为了解决上述问题,在路由组件复用的情况下,我们需要对路由对象进行一个监听,来响应路由参数的变化。
- 通过watch监听
watch: {
//to是当前路由信息对象,from是切换之前的
$route: (to, from) => {
console.log(to.params.id);
//发起 ajax,请求后端接口数据(数据驱动视图)
},
},
- 通过导航守卫beforeRouteUpdate监听
使用时需要注意,在最后一定要调用
next(),不然会阻塞路由,会卡在当前函数,页面切换不过来,next()就相当于放行。
beforeRouteUpdate(to, form, next) {
console.log(to.params.id);
//一定要调用next(),不然会阻塞路由,就会卡在当前函数,
next();
},
404路由和路由匹配的优先级
当用户在地址栏上输入的地址没有匹配到路由时,我们应该给用户显示一个404页面。我们需要在index.js中通过使用通配符*来配置一个404路由。
{
path: '*',
//异步加载组件
// @ 表示的就是当前项目目录
component: () => import('@/views/404')
}
因为404页面只有在路由不匹配或找不到的情况下才会加载,所以我们可以通过组件的异步加载方式来进行一个导入,异步加载对于提升性能有很大的帮助。
当我们使用通配符的时候配置了一个user-*之类的路由,此时可以通过$route.params.pathMatch在当前页面来获取通配符代表的内容。比如我们在地址栏输入user-admin,则能够通过上述代码获取到admin。
路由匹配的优先级
同一个路径可以匹配多个路由,匹配的优先级遵循路由定义的顺序,即谁先定义谁的优先级高。
如果我们配置两个路由的path和name都相同,但加载的组件不同,当我们在浏览器上访问时,只会加载最前面的那个配置的路由,因为路由会从上往下依次匹配。
注意:404路由一定要放在整个路由配置的最后面,因为路由会从上往下依次匹配。
路由查询参数
有时候用户可能会在网页输入类似于这样的一串地址http://localhost:8080/page?id=1&title=book,尤其是在查询的时候,那么我们该怎样来定义这个路由信息呢。面对这种情况,vue-router中给我们提供了一个查询参数query。
首先在views中创建一个Page.vue,然后在index.js中进行配置:
{
//后面的参数不需要在当前的路由进行配置,只需要在匹配的时候定义query就行
path: '/page',
name: 'page',
component: () => import('@/views/Page')
},
然后在App.vue中使用查询参数query来进行后面参数的一个配置,和之前的params的一个配置差不多,这边暂时写死,在项目中肯定是需要传参动态变化的。
<router-link :to="{name:'page',query:{id:1,title:'book'}}">Page</router-link>|
配置我们已经完成了,但我们可能还会需要获取到id和title。我们可以在Page页面中通过this.$route.query来获取这个query对象,然后来获取我们想要的值。
created() {
console.log(this.$route);
const { id, title } = this.$route.query;
console.log(id, title);
//与后端发生交互
},
路由重定向和别名
重定向
首先在index.js中配置:
{
path: '/',
redirect: '/home'
}
当我们访问http://localhost:8080/的时候会给我们重定向到home页面,同时还可以通过下面这种方式来配置:
{
path: '/',
redirect: {
name: 'home'
}
}
别名
我们可以在配置路由信息的时候通过alias给路由起一个别名,然后在地址栏通过http://localhost:8080/aaa也能够访问到page页面。
{
path: '/page',
name: 'page',
component: () => import('@/views/Page'),
alias: '/aaa' //给路由起别名
}
别名的功能:可以自由的将ui的结构映射到任意的url上。
路由组件传值
在组件中我们通过$route.params.id的方式来获取地址栏上的参数,如果我们的项目越来越大,地址栏上的参数可能越来越多,那么在当前组件中通过这种方式来获取值会显得很复杂,并且使得和当前的路由形成高度的耦合,使得当前组件只能在特定的url上使用,限制了灵活性。为了解决这个问题,我们可以在定义路由信息的时候给它加一个props属性。
{
path: '/user/:id',
name: 'User',
component: User,
props: true
},
然后在User.vue中通过props: ["id"],那么我们就可以在当前页面通过{{id}}对id进行直接获取。
这个props也可以是一个函数:
{
path: '/user/:id',
name: 'User',
component: User,
//props: true
props: (router) => ({
id: router.params.id,
title: router.query.title
})
}
然后我们通过http://localhost:8080/user/1?title=book进行一个访问,能够获取到1和book两个参数。props传值解决了$route耦合的问题。
编程式导航
我们之前使用的<router-link :to="{name:'home'}">Home</router-link>|这种属于声明式导航,我们也可以通过this.$router获取路由实例对象,然后再通过使用其中的方法来实现编程式导航。
这边注意区分一下this.$router和this.$route,this.$router获取的是VueRouter对象,而this.$route获取的是路由信息对象。
通过VueRouter对象中的push()可以实现编程式导航。
<button @click="goHome">跳转到首页</button>
goHome() {
this.$router.push("/");
},
通过这样的一种方法也可以跳转到首页。同时push中也可以是下面这几种写法 this.$router.push("name");、this.$router.push({path: "/",});、this.$router.push({name: "User",params: { id: 2 },});、this.$router.push({path: "/page",query: { id: 2, title: "tom" },});。
同时还有我们比较常用的前进、后退,可以通过VueRouter对象中的go()实现,其中0是刷新、正数是前进、负数是后退,数字的大小是步数,当历史记录小于填入的步数时会失败。
<button @click="goBack">后退</button>
goBack() {
this.$router.go(-1);
},
嵌套路由
当子组件结构相同,可以复用时,采用的是动态路由匹配,但是当子组件结构不同,不能复用时,我们就需要用到嵌套路由。
比如说user组件下有一个user/1/profile还有一个user/1/posts,并且它们的结构是不相同的,难道我们要给user写两套样式吗?
解决:我们可以写两个组件profile和posts,将它们渲染到user组件中,那么需要在user组件中提供<router-view />来作为它的两个子组件的出口,即router-view中嵌套router-view,这就是嵌套路由。
嵌套路由3步:
- 定制孩子路由信息
- 定义跳转(router-link)
- 定义路由出口(router-view)
首先我们在index.js中的user路由信息对象中配置一个children来定制它的孩子路由:
注意:在children配置孩子路由信息时,path中不要加/,因为它默认会有一个/。
{
//:id就是动态路由匹配
path: '/user/:id',
name: 'User',
component: User,
//props: true
props: (router) => ({
id: router.params.id,
title: router.query.title
}),
//配置嵌套路由
children: [{
//这边不要加'/',它默认会有一个'/'
path: 'profile',
component: () => import('@/views/Profile')
},
{
path: 'posts',
component: () => import('@/views/Posts')
}
]
},
然后在App.vue中给这个两个添加跳转
<router-link to="/user/1/profile">user/1/profile</router-link>|
<router-link to="/user/1/posts">user/1/posts</router-link>|
最后在User.vue中给它的两个孩子路由添加一个<router-view/>进行渲染即可。
<!-- 渲染子路由的组件出口 -->
<router-view></router-view>
命名视图
在网页上我们可能需要展示多个视图,比如在首页页面,我们既要展示主要内容,同时还要展示侧边栏内容这两个视图,这种情况不属于嵌套路由,因为此时这两个组件是属于同级的,需要在同一个页面上同时展示,这个时候可以用到命名视图。
首先对首页的路由信息重新进行一个配置,之前是通过 component 进行配置加载视图,现在我们需要通过components进行一个重新的配置,使用这种方式配置能够定义更多的视图,default就是当前路由默认的名字,然后在下面可以继续挂载想在当前页面显示的视图,key是给视图命名,值对应想导入的组件。
{
path: '/home',
name: 'home',
//component: Home
//通过这种方式能够定义更多的视图
components: {
default: Home, //默认的名字
main: () => import('@/views/Main'), //主要内容
sideBar: () => import('@/views/sideBar') //侧边栏内容
}
},
然后在views中创建导入的Main组件和sideBar组件,最后在App.vue中再添加两个router-view,并添加name属性,然后将components中需要展示的两个视图的命名填入即可,同时我们还可以给router-view添加类名,添加样式布局等。
<!-- 每个router-view都会被渲染成一个div -->
<!-- 默认的Home -->
<router-view />
<!-- Main -->
<router-view name="main" />
<!-- sideBar -->
<router-view name="sideBar" />
其实我们也可以就在默认的router-view中对主要内容和侧边栏内容进行布局,但是在后期的维护和优化上,命名视图的结构更好。
导航守卫
导航表示路由正在发生改变。vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
全局守卫
你可以在index.js中通过router.beforeEach注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
那么全局守卫到底能够帮我们干什么?
需求:用户在浏览网站时,会访问很多组件,当用户跳转到/notes,发现用户还没有登录,此时应该让用户登录之后才能查看,应该让用户跳转到登录页面,登录完成后就可以查看/notes页面的内容,这个时候全局守卫起到了关键的作用,因为全局守卫就是不管哪一个路由被访问时都会被调用。
首先在index.js中将两个路由信息进行配置
{
path: '/notes',
name: 'notes',
component: () => import('../views/Notes.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue')
},
然后在views中创建这个两个组件Notes.vue和Login.vue,再在App.vue中提供一个<router-link :to="{name:'notes'}">我的笔记</router-link>|。
接下来在index.js中注册一个全局守卫,并对代码逻辑进行编码,首先判断用户当前访问的是不是/notes,然后获取一下用户信息,如果获取到用户信息则直接next()放行,允许查看,否则跳转到登录页面,通过在next()中传参跳转路由的path实现,最外层的那个next()放行一定要写,因为不管哪个一个路由被访问全局守卫都会调用。
//to就是当前访问路由
router.beforeEach((to, from, next) => {
//console.log(to);
//用户访问了/notes
if (to.path === '/notes') {
//获取用户登录信息
const user = JSON.parse(localStorage.getItem('user'));
if (user) {
//用户已经登录,对/notes放行,允许查看我的笔记
next();
} else {
//用户没有登录 跳转到登录页面
next('/login');
}
}
next();//一定要写
})
接着对Login.vue进行一个编写,handleLogin()首先模拟与后端发生交互,然后将登录信息存储到本地(退出的时候需要进行一个清除),登录成功后通过一个编程式导航this.$router.push("notes");跳转回/notes。
<template>
<div>
<h2>登录页面</h2>
<p>
<label for>用户名:</label>
<input type="text" v-model="user" />
</p>
<p>
<label for>密码:</label>
<input type="password" v-model="pwd" />
</p>
<button @click="handleLogin">登录</button>
</div>
</template>
<script>
export default {
data() {
return {
user: "",
pwd: "",
};
},
methods: {
handleLogin() {
//1.获取用户名和密码
//2.与后端发生交互
setTimeout(() => {
let data = {
user: this.user,
};
//保存用户名到本地
if (data.user) {
localStorage.setItem("user", JSON.stringify(data));
this.$router.push("notes");
}
}, 1000);
},
},
};
</script>
<style scoped>
</style>
组件内容的守卫
beforeRouteEnterbeforeRouteUpdate(2.2 新增)beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter守卫不能够访问this,但是可以通过传一个回调给next来访问组件实例,用的较少。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteUpdate前面提过,一般用于组件复用的情况。
beforeRouteLeave这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。保证用户的一个体验。
<template>
<div>
<h2>编辑页面</h2>
<textarea v-model="content" cols="30" rows="10"></textarea>
<button @click="saveContent">保存</button>
<ul>
<li v-for="(item,index) in list" :key="index">{{item.title}}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
content: "",
list: [],
};
},
methods: {
saveContent() {
//保存到列表
this.list.push({ title: this.content });
//清空
this.content = "";
},
},
//监测用户是否离开当前页面
beforeRouteLeave(to, from, next) {
if (this.content) {
//如果输入框中有内容
alert("请保存数据之后再离开"); //保证用户体验
//表示默认失败,恢复当前页面
next(false);
} else {
//如果输入框中没有内容,则直接放行
next();
}
},
};
</script>
<style scoped>
</style>
meta路由元信息实现权限控制
前面的全局守卫中我们是通过给router注册一个beforeEach来实现对访问/notes的一个权限控制,在实际的一个项目中有很多的链接访问我们需要进行一个权限认证,难道我们要在beforeEach中做多个判断吗?或者说是用一个数组将所有不需要认证的添加到白名单,在白名单内的就直接放行,不在白名单内的则需通过认证。
为了解决上述问题,vue给我们提供了一个meta字段,一旦配置meta: {requiresAuth: true}就相当于给当前路由加到黑名单,需要经过认证才能够访问。
{
path: '/blog',
name: 'blog',
component: () => import('../views/Blog.vue'),
meta: {
requiresAuth: true
}
},
此时我们需要对之前的beforeEach进行一个修改
router.beforeEach((to, from, next) => {
//console.log(to);
//matched 中有我们想要的属性
//console.log(to.matched);
//some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。
if (to.matched.some(res => res.meta.requiresAuth)) {
//需要权限 黑名单
if (!localStorage.getItem('user')) {
//如果用户没有登录,跳转登录页面
next({
path: '/login',
//将跳转前的路径作为查询参数添加
query: {
//fullPath 获取完整的路径
redirect: to.fullPath
}
});
} else {
//如果用户登录了,直接放行
next();
}
}
//白名单,直接放行
next();
})
同时我们还需要不同的页面跳转登录成功后还能够跳回,那么Login.vue中也不能写死,需要进行修改,将next({ path: '/login', //将跳转前的路径作为查询参数添加 query: { //fullPath 获取完整的路径 redirect: to.fullPath } });中的query取出就能获取到跳转之前的路径,用于登录成功后跳回。
handleLogin() {
//1.获取用户名和密码
//2.与后端发生交互
setTimeout(() => {
let data = {
user: this.user,
};
//保存用户名到本地
if (data.user) {
localStorage.setItem("user", JSON.stringify(data));
//this.$router.push("notes");
//修改,不写死
this.$router.push({
//为了使不同的页面能够在登录成功后重新跳回,且不写死
//将beforeEach跳转时传递的查询参数取出
path: this.$route.query.redirect,
});
}
}, 1000);
},
数据获取
有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户数据。我们可以通过两种方式来实现:
- 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的提示。
- 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。
模拟导航完成之后获取数据
- 在当前项目目录下创建
vue.config.js文件,创建后台数据接口
module.exports = {
devServer: {
before: (app, serve) => {
app.get('/api/post', (req, res) => {
res.json({
title: 'vue-router的数据获取',
body: '1.在导航完成之后获取'
})
})
}
}
}
- 通过
npm i axios -s安装axios,在main.js中导入并绑定到vue的原型上
import axios from 'axios'
Vue.prototype.$http = axios;
- 创建组件配置路由
{
path: '/post',
name: 'post',
component: () => import('@/views/Post')
}
- 在App.vue提供router-link并绑定to
<router-link :to="{name:'post'}">Post</router-link>| - 开发Post,模拟导航完成后获取数据
效果图:
看不到的话可在Network中将网速调成3G,就可以很明显的看到:
<template>
<div class="post">
<div v-if="loading" class="loading">Loading......</div>
<div v-if="error" class="error">{{error}}</div>
<div v-if="post">
<h3>{{post.title}}</h3>
<h3>{{post.body}}</h3>
</div>
</div>
</template>
<script>
export default {
data() {
return {
post: null,
error: null,
loading: false,
};
},
//导航完成之后获取数据
created() {
this.getPostData();
},
//对路由进行一个监听,每次路由发生变化都需要重新获取数据
watch: {
$route: "getPostData",
},
methods: {
async getPostData() {
try {
//正在获取数据的时候展示Loading......
this.loading = true;
//获取数据
const res = await this.$http.get("/api/post");
//console.log(res.data);
this.post = res.data;
//获取完数据后
this.loading = false;
} catch (error) {
this.error = error.toString();
}
},
},
};
</script>
<style scoped>
</style>