# Vue路由
# 什么是SPA
SPA
SPA 是single page application 的简称,翻译为单页应用。 简单来说SPA就是一个Web项目只有一个HTML页面,一但页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用JS动态的变换HTML的内容,来模拟多个视图之间的跳转。
# 前端路由的出现
TIP
随着AJAX
技术的出现,才逐渐演化出了SPA,SPA的出现大大提高了用户的体验。在于用户的交互中,不再需要刷新页面,通过AJAX
技术异步获取数据,页面展示变得更加流畅。
但由于SPA中用户的交互是通过JS
改变HTML
内容来实现的,页面本身的URL并没有发生变化,这导致了两个问题:
- 1、SPA无法记录用户的前进、后退操作
- 2、SPA中虽然业务的不同会有多种页面的展示,但是只有一个
URL
,对用户来说不够友好,每次刷新页面,都要重新点一遍要去到的页面 - 3、没有路径-页面的统一概念,页面管理复杂,维护起来不友好
前端路由就是为了解决上述问题而出现的,如vue-router,react-router
# Vue-router
路由
前端路由就是一个路径管理器,通俗的说,vue-router
就是一个WebApp
的链接路径管理系统。vue的单页面应用是基于路由和组件的,路由用来设置访问路径,相当于页面对应的URL
,并将路径和组件映射起来。
传统的页面应用,是采用超链接的方式来实现页面切换和跳转的。在vue-router
中,则是路径之间的切换,也就是组件的切换。
路由模块的本质就是建立起URL
和页面之间的映射关系,在刷新、前进、后退时均通过url
来实现。
为什么不用a
标签:
因为用vue做的都是单页应用,相当于只有一个主index.html
(也就),所以a
标签是起不到作用的,你必须使用vue-router
来管理。
# 原理
原理
简单的说,只有一个HTML页面,为每一个组件匹配一个路由(访问路径)。在用户刷新、前进、后退、跳转操作时,不更新整个页面,而是根据路由只更新某个组件。 要做到上面的需求我们满足以下两个核心:
- 1、改变
url
而不让浏览器向服务器发送请求。 - 2、可以监听到url的变化,并执行对应的操作(如组件切换)
对此,有两种模式实现的以上的功能:hash模式
和 history模式
# hash模式
hash
vue-router
默认采用hash
模式:使用url
后的#
后面的字符。比如www.baidu.com/#aaaa
其中的#aaaa
就是我们要的hash
值。
为什么可以用hash
:
- 1、
hash
值的变化不会导致浏览器向服务器发送请求,不会引起页面刷新。 - 2、
hash
值的变化改变会触发hashchange
事件 - 3、
hash
值改变也会在浏览器的历史中留下记录,使用浏览器的后退
按钮,就可以回到上一个hash
值
在H5
的history
模式出现之前,基本都是使用hash
模式实现前端路由的
# history模式
history
在HTML5
之前,浏览器就已经有了history
对象。但是只能用于页面跳转
history.go(n)
// 前进或者后退,n代表页数,负数表示后退history.forward()
// 前进一页history.back()
// 后退一页
在HTML5
中,history新增了以下API
history.pushState()
// 向历史栈中添加一新的状态history.replaceState()
// 修改当前项在历史栈中的记录history.state
// 返回当前状态对象
pushState
和replaceState
均接收三个参数(state,title,url)
state
: 合法的js对象,可以用在popstate
事件中title
: 大多数浏览器忽略这个参数,可以传null占位url
: 任意有效的URL(必须与当前url同源,否则会抛出异常),用于更新浏览器的url。在调用pushState
或replaceState
之后,会立即产生一个新的带有新url的历史记录(当前url已变成最新的),但浏览器不会立即加载这个url(页面不会刷新),可能会在稍后某些情况下加载这个url,比如用户重新打开浏览器或在地址栏中按回车键。
pushState
和replaceState
的区别在于:
pushState
会保留现有历史记录的同时,将带有新url的记录添加到历史栈中。replaceState
会将历史栈中的当前页面历史中的state、url
替换为最新的
由于pushState
和replaceState
方法可以在改变当前url的同时,并不刷新页面,这样就可以用来实现前端路由。
但是由于调用pushState
和replaceState
不会触发事件(popstate
事件只有在用户手动点击前进、后退按钮或者在js中调用go、back、forward
方法时才会触发),所以有对应的解决方案:
- 1、点击前进、后退按钮或者在
js
中调用go、back、forward
方法:监听popstate
事件,在事件回调中根据当前的url
,渲染对应的页面即可。 - 2、当需要跳转路径时(调用
push
方法时):先根据拿到的路径渲染对应的页面,然后在页面渲染完成之后通过pushState
或replaceState
方法来更新浏览器的url。
这样就实现了history模式的前端路由
history 在修改了url后,虽然页面不会刷新,但是我们在手动刷新页面之后,浏览器会以当前url向服务器发送请求,但是由于我们只有一个html
文件,浏览器在处理其他路径的时候,就会出现404的情况。此时需要在服务端增加一个覆盖所有情况的候选资源:如果url匹配不到静态资源,就返回单页应用的html
文件。这样,就完全交给了前端来处理对应的路由。
# hash、history模式的选择
区别
hash
优点:
- 1、兼容性好,可以兼容IE8
- 2、不需要服务端做任何配置即可处理单页应用
缺点:
- 1、路径丑
- 2、锚点功能会失效
- 3、相同的
hash
不会更新历史栈
history
优点:
- 1、路径更好看
- 2、锚点功能可用
- 3、
pushState
可添加相同的记录到历史栈中
缺点:
- 1、兼容性差,不能兼容IE9
- 2、需要服务端支持
# vue-router的使用
注入vue-router
1、在要使用路由的项目中打开
CMD
命令 输入npm install vue-router
,安装vue
的路由模块2、在页面中先引入
vue.js
,再引入vue-router.js
,因为vue-router
是基于vue
的。利用Vue.js
提供的插件机制,Vue.use(plugin)
来安装VueRouter
,这样做的目的是为了不依赖Vue
的版本,两者的版本可以混用。这个插件机制的原理是调用插件的install
方法(如果有install
方法,没有就把传入的插件当成函数调用)。
import Vue from 'vue'
import VueRouter from 'vue-router'
// 将VueRouter当做插件传入:目的是为了不依赖vue,可以版本混用,也可以只用VueRouter
Vue.use = function(plugin, options){
plugin.install(this, options)
}
Vue.use(VueRouter) // 会调用插件的install方法并把Vue传进去
install.js
的作用:
- 1、引入
Vue
并保存,方便之后使用 - 2、将根组件注入到每一个组件上,保证每一个
Vue
的实例都可以拿到根组件上的router属性 - 3、实现路由响应式原理,将根组件上的
_route
属性定义为响应式数据,这样_route
属性改变就可以更新视图 - 4、初始化路由
- 5、注册全局路由组件
// install.js
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install(Vue){
_Vue = Vue
// 采用mixin 的 方式,将根组件注入到每一个组件上
Vue.mixin({
beforeCreate(){
// 存在router属性则代表是根组件
if(this.$options.router){
this._routeRoot = this
this._router = this.$options.router
/*
此处init要放在defineReactive之前。
因为首次渲染已经存在路径,需要在init时 transitionTo更新history.current,
init之后,此时history.current已经是当前url对应的路由对应的记录,
接下来我们只需要在 根组件上定义响应式属性_route,指向最新的路由记录,即可更新视图
*/
this._router.init(this)
Vue.utils.defineReactive(this,'_route', this._router.history.current)
} else {
this._routeRoot = this.$parent && this.$parent._routeRoot || this
}
}
})
// 为了在组件中方便使用属性路由属性,增加属性代理
Object.defineProperty(Vue.property, '$route',{
get(){
return this._routeRoot._route
}
})
Object.defineProperty(Vue.property, '$router',{
get(){
return this._routeRoot._router
}
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
}
# 实例化VueRouter
TIP
- 1、声明创建路由表,在路由表中将路径和对应的组件关联起来
- 2、创建路由实例,初始化路由并传入路由表
- 3、在vue的实例中注册路由
import App from './App'
let Home = { template:'<div>首页</div>' };
let List = { template:'<div>列表页</div>' };
// 1、创建路由实例
let router = new VueRouter({//初始化路由:传入路由表
mode: 'history', // 需要后端做支持
routes:[
// 一级路径前必须加 ‘/’
{ path: '/', component:Home },//默认展示的路由(默认展示的不需要加/)
{ path: '/home', component:Home },//一个路径对应一个组件
{ path: '/list', component:List },
{ path: '*', redirect:'/home' }//用户随便输入路径时,重定向到home组件,防止出现404
] // es6简写
});
//2、在vue根实例中注入路由实例,之后就可以在页面中使用了
new Vue({
el:'#app',
router, // 注入路由(es6简写)
render: h => h(App)
})
// vue路由src/index.js
export default class VueRouter {
// 会根据对应的模式生成对应的 history实例,挂载到当前类的实例上
init(app){ // 此处传入的是在install.js中调用router.init时 传入的根组件的实例
const history = this.history
const setupListener = ()=>{
// 方便加一些其他的操作
history.setupListener()
// todo...
}
// 拿到当前的路径,调用transitionTo方法,根据路径匹配到对应的路由记录route,
history.transitionTo(history.getCurrentLocation(), setupListener)
history.listen(route =>{
/*
因为在install.js中 根(app)组件的_route属性已经被定义为了响应式的
Vue.util.defineReactive(this, '_route', this._router.history.current)
所以只需要更新数据,就可以驱动视图更新
*/
app._route = route
})
}
}
# router-view
router-view
router-view
(全局组件:用来渲染路由对应的组件)
在页面中使用router-view
这个全局组件来将路由对应的页面渲染到页面上
<div id="app">
<router-view></router-view>
</div>
# router-link
router-link
使用router-link
全局组件,来实现点击跳转
router-link
存在两个属性:
to
:跳转到哪个(必须加,值为要跳转的路径)tag
:要把router-link
变为哪个标签(不改默认是a
标签)
<!-- 修改上面的HTML如下 -->
<div id="app">
<router-link to="/home" tag="button">首页</router-link>
<router-link to="/list" tag="button">列表页</router-link>
<!-- 如果写成对象的形式,而且需要params参数的话,就只能用name来实现跳转了(用ptah的话会导致params不生效) -->
<router-link :to="{name:'list',params:{userId:1}}" tag="button">列表页</router-link>
<router-view></router-view>
</div>
router-link
和a
标签的区别
router-link
默认会使用a
标签,但是给a
标签添加了点击事件,并没有直接使用a
标签的href
属性去跳转,通过a
标签的点击事件,在事件中拿到要跳转的路径,并根据路由匹配出对应的页面并展示,而不用刷新整个页面。而a
标签是利用的href
属性去做的页面的跳转,这样的话整个页面都会刷新。
# 路由信息与方法
路由
当在vue
的实例中注册过路由之后,每个vue组件实例都可以通过获取$router
、$route
属性来获取根组件上的_router
、_route
属性(因为在Vue.use
注入的时候,install.js
中已经做了代理);
$route
:路由信息对象,表示当前激活的路由的状态信息,包含了当前URL解析的信息,还有URL匹配到的路由记录$router
:当前的路由的实例,原型上有各种跳转的方法
# $router
其原型上存储了跳转的方法
this.$router.push()
:强制跳转到某个路径,参数为路径this.$router.replace()
:路由替换,将当前路径替换为新的路径(很少用到)this.$router.go()
:返回某一级,参数为返回多少级(-1为上一级,1为下一级)
# $route
路由信息对象,表示当前激活的路由的状态信息
当前激活的路由信息对象。这个属性是只读的,里面的属性是 immutable (不可变) 的,不过可以 watch
(监测变化) 它。
$route.path
:字符串,对应当前路由的路径,总是解析为绝对路径,如/foo/bar
。$route.params
:一个key/value
对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象。$route.query
:一个key/value
对象,表示URL
查询参数。例如,对于路径/foo?user=1
,则有$route.query.user == 1
,如果没有查询参数,则是个空对象。$route.hash
:当前路由的hash
值 (带#
) ,如果没有hash
值,则为空字符串。$route.fullPath
:完成解析后的URL
,包含查询参数和hash
的完整路径。$route.name
:当前路由的名称,如果有的话。$route.redirectedFrom
:如果存在重定向,即为重定向来源的路由的名字。
//由于路径有很多,而我们不能把路径写死,所以要写成类似正则的形式来匹配路径
/article/2/d //一个路径
/article/:c/:a //表示路径匹配,和上面的匹配后产生一个对象,存在$route.params当中:{c:2,a:d}
params
传参和query
传参的区别
params
要用name
来引入,接收参数用this.route.params.xxx
//params语法:刷新页面会消失 this.$router.push({ name:"Search", params:{ id:123 }}); //这是传递参数 this.$route.params.id; //这是接收参数 // 如果是路径参数,需要用path /a/:userId 路径参数的话刷新是不会消失的 this.$router.push({ path:"/a/123" }); //这是传递参数 this.$route.params.userId //这是接收参数
query
要用path
来引入,接收参数用this.route.query.xxx
//query语法: this.$router.push({ path:"/a", query:{id:123 }}); //这是传递参数 this.$route.query.id; //这是接收参数
注意
query
刷新不会丢失query
里面的数据(因为会带到url
上),params
如果是路径参数的话,因为也会映射到url中,所以刷新也不会消失,如果不是路径参数的话,通过name
的方式进入组件,params
传参的话,刷新会丢失params
里面的数据。
# 路由的嵌套
可在路由表中嵌套二级路由,嵌套二级路由的一级路由的
template
也要做对应的修改;
<div id="app">
<router-link to="/home">首页</router-link>
<router-link to="/detail">详情页</router-link>
<router-view></router-view><!--一级路由显示区域-->
</div>
<template id="detail">
<div>
<router-link to="/detail/info">个人中心</router-link>
<router-link to="/detail/about">关于我</router-link>
<router-view></router-view><!--二级路由显示区域-->
</div>
</template>
//组件
let home={template:'<div>home</div>'};
let detail={template:'#detail'};
let info={template:'<div>info</div>'};
let about={template:'<div>about</div>'};
//创建路由表
let routes=[
{ path:'/home',component:home },
{
path:'/detail',
component:detail,
//二级路由写在childern属性当中
children:[
//二级以及二级以上路由的路径永远不带‘/’,如果带‘/’代表是一级路由
{ path:'info',component:info },
{ path:'about',component:about }
]
},
];
//初始化路由并传入路由表
let router = new VueRouter({
mode: 'history',
routes
});
let vm = new Vue({
el:'#app',
//注册路由
router
})
# 动态路由
TIP
需要用到的方法:
router.beforeEach
:vue-router
提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。router.addRoutes()
:动态挂载路由(此方法的作用只是在路径中访问时可以看到对应的页面,但是菜单展示还需要),参数必须是一个符合routes
选项要求的数组
这里还有一个小hack
的地方,就是router.addRoutes
之后的next()
可能会失效,因为可能next()
的时候路由并没有完全add
完成,好在查阅文档发现next('/')
or next({ path: '/' })
: redirect to a different location. The current navigation will be aborted and a new one will be started.
这样我们就可以简单的通过next(to)
巧妙的避开之前的那个问题了。这行代码重新进入router.beforeEach
这个钩子函数,这时候再通过next()
来释放钩子,就能确保所有的路由都已经挂载完成了。
router index.js
文件
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
// 创建静态路由表
export const constantRouterMap = [
{ path: '/404', component: () => import('@/views/404'), hidden: true },
{
path: '/',
component: Layout,
redirect: '/home',
name: 'Home',
children: [{
path: 'home',
component: () => import('@/views/home/index'),
meta: { title: '首页', icon: 'home' }
}]
}
]
// 初始化路由传入静态路由表并导出
export default new Router({
mode: 'history',// 使用history模式
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
// 全部路由(需要过滤的动态路由表,以后加项目的话直接加在这里即可,拉到权限后需要根据权限过滤此表)
export const asyncRouterMap = [
// 权限管理
{
path: '/access',
component: Layout,
name: 'access',
children: [
{
path: 'index',
name: 'access/index',
component: () => import('@/views/access/index'),
meta: { title: '权限管理', icon: 'lock' }
}
]
},
// 运营后台
{
path: '/operation',
component: Layout,
name: 'operation',
meta: {
title: '运营后台',
icon: 'operation'
},
children: [
// 映客直播
{
path: 'live',
component: () => import('@/views/operation/index'), // Parent router-view
name: 'operation/live',
meta: { title: '映客直播' },
children: [
// 意见反馈
{
path: 'feedback',
name: 'operation/live/feedback',
component: () => import('@/views/feedback/index'),
meta: { title: '意见反馈' }
}
]
}
]
}
]
路由创建好之后需要在拉取权限之后对全局路由表进行过滤,筛选掉没有用的路由。