路由本质上来说就是一种对应关系,比如说我们在浏览器中输入不同的 url 地址,我们就能访问不同的资源,那么这 url 地址与资源之间的对应关系,就是路由。
后端路由是通过服务器实现的,根据不同的用户 url 请求,返回不同的资源,本质上就是 url 地址与服务器资源之间的对应关系。
前端路由是通过页面 hash 值(锚链接) 的变化实现的,根据不同的页面事件来显示不同的页面内容,本质上是用户事件与事件处理函数之间的对应关系。
前端路由的核心就是监听事件并分发执行事件处理函数,所以需要依靠 window.onhashchange 事件来监听 hash 值的变化,以及 location.hash 获取目前页面的 hash 值。
SPA(Single Page Application)单页面应用程序,整个页面只有一个页面,内容的变化通过 ajax 局部更新实现,同时支持浏览器地址栏的前进与后退操作,又称单页面多视图。其实现原理是基于 url 地址的hash变化,hash变化会导致浏览器访问记录的变化,但不会触发新的url请求。SPA最核心的技术点就是前端路由。
SPA 还有后端路由渲染和ajax前端渲染两种解决方案,但是后端路由渲染的性能很低,ajax前端渲染,虽然性能高,但不支持浏览器的前进后退操作。
① 良好的交互体验
内容的改变不需要重新加载整个页面,页面数据的获取通过ajax异步获取,没有页面之间的跳转,不会出现跳转白屏现象。
② 良好的前后端分离的工作模式
前端只需要专注于页面的渲染,更利于前端工程化的发展。
后端只需要专注于API接口的提供,更易实现API接口的复用。
③ 减轻服务器的压力
服务器只提供数据,不负责页面的合成和逻辑的处理,吞吐能力提高几倍。
① 首屏加载慢
解决方案:路由懒加载、代码压缩、CDN加速、网络传输压缩
② 不利于SEO(搜索引擎优化)
解决方案:SSR服务器端渲染
<script src="./lib/vue_2.5.22.js">script>
<div id="app">
<a href="#/zhuye">主页a>
<a href="#/keji">科技a>
<a href="#/caijing">财经a>
<a href="#/yule">娱乐a>
<component :is="comName">component>
div>
<script>
// #region 定义需要被切换的 4 个组件
// 主页组件
const zhuye = {
template: '主页信息
'
}
// 科技组件
const keji = {
template: '科技信息
'
}
// 财经组件
const caijing = {
template: '财经信息
'
}
// 娱乐组件
const yule = {
template: '娱乐信息
'
}
// #endregion
// #region vue 实例对象
const vm = new Vue({
el: '#app',
data: {
comName: 'zhuye'
},
// 注册私有组件
components: {
zhuye,
keji,
caijing,
yule
}
})
// #endregion
// 监听 window 的 onhashchange 事件,根据获取到的最新的 hash 值,切换要显示的组件的名称
window.onhashchange = function() {
// 通过 location.hash 获取到最新的 hash 值
console.log(location.hash);
// 获取的hash值为 #/xxx 形式 所以使用 slice 去除 #
switch(location.hash.slice(1)){
case '/zhuye':
vm.comName = 'zhuye'
break
case '/keji':
vm.comName = 'keji'
break
case '/caijing':
vm.comName = 'caijing'
break
case '/yule':
vm.comName = 'yule'
break
}
}
script>
Vue Router 是一个 Vue 官方提供的路由管理器,功能十分强大。与 Vue 十分契合,可以一起方便的实现SPA(single page web application,单页应用程序)应用程序的开发。在页面中,想要使用Vue Router 需要跟Vue一样引入库文件,但Vue Router依赖于Vue,所以需要先引入Vue,再引入Vue Router。
默认为hash模式,URL中带 # 。可以通过mode来设置为历史模式(借助于HTML5的 history API实现):
const router = new VueRouter({
mode: 'history',
routes: [...]
})
<script src="./lib/vue_2.5.22.js">script>
<script src="./lib/vue-router_3.0.2.js">script>
<div id="app">
<router-link to="/user">Userrouter-link>
<router-link to="/register">Registerrouter-link>
<router-view>route-view>
div>
<script>
// 定义路由组件
const User = {
template: 'User 组件
'
}
const Register = {
template: 'Register 组件
'
}
// 创建路由实例对象
const router = new VueRouter({
// 配置路由规则
routes: [
//每一个路由规则都是一个对象,对象中至少包含path和component两个属性
//path表示 路由匹配的hash地址,component表示路由规则对应要展示的组件对象
{ path: '/user', component: User },
{ path: '/register', component: Register }
]
})
// 创建 vm 实例对象
const vm = new Vue({
// 指定控制的区域
el: '#app',
data: {},
// 挂载路由实例对象
router: router
// 可简写为:
// router
})
script>
路由重定向是指:在用户访问某个地址A时,强制用户跳转到地址 B,并展示相关内容。想要实现重定向,需要使用路由规则的 redirect 属性。我们一般通过路由重定向为页面设置默认展示的组件。
var myRouter = new VueRouter({
//routes是路由规则数组
routes: [
//path设置为/表示页面最初始的地址 / ,redirect表示要被重定向的新地址,设置为一个路由地址即可
{ path:"/",redirect:"/user"},
{ path: "/user", component: User },
{ path: "/login", component: Login }
]
})
路由嵌套是指:当我们点击父级路由链接,在路由占位符显示的组件内容中有子级路由链接,点击子级路由链接时,在子级路由占位符显示子级组件内容。子级路由规则,由父级路由的 children 属性来配置。
// 父级路由
const Register = {
template: `
Register 组件
tab1
tab2
`
}
// 两个子级组件
const Tab1 = {
template: 'tab1 子组件
'
}
const Tab2 = {
template: 'tab2 子组件
'
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user'},
{ path: '/user', component: User },
// children 数组表示子路由规则
{ path: '/register',
component: Register,
children: [
{ path: '/register/tab1', component: Tab1 },
{ path: '/register/tab2', component: Tab2 }
]
}
]
})
补充:
①、 在嵌套路由中,子路由和父路由之间如何传值?
虽然是通过路由嵌套的方式展示的页面,当其形式还是相当于父子组件的的关系,所以我们可以通过父子组件传值的方式来传值,只不过是将router-view
视为子组件在父组件中的载体,具体如下:
// 子路由 通过 this.$emit 向父路由传递数据
this.$emit('other-tab', 4)
// 父路由 通过绑定事件来接受值
<router-view @other-tab="toOthersTab($event)" />
// 取值事件
toOthersTab (val) {
console.log(val) // 4
}
那么同理,父路由也可以给子路由传递数据:
// 父路由传参
<router-view :id="12138" @other-tab="toOthersTab($event)" />
// 子路由通过 props 接收
props: {
id: {
type: Number,
default: 0
}
},
mounted () {
console.log(this.id)
}
②、在子路由中如何调用父路由的方法和变量?
如果我们想要在子路由中直接去调用父路由的方法和参数,而且不想借用this.$emit
这种麻烦的方法,我们可以通过this.$parent
来实现:
// 父路由中的方法和变量
data () {
return {
father: 'me'
}
},
methods: {
toLog () {
console.log('这是父路由中的方法')
}
}
// 在子路由中调用父路由的方法和变量
data () {
return {
son: 'you'
}
},
methods: {
toFather () {
console.log('子路由调用父路由中的变量')
this.son = this.$parent.father
console.log('son=' + this.son)
console.log('子路由调用父路由中的方法')
this.$parent.toLog()
}
}
③在父路由中调用子路由的变量和方法
如果我们想要在父路由中,直接调用子路由的方法和变量,我们可以通过$ref
来引用router-view
这个dom元素的方式,去调用对应的变量和方法。
这种方法唯一要注意的就是在调用子路由的属性和方法的时候,子路由必须已经加载完成,否则无法访问到变量和方法,推荐在父路由的mounted阶段以后再调用,因为此时子路由必定已经加载完成:
// 子组件定义变量和方法
data () {
return {
son: 'son'
}
},
methods: {
test () {
console.log('这是子路由中方法,尝试是否能被父路由调用')
}
}
// 在父路由中通过ref调用子路由的变量和方法
<reouter-view ref='son' />
mounted () {
// 在mounted阶段调用 因为子路由此时已经加载完成
console.log('调用子路由中的方法')
console.log(this.$refs.son.test())
console.log('调用子路由中的变量')
console.log(this.$refs.son.son)
}
6、动态路由匹配(路由传参)
① $route.params.参数
设置路由规则时,在路径path里,通过 /:参数 的形式,设置参数。然后在组件中通过 {{ $route.params.参数 }} 的形式,获取参数。该方法耦合度高,不够灵活,很少使用。
<div id="app">
<router-link to="/user/1">User1router-link>
<router-link to="/user/2">User2router-link>
<router-link to="/user/3">User3router-link>
<router-view>router-view>
div>
<script>
const User = {
template: 'User 组件 -- 用户id为: {{$route.params.id}}
'
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user'},
{ path: '/user/:id', component: User },
]
})
script>
② props 的值为 true
在路由规则中,将props的值设置为 true,使 $route.params 成为组件属性。在路径path里,通过 /:参数 的形式,设置参数,然后在组件中,使用 props 接收参数即可。
// 使用 props['参数'] 来接收参数
const User = {
props: ['id'],
template: 'User 组件 -- 用户id为: {{id}}
'
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user'},
{ path: '/user/:id', component: User, props: true },
]
})
③ props 的值为对象
我们将props 的值设置为一个对象,就相当于将对象中的所有属性传递给组件,组件使用 props[‘属性名1’,‘属性名2’] ,即可接收数据。
const User = {
// 接收对象中的数据,但id接收不到,因为传递的对象中没有这个属性
props: ['id', 'uname', 'age'],
template: 'User 组件 -- 用户id为: {{id}} -- 姓名为:{{uname}} -- 年龄为:{{age}}
'
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user'},
// 传递的是静态对象
{ path: '/user/:id', component: User, props: { uname: 'lisi', age: 20 } }
]
})
④ props 的值为函数
如果既想获取路径传递的参数值,又想获取传递的对象数据,那么props应该设置为函数形式。
// 组件
const User = {
props: ['id', 'uname', 'age'],
template: 'User 组件 -- 用户id为: {{id}} -- 姓名为:{{uname}} -- 年龄为:{{age}}
'
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user' },
{
// 路径传递的参数
path: '/user/:id',
component: User,
// 以函数的形式 传递数据到组件
props: route => ({ uname: 'zs', age: 20, id: route.params.id })
},
{ path: '/register', component: Register }
]
})
7、命名路由
为了更加方便的表示路由的路径,我们可以给路由规则起一个别名,即为命名路由。
<router-link :to="{ name: 'user', params: {id: 3} }">User3router-link>
<script>
// 声明组件
const User = {
props: ['id', 'uname', 'age'],
template: 'User 组件 -- 用户id为: {{id}} -- 姓名为:{{uname}} -- 年龄为:{{age}}
'
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user' },
{
// 命名路由
name: 'user',
path: '/user/:id',
component: User,
props: route => ({ uname: 'zs', age: 20, id: route.params.id })
}
]
})
script>
8、编程式导航
页面导航分为两种方式:声明式导航和编程式导航。通过点击链接实现导航的方式,叫做声明式导航,比如:网页中的 a 超连接 或者 vue router 里面的 标签。通过调用 JavaScript 形式的API实现导航的方式,叫做编程式导航,比如:网页中的location.href 。
vue Router 中常用的编程式导航 API 有:
this.$router.push()
该API的参数形式有很多:
① 路径字符串,例如:this.$router.push(‘/user’)。
② 路径对象,例如:this.$router.push({path: ‘/user’})。
③ 命名路由对象,例如:this.$router.push({name: ‘/user’})
④ 带查询参数,变成 /user?uname=lisi的形式,例如:this.$router.push({ path: ‘/user’,query: { uname } })
this.$router.replace()
该API与this.$router.push() 作用基本相同。只不过它在跳转到指定 hash 地址,不会增加新的历史记录,只会替换掉当前的历史记录。
this.$router.go(n)
该 API 参数是数字,数字的大小表示浏览器历史中前进或后退的页数,可以为负值,负值代表后退,正值代表前进。
综合案例:
<router-link :to="{ name: 'user', params: {id: 3} }">User3router-link>
<router-link to="/register">Registerrouter-link>
<script>
const User = {
props: ['id', 'uname', 'age'],
template: `
User 组件 -- 用户id为: {{id}} -- 姓名为:{{uname}} -- 年龄为:{{age}}
`,
methods: {
// 代码中用到的方法
goRegister() {
this.$router.push('/register')
}
},
}
const Register = {
template: `
Register 组件
`,
methods: {
goBack() {
// 后退
this.$router.go(-1)
}
}
}
// 创建路由实例对象
const router = new VueRouter({
// 所有的路由规则
routes: [
{ path: '/', redirect: '/user' },
{
// 命名路由
name: 'user',
path: '/user/:id',
component: User,
props: route => ({ uname: 'zs', age: 20, id: route.params.id })
},
{ path: '/register', component: Register }
]
})
// 创建 vm 实例对象
const vm = new Vue({
// 指定控制的区域
el: '#app',
data: {},
// 挂载路由实例对象
// router: router
router
})
script>
9、导航守卫
导航守卫是用来控制路由的访问权限,比如可以结合token 检查用户是否登录,当未登录的用户想要访问某些页面时,通过路由导航将其跳转到登录页面。vue-router 的导航守卫有全局前置守卫、全局解析守卫、路由独享的守卫等等等。我目前只学习了全局前置守卫,所接下来就总结一下全局前置守卫。剩下的后续补充。
① 全局前置守卫:
当给项目设置了全局前置守卫之后,在每次发生路由的导航跳转时,都会触发。所以开发者可以在全局前置守卫中,对路由的访问权限进行控制。
基本语法:
// 创建路由实例对象
const router = new VueRouter({....})
// 设置全局前置守卫
router.beforeEach((to,from,next) => {
// to 是将要访问的路由对象,包含路由地址等相关信息。
// from 是将要离开的路由对象,包含路由地址等相关信息。
// next 是一个内置函数,调用 next() 表示允许通过,
// next('路由地址') 表示强制跳转到某个页面
// next(false) 表示不允许跳转
})
基本使用:
// 创建路由实例对象
const router = new VueRouter({....})
// 需要权限才能访问的页面路由地址,以数组的形式
const pathArr = ['/home', '/home/users', '/home/rights']
// 设置全局前置守卫
router.beforeEach(function(to, from, next) {
// to将要访问的路由的地址 是否需要权限
if (pathArr.indexOf(to.path) !== -1) {
// 验证权限
const token = localStorage.getItem('token')
// 根据结果不同 进行不同的操作
if (token) {
next()
} else {
next('/login')
}
} else {
next()
}
})
② 组件内的守卫钩子函数
组件内的守卫钩子函数是指写在某一页面组件内的钩子函数,只有当页面的路由变化与该组件相关时才会调用执行。一同提供了三种钩子函数,适用于各种不同的场景:
beforeRouteEnter(to,from,next)
该守卫钩子函数是在要跳转该组件对应的路由(或该组件对应路由的子路由)并且在该组件渲染之前调用执行的。第一个参数 to
表示要跳转到的路由信息对象,常用属性为 to.path
可以获取到要跳转到的路由地址。第二个参数form
表示要跳转离开的路由信息对象。因为该守卫函数执行时,组件实例还没有被创建,所以不能获取组件的this
,也就是无法获取或改变组件中的数据,但我们可以通过给第三个参数 next
传递一个回调函数,该函数的参数就是组件实例,通过这种方式来操作组件中的数据:
// 守卫钩子函数1
beforeRouteEnter (to, from, next) {
next((vm) => {
// 通过 vm 访问组件实例
console.log(vm.a); // 1
})
},
data() {
return {
a: 1
}
}
beforeRouteUpdate(to,from)
:
该守卫钩子函数是在当前路由发生了改变但该组件被复用时调用执行,经过我的实验,只有在动态路由这一种情况下,该方法才会被执行,举例来说就是:如果一个路由地址定义为:/course/:id
,当路由地址从/course/1
跳转到 /course/2
时,该守卫钩子函数才会被执行。函数的两个参数与beforeRouteEnter(to,from)
意义相同,但此时组件实例已经创建好了,所以在该守卫钩子函数里面可以操作组件中的数据:
// 守卫钩子函数2
beforeRouteUpdate(to,from){
// 通过this访问组件实例
console.log(this.a); // 1
},
data() {
return {
a: 1
}
}
beforeRouteLeave(to,from)
:
该守卫钩子函数是在跳转离开该组件时调用执行,也可以直接操作组件中的数据。函数的两个参数与beforeRouteEnter(to,from)
意义相同,这个守卫钩子函数通常用来预防用户在还未保存修改前突然离开的情况,可以通过return false
来取消路由跳转,让页面不变:
// 守卫钩子函数3
beforeRouteUpdate(to,from){
// 提示用户有未保存的内容,是否要离开
const answer = window.confirm('你还有未保存的内容,是否要离开本页面?')
// 如果不离开 则取消路由跳转
if (!answer) return false
}
③ 完整的路由跳转解析流程
- 导航被触发,路由进行跳转。
- 在要离开的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在复用的组件里调用 beforeRouteUpdate 守卫(2.2版本新增)。
- 调用路由配置里的 beforeEnter 守卫。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5版本新增)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
- 结束。
10、路由跳转时在新窗口打开页面
① router-link 标签
vue-router的 router-link
标签最后会被转换成 a
标签,所以我们可以在 router-link
标签上增加 target="_blank"
属性,这样就能实现在路由跳转的时候实现在新窗口打开页面的效果。
// 在新窗口打开页面
<router-link target="_blank" :to="{path:'/myInfo'}">新页面打开myInfo页</router-link>
// 可以传递参数,与原来 router-link 路由传参方式相同
<router-link target="_blank" :to="{path:'/myInfo',params:{id:'8'}}">新页面打开myInfo页</router-link>
② 编程式导航
当我们需要在点击事件或者某个函数中实现在新窗口中打开某个页面的效果,我们可以借助编程式导航来实现,但是在Vue2.0以后,只有$router.resolve()
支持这种操作,$router.push()
和 $router.go()
都不支持。
// 事件函数
function () {
let routeData = this.$router.resolve({
path: '/myInfo'
// 或者 path 和 name 都可,没有什么不同
// name: '/myInfo'
});
// 在新窗口 打开页面
window.open(routeData.href, '_blank');
}
// 也可传递参数
function () {
let routeData = this.$router.resolve({
path: '/myInfo'
// 或者 path 和 name 都可,没有什么不同
// name: '/myInfo'
query: {
id: 12138
}
});
// 在新窗口 打开页面
window.open(routeData.href, '_blank');
}
// 新页面中取参数的方式
this.$route.query.id