Vue-Router

414 阅读4分钟

介绍

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 -S

main.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.$routethis.$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>

这个问题其实也可以通过在子组件内布局整个视图来解决,不过命名视图的方法在整个项目结构上更加舒服,需要多个路由共享的组件,也不需要在组件内引入就可以很方便的显示

导航守卫

导航表示路由正在发生改变

完整的导航解析流程

  1. 导航被触发
  2. 在失活的组件里调用离开守卫
  3. 调用全局的 beforeEach 守卫
  4. 在复用的组件里调用 beforeRouteUpdate 守卫
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫
  9. 导航被确认
  10. 调用全局 afterEach 钩子
  11. 触发 DOM 更新
  12. 用创建好的实例调用 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);
	}
}

路由组件内获取数据

Mock Server 模拟后端数据

  • 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>