介绍
Vue Router 是 Vue 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
起步
npm i vue-router -S 或者 Vue create project name 创建项目时添加Vue Router,我们需要做的是,将组件 (components) 映射到路由 (routes),然后告诉 Vue Router 在哪里渲染它们
npm i vue-router -Smain.js中
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter)推荐使用 Vue add router 添加插件,记得提前提交。
Vue-Cli 创建项目
Vue create project name
项目创建完成后,会在src文件夹下创建router文件夹,内有index.js
import Vue from 'vue'
//挂载路由器组件
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'// 前面是..
//模块化机制使用VueRouter
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
//创建路由器对象
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
//抛出路由器对象
export default router
router会被挂载到main.js实例中
import router from './router'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
标签使用
使用 <router-link to=''></router-link> 作为路由器的标签,router-link会被渲染为a标签 ( 所以css样式要在a标签内写 ),to属性会被渲染为herf属性,内部填写router文件夹内,路由器对象对应的path属性的值,点击对应路由器标签,router就会加载path下的组件,这个组件会在被渲染到当前组件的 <router-view></router-view> 内,这个标签相当于路由组件的出口
<div id="app">
<router-link to="/">首页</router-link>
<router-link to="/About">关于我们</router-link>
<router-view></router-view>
</div>
命名路由
给路由对象内添加name属性,为路由组件命名
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About
}
]
动态路由匹配
动态路由的作用是调用同一个组件来生成不同的内容,组件内获取到对应的id,向后端发送请求,后端根据不同的id返回对应的数据来展示不同内容
路由对象内的path属性的值后加 /:id 来配置动态路由匹配:
{
path: '/user/:id',
name: 'user',
component: User
}
通过 :to="{name:'user',params:{id:1}}" 绑定to属性为动态路由
<router-link :to="{name:'user',params:{id:1}}">用户1</router-link>
<router-link :to="{name:'user',params:{id:2}}">用户2</router-link>
动态路由组件复用
当路由参数发生变化时,动态路由匹配的数据直接被替换,原来的组件实例会被复用,两个路由渲染了同一个组件,复用显得更加高效
但是由于这个特性,created钩子函数会失效,只会被调用一次
created() {
console.log(this.$route.params.id);
},
考虑到路由组件会复用的情况,响应路由参数的变化,可以通过:
- watch属性来监听$route对象,这会接收两个值,为to和from
watch: {
$route(to, from) {
console.log(to.params.id);
//发起ajax 请求后端接口数据 数据驱动视图
}
}
- vue提供了一个路由导航守卫的钩子函数
beforeRouteUpdate(to, from, next),next相当于一个中间件,一定要调用
beforeRouteUpdate(to, from, next) {
//通过to.params.id来获取动态路由的id
console.log(to.params.id);
//一定要调用next,不然会堵塞整个路由
next();
}
404路由 & 异步组件加载
当匹配不到路由的时候,会在当前页面显示此组件,异步组件加载要比全局加载性能上要好很多,它只有在需要进入当前路由的时候,才会去加载这个组件
{
path: '*',
component: () => import('@/views/404')
}
*表示通配符,当上方的路由都找不到时,会显示当前路由内容404路由一定要放在路由最下面,因为 路由优先级是从上向下匹配的,同名路由会匹配上方的那个
通配符路由
路由地址使用通配符,当路由地址匹配到通配符前的地址后,会调用这个路由的组件,并向 $route.params 中会添加 pathMatch 并传入通配符的这个参数,可以根据这个参数通过 路由导航守卫 来执行相应操作
{
path: '/user-*',
component: () => import('@/views/User-admin')
}
<router-link :to="/user-xxx">管理员</router-link>
<template>
<div>
<h3>User-admin页面</h3>
<h4>{{ $route.params.pathMatch }}</h4>
</div>
</template>
组件显示:
User-admin页面
1234
路由查询参数
查询路由是直接找到对应路由,不需要在当前配置
{
path: '/page',
name: 'page',
component: () => import('@/views/Page')
}
而是在匹配这个路由的时候定义query
<router-link :to="{name:'page',query:{id:1,title:'foo'}}">Page</router-link>
在组件内获取query的值并发送请求与后端发生交互
created() {
//获取query的值
const { id, title } = this.$route.query;
console.log(id, title);
//与后端发生交互
}
http://localhost:8080/page?id=1&title=foo 路由参数 params 是 / 路由查询 query 是 ?
路由重定向
很多情况访问首页的时候,输入http://localhost:8080/home ,因为首页路由的地址是 / ,是访问不到首页的,这时候可以路由重定向,让地址栏输入这个地址的时候跳转到已有的路由地址
{
path: '/', //地址栏输入的地址
redirect: '/home' //跳转后的路由地址
redirect: {name:'home'} //也可以通过命名路由来跳转
},
{
path: '/home',
name: 'home',
component: Home
},
路由别名
当地址栏的url过长,可以通过alias属性对当前地址进行映射,作用就是让UI的结构,映射到任意的url上,用的比较少
{
path: '/page',
name: 'page',
component: () => import('@/views/Page'),
alias: '/aaa'
}
当访问 http://localhost:8080/aaa 时,路由会匹配到 /page 路由进行加载,且地址仍然是 /aaa,
路由组件传参
在组件中使用 $router.params 或 $router.query 的方式来获取对应地址栏上的参数,项目一旦越来越大,地址栏上的参数会越来越多,这个方法会显得非常复杂,会让路由形成高度的耦合,导致当前组件只能在特定的url中使用,限制了当前的灵活性
为了解决这个问题,可以在定义路由的时候,加一个 props: true 属性,这相当于父子组件传值
{
path: '/user/:id',
name: 'user',
component: User,
props: true
}
在路由组件内通过props接收,注意传入属性需要引号包裹,router-link传过来的值就可以直接使用,不用通过 $route.params 或 $router.query 获取了
props: ['id']
路由的props函数
还可以在当前路由的 props 定义函数
{
path: '/user/:id',
name: 'user',
component: User,
props: (route) => ({
id: route.params.id,
title: route.query.title
})
}
props: ['id', 'title']
编程式导航
<router-link :to="{name:'page',query:{id:1,title:'foo'}}">Page</router-link>以上这种方法叫 声明式导航
当把 VueRouter 挂载到整个 Vue 实例上之后,在任意组件都可以通过 this.$route 和 this.$router 来获取 当前路由器配置对象 和 路由器实例对象
console.log(this.$route)
//--------------------------------------------
{name: "user", meta: {…}, path: "/user/1", hash: "", query: {…}, …}
fullPath: "/user/1"
hash: ""
matched: [{…}]
meta: {}
name: "user"
params: {id: 1}
path: "/user/1"
query: {}
__proto__: Object
console.log(this.$router)
//-------------------------------------------
VueRouter {app: Vue, apps: Array(1), options: {…}, beforeHooks: Array(0), resolveHooks: Array(0), …}
afterHooks: []
app: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
apps: [Vue]
beforeHooks: []
fallback: false
history: HTML5History {router: VueRouter, base: "", current: {…}, pending: null, ready: true, …}
matcher: {match: ƒ, addRoutes: ƒ}
mode: "history"
options: {mode: "history", base: "/", routes: Array(7)}
resolveHooks: []
currentRoute: (...)
__proto__: Object
获取到 this.$router 之后,所在的 proto 原型内,有 push() / go() / back() 三个方法,可以对路由进行动态的跳转
这种方法实现的导航称为 编程式导航
可以通过绑定事件触发方法
<button @click="goHome">跳转到首页</button>
获取 $router,通过 push() 方法,跳转到对应路由地址
methods: {
goHome() {
this.$router.push("/"); //跳转到首页
this.$router.push({ //path 或 name 填写任意一个,就可以跳转到对应路由
path: '/',
name: "home"
});
this.$router.push("name"); //给当前组件 $route.params.id 传值
this.$router.push({ //跳转到对应路由并给组件 params 传值
name: "user",
params: { id: 2 }
});
}
},
还可以通过 go() 和 back() 来实现跳转和后退
<button @click="goBack">后退</button>
methods: {
goBack() {
this.$router.back();
}
}
以上方法可以简写为
<button @click="$router.back()">后退</button>
嵌套路由
嵌套路由是通过路由内声明一个 children 数组,在内部添加路由,注意path值前面不能加 /
{
path: '/user/:id',
name: 'user',
component: User,
redirect: '/user/:id/posts', //需要设置加载路由组件后显示嵌套路由的默认子路由,需要在路由内对父级的路由重定向
children: [
{
path: 'posts',
name: 'posts',
component: () => import('@/views/Posts')
},
{
path: 'profile',
name: 'profile',
component: () => import('@/views/Profile')
}
]
}
组件内直接写路由连接和路由出口即可
<router-link :to="{name:'posts'}">posts</router-link>
<router-link :to="{name:'profile'}">profile</router-link>
<hr/>
<router-view></router-view>
命名视图
当需要在同一级路由展示多个同级组件,切换到其他路由又不需要显示这些组件时,直接在父组件引入这些子组件是无法实现的,这时就需要使用命名视图,让当前路由内可以展示路由组件和其他同级组件
{
path: '/home',
name: 'home',
components: {
default: Home, //默认组件
main: () => import('@/views/Main'), //需要显示的同级组件,命名:引入组件的方式放入当前路由
sideBar: () => import('@/views/SideBar') //其他路由也可以通过这个方式将这个组件在其他路由页面内显示
}
}
在父组件中通过命名引入的组件来插入当前视图,这些插入的组件,在其他没有使用命名视图引入组件的路由内,是不会显示的,同理如果在其他组件内只引入了其中一个组件,会显示引入的那个,在父组件中引入的视图,是可以被其他路由共享的
<router-view></router-view>
<router-view name="main"></router-view>
<router-view name="sideBar"></router-view>
这个问题其实也可以通过在子组件内布局整个视图来解决,不过命名视图的方法在整个项目结构上更加舒服,需要多个路由共享的组件,也不需要在组件内引入就可以很方便的显示
导航守卫
导航表示路由正在发生改变
完整的导航解析流程
- 导航被触发
- 在失活的组件里调用离开守卫
- 调用全局的
beforeEach守卫 - 在复用的组件里调用
beforeRouteUpdate守卫 - 在路由配置里调用
beforeEnter - 解析异步路由组件
- 在被激活的组件里调用
beforeRouteEnter - 调用全局的
beforeResolve守卫 - 导航被确认
- 调用全局
afterEach钩子 - 触发 DOM 更新
- 用创建好的实例调用
beforeRouteEnter守卫中传给next的回调函数
全局守卫
可以使用 router.beforeEach() 注册一个全局前置守卫
const router = new VueRouter({...})
router.beforeEach((to,from,next)=>{
// ...
})
全局守卫的登陆操作
用户在浏览网站时,访问 /notes,发现还没有登录,应该让用户跳转到登录页面,登录完成后才可以查看 /notes 页面的内容,这个时候就需要全局守卫来完成了
- 配置路由
{
path: '/notes',
name: 'notes',
component: () => import('@/views/Notes')
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login')
}
- 创建 Notes.vue 和 Login.vue 组件
<template>
<div>
<h3>登录页面</h3>
<input type="text" v-model="user" />
<br />
<input type="password" v-model="pwd" />
<br />
<button @click="Handlelogin">登录</button>
</div>
</template>
<script>
export default {
data() {
return {
user: "",
pwd: ""
};
},
methods: {
Handlelogin() {
//获取用户名和密码
//与后端发生交互
setTimeout(() => {//模拟获取数据
let data = {
user: this.user
};
//保存用户名到本地
localStorage.setItem("user", JSON.stringify(data));
//跳转到笔记页
this.$router.push({
name: "notes"
});
}, 2000);
}
}
};
</script>
- 添加 router-link
<template>
<div id="app">
<router-link :to="{name:'notes'}">我的笔记</router-link>
<button @click="HandelLogout">退出登录</button>
</div>
</template>
<script>
export default {
methods: {
HandelLogout() {
localStorage.removeItem("user");
this.$router.push({
name: "home"
});
}
}
};
</script>
- router/index.js 内创建全局守卫
router.beforeEach((to, from, next) => {
//用户访问了notes
if (to.path === '/notes') {
//获取本地数据
const user = JSON.parse(localStorage.getItem('user'));
if (user) {
//用户已登录
next()
} else {
//跳转到登录页面
next('/login')
}
}
next()
})
组件内的守卫
可以在路由组件内直接定义以下路由导航守卫:
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
beforeRouteEnter(to,from,next){
//在渲染该路由组件之前被调用,不能获取组件实例 'this',因为当前守卫执行时,组件实例还没有被创建,很少应用
}
beforeRouteUpdate(to,from,next){
//在当前路由组件改变,此组件被复用时调用
//例如,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转时,由于会调用同样的 Foo 组件,
//Foo组件并没有被重复渲染,created钩子函数失效,而这个钩子函数则会在组件被复用时调用,且可以访问实例'this'
}
beforeRouteLeave(to,from,next){
//导航离开该组件的对应路由时被调用,可以访问'this'
}
beforeRouteLeave示例:阻止没有提交表单时跳转到其他页面
<template>
<div>
<h2>用户编辑页面</h2>
<input v-model="content" />
</div>
</template>
<script>
export default {
data() {
return {
content: ""
};
},
beforeRouteLeave(to, from, next) {
if (this.content) { //如果content有值则弹出提示信息,并用next(false)阻止路由跳转
alert("请确保提交表单后再离开");
next(false);
} else { //反之让路由通过
next();
}
}
};
</script>
路由meta元信息实现权限控制
vue提供了在路由内添加meta元信息的方式,让有很多路由需要权限时,来实现权限控制
meta: { requireAuth: true } //requireAuth是自定义属性,根据需求自己命名
//创建全局导航守卫,当路由发生变化进行判断
router.beforeEach((to, from, next) => {
//用户访问了添加meta的路由时
if (to.matched.some(record => record.meta.requireAuth)) {
//判断本地存储是否有有user属性
if (!localStorage.getItem('user')) {
//判断取反,没有值的话,进入login路由
next({
path: '/login',
//跳转到login路由组件,并传入query参数,定义属性redirect的值为当前地址
query: {
redirect: to.fullPath
}
})
//user如果有值,直接跳转
} else {
next()
}
}
//访问的路由没有meta,跳转
next()
})
登录组件的方法:
methods: {
Handlelogin() {
//获取用户名和密码
//与后端发生交互
setTimeout(() => {
let data = {
user: this.user
};
//保存用户名到本地
localStorage.setItem("user", JSON.stringify(data));
//跳转到路由地址
this.$router.push({
//获取地址为当前路由传入的redirec属性,meta判断传入的前一个路由的完整地址
path: this.$route.query.redirect
});
}, 2000);
}
}
路由组件内获取数据
- vue.config.js 模拟数据接口
module.exports = {
devServer: {
before: (app, serve) => {
app.get('/api/post', (req, res) => {
res.json({
title: 'vue-router的获取数据',
body: '1.在导航完成之后获取数据'
})
})
}
}
}
- 安装axios,并在main.js引入
import axios from 'axios'
Vue.prototype.$https = axios;
- 组件内结构
<template>
<div>
<h2>获取数据</h2>
<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>
<h4>内容:{{post.body}}</h4>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
post: null,
error: null,
loading: false
};
},
created() {
//路由组件被创建后调用getPostData方法获取数据
this.getPostData();
},
watch: {
//监听$route,当前路由发生变化重新调用此路由组件时调用getPostData请求数据
$route:'getPostData'
},
methods: {
//异步函数
async getPostData() {
try {
//发送请求前显示loading
this.loading = true;
//向接口发起请求并解构data获取数据
const { data } = await this.$https.get("/api/post");
//获取数据后隐藏loading
this.loading = false;
//获取数据后将data赋值给组件内的post属性
this.post = data;
} catch (error) {
//如果返回错误,则赋值error并显示
this.error = error.toString();
}
}
}
};
</script>