Vue Router
1、SPA(Single Page Application)
- 后端渲染(存在性能问题)
- Ajax前端渲染(前端渲染提高性能,但是不支持浏览器的前进后退操作)
- SPA单页应用程序:整个网站只有一个页面,内容的变化通过Ajax局部更新实现、同时支持浏览器地址的前进和后退。
- SPA实现原理之一:基于URL地址的hash(hash变化会导致浏览器记录访问历史的变化,但是hash的变化不会触发新的URL请求)
- 在实现SPA的过程中,最核心的技术点就是前端路由。
2 Vue Router的概念
VueRouter 是 SPA(单页应用)的路径管理器,它允许我们通过不同的 URL 访问不同的内容。
先了解 VueRouter 的两个内置组件:
<router-link>
:该组件用于设置一个导航链接,切换不同 HTML 内容。 to 属性为目标地址,即要显示的内容。例:<router-link to="/index">首页</router-link>
;<router-link>
默认会被渲染为 a 标签,to 属性默认会被渲染为 href 属,性,to 属性的值默认会被渲染为 # 开头的 hash 地址。
<router-view>
:通过路由规则匹配到的组件,将会被渲染到 <router-view>
所在的位置。
2.1 使用步骤
- 引入相关的库文件
<!-- 导入 vue 文件,为全局 window 对象挂载 Vue 构造函数 -->
<script src="./lib/vue_2.5.22.js"></script>
<!-- 导入 vue-router 文件,为全局 window 对象挂载 VueRouter 构造函数 -->
<script src="./lib/vue-router_3.0.2.js"></script>
- 添加路由链接
<router-link to='/user'>User</router-link>
<router-link to='/register'>Register</router-link>
- 添加路由填充位
<router-view></router-view>
- 定义路由组件
let User = {
template:`<div>User组件</div>`
}
let Register = {
template:`<div>Register组件</div>`
}
- 配置路由规则并创建路由实例
const routes = [
{path:'/user',component:User},
{path:'/register',component:Register}
]
var router = new VueRouter({
routes
})
- 把路由挂载到 Vue 根实例中
const vue = new Vue({
el:'#app',
router
})
2.2 嵌套路由
2.2.1 基本使用
实际项目中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件。
要配置嵌套路由,我们需要在配置的参数中使用children 属性。
{
path: '路由地址',
component: '渲染组件',
children: [
{
path: '路由地址',
component: '渲染组件'
}
]
}
- 嵌套路由功能分析
- 点击父级路由链接显示模板内容
- 模板内容中又有子级路由链接
- 点击子级路由链接显示子级模板内容
- 父路由组件模板
父级路由链接
父组件路由填充位
<div id="app">
<p>
<router-link to='/user'>User</router-link>
<router-link to='/register'>Register</router-link>
</p>
<router-view></router-view>
</div>
- 子级路由模板
const Register = {
template: `<div>
<h1>Register 组件</h1>
<hr/>
<router-link to="/register/tab1">Tab1</router-link>
<router-link to="/register/tab2">Tab2</router-link>
<!-- 子路由填充位置 --> <router-view/>
</div>`
}
- 嵌套路由配置
const router = new VueRouter({
routes: [
{ path: '/user', component: User },
{
path: '/register',
component: Register,
// 通过 children 属性,为 /register 添加子路由规则
children: [
{ path: '/register/tab1', component: Tab1 },
{ path: '/register/tab2', component: Tab2 }
] } ]
})
完整示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
</style>
<body>
<div id="app">
<!-- router-link 是 vue 中提供的标签,默认会被渲染为 a 标签 -->
<!-- to 属性默认会被渲染为 href 属性 -->
<!-- to 属性的值默认会被渲染为 # 开头的 hash 地址 -->
<router-link to="/user">User</router-link>
<router-link to="/register">Register</router-link>
<!-- 路由填充位(也叫做路由占位符) -->
<!-- 将来通过路由规则匹配到的组件,将会被渲染到 router-view 所在的位置 -->
<router-view></router-view>
</div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript" src="js/vue-router_3.0.2.js"></script>
<script type="text/javascript">
const User = {
template: '<h3>User</h3>'
}
const Register = {
template: `<div>
<h3> Register组件</h3>
<hr>
// 子路由链接
<router-link to="/register/Tab1">Tab1</router-link>
<router-link to="/register/Tab2">Tab2</router-link>
// 子路由占位符
<router-view></router-view>
</div>`
}
const Tab1 = {
template: '<h3>Tab1</h3>'
}
const Tab2 = {
template: '<h3>Tab2</h3>'
}
// 创建路由实例对象
const router = new VueRouter({
// routes 是路由规则数组
routes: [
// 每个路由规则都是一个配置对象,其中至少包含 path 和 component 两个属性:
// path 表示当前路由规则匹配的 hash 地址
// component 表示当前路由规则对应要展示的组件
{
path: '/',
redirect: '/user'
}, {
path: '/user',
component: User
}, {
path: '/register',
component: Register,
children: [{
path: '/register/Tab1',
component: Tab1
}, {
path: '/register/Tab2',
component: Tab2
}]
}
]
})
const vue = new Vue({
el: '#app',
// router: router,
router, //es6中属性名和属性值一样可以简写。
methods: {
},
})
</script>
</html>
2.3 定义路由地址
在上述的例子中,我们通过 ‘/register/tab1’ 来访问嵌套路由,但是有时候你可能不希望使用嵌套路径,这时候我们可以对上面例子中的配置信息做一点修改:
const routes = [
{path:'/user',component:User},
{path:'/register',
component:Register,
children:[
{path:'/registerTab1',component:Tab1},
{path:'/registerTab2',component:Tab2}
]
}
]
2.4 路由命名
我们可以在 route 对象中添加一个 name 属性用来给路由指定一个名字:
const router = new VueRouter({
routes: [
{
path: '/user',
name: 'user',
component: '[component-name]'
}
]
})
2. 5 vue-route编程式导航
2.5.1 页面导航的两种方式
- 声明式导航:通过点击链接实现导航的方式,叫做声明式导航
例如:普通网页中的 链接 或 vue 中的 - 编程式导航:通过调用JavaScript形式的API实现导航的方式,叫做编程式导航
例如:普通网页中的 location.href
Vue Router 提供了router的实例方法,通过编写代码来实现导航功能。在 Vue 实例内部,你可以通过$router
访问路由实例。
2.5.2 router.push
这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
基本用法
<div id="app">
<div>
<button @click="jump('index')">首页</button>
<button @click="jump('article')">文章</button>
</div>
<router-view></router-view>
</div>
var vm = new Vue({
el: '#app',
router,
data() {
return {}
},
methods: {
jump(name) {
this.$router.push(name)
}
}
})
对象格式的参数
// 字符串形式的参数
router.push('home')
// 通过路径描述地址
router.push({ path: 'home' })
// 通过命名的路由描述地址
router.push({ name: 'user' }})
当以对象形式传递参数的时候,还可以有一些其他属性,例如查询参数 params、query。
2.5.3 router.replace
跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是替换掉当前的 history 记录。
<div id="app">
<div>
<button @click="jump('index')">首页</button>
<button @click="jump('article')">文章</button>
</div>
<router-view></router-view>
</div>
var vm = new Vue({
el: '#app',
router,
data() {
return {}
},
methods: {
jump(name) {
this.$router.replace(name)
}
}
})
2.5.4 router.go
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步。
// 在浏览器记录中前进一步
router.go(1)
// 后退一步记录
router.go(-1)
// 前进 3 步记录
router.go(3)
// 如果 history 记录不够用,路由将不会进行跳转
router.go(-100)
router.go(100)
<div id="app">
<div>
<router-link to="index">首页</router-link>
<router-link to="article">文章</router-link>
</div>
<button @click="go(1)">前进一步</button>
<button @click="go(-1)">后路一步</button>
<button @click="go(3)">前进三步</button>
<button @click="go(-3)">后路三步</button>
<router-view></router-view>
</div>
var vm = new Vue({
el: '#app',
router,
data() {
return {}
},
methods: {
go(n) {
this.$router.go(n)
}
}
})
2.5.5 <router-link>
跳转命名路由
实际上 router-link 的 to 属性可以接收一个对象。
<router-link :to="{path: '/index'}">首页</router-link>
<router-link to="/article">文章</router-link>
除了通过 path 可以链接到路由外,还可以通过路由 name 实现链接跳转:
<div id="app">
<div>
<router-link :to="{name: 'index'}">首页</router-link>
<router-link :to="{name: 'article'}">文章</router-link>
</div>
<router-view></router-view>
</div>
3.5.6 编程式导航跳转命名路由
<div id="app">
<div>
<button @click="jump('index')">首页</button>
<button @click="jump('article')">文章</button>
</div>
<router-view></router-view>
const routes = [
{ path: '/index', name: 'index', component: Index },
{ path: '/article', name: 'article' , component: Article }
]
const router = new VueRouter({
routes: routes
})
var vm = new Vue({
el: '#app',
router,
data() {
return {}
},
methods: {
jump(name) {
this.$router.push({
name: name
})
}
}
})
2.6 路由重定向和路由别名
路由别名和重定向在项目中经常使用。
2.6.1 路由重定向
重定向也是通过 routes 配置来完成,可以配置路由重定向到具体路由地址、具名路由或者动态返回重定向目标。
重定向到路由地址
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面;
通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
var router = new VueRouter({
routes: [
// 其中,path 表示需要被重定向的原地址,redirect 表示将要被重定向到的新地址
{path:'/', redirect: '/user'},
{path:'/user',component: User},
{path:'/register',component: Register}
]
})
重定向到具名路由
通过属性 redirect 重定向到具名路由:
const routes = [
{path:'/',redirect:{name:'user'}},
{path:'/user',component:User,name:'user'},
{path:'/register',
component:Register,
children:[
{path:'/registerTab1',component:Tab1},
{path:'/registerTab2',component:Tab2}
]
}
]
动态返回重定向目标
属性 redirect 可以接收一个方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})
2.6.2 路由别名
当用户访问 /a 时,URL 将会被替换成 /b,然后匹配路由为 /b。
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
<div id="app">
<div>
<router-link to="/index">首页</router-link>
<router-link to="/article">文章</router-link>
</div>
<router-view></router-view>
</div>
const routes = [
{ path: '/index', component: Index, alias: '/' },
{ path: '/article', component: Article }
]
const router = new VueRouter({
routes: routes
})
var vm = new Vue({
el: '#app',
router: router,
data() {
return {}
}
})
2.7 VueRouter 路由传参
通常我们有两种方式来传递参数:
(1) 在 Vue Router 中用query传递参数,query是拼接在url后面的参数,不是路由的一部分,比如/page/detail?id=123
。
(2)在 Vue Router 中用params传递参数,params要拼接在url里面,params是路由的一个部分,比如/page/detail/123
。如果这个路由有params参数,但是在跳转的时候没有传递这个参数,会导致跳转失败或者页面没有内容。
2.7.1 params 传参
params方法传参的时候,要在路由后面加参数名占位;
路由配置
const router = new VueRouter({
routes: [
{
path: "/home",
component: Home,
name: "Home",
children: [
// 动态路径参数以冒号 ":" 开头
{ path: "page1/:id", component: Page1, name: "Page1" }
]
}
]
});
跳转方法
// 方法一:强制刷新参数会丢失
this.$router.push({ name: "Page1", params: { id: 123 } });
// 会跳转到 /home/page1/123
//方法二:强制刷新参数不会丢失
this.$router.push('/home/page/'+id)
//方法三: 强制刷新参数会丢失
<router-link :to="{ name: 'Page1', params: {id: 1234}}">goto Page1</router-link>
<!-- 点击会跳转到 /home/page1/1234 -->
获取参数的方式
<!-- Page1.vue -->
<template>
<!-- $route 可直接注入到模板 -->
<div>{{ $route.params.id }}</div>
</template>
2.7.2 query传参
query 的传参模式,不需要修改路由配置,只需要在导航的时候传参:
配置路由
const router = new VueRouter({
routes: [
{
path: "/user",
component: User,
name: "users",
}
]
});
跳转方法
// 方法一:
this.$router.push({ name: "users", query: { id: 123 } });
// 会跳转到 /user?id=123
// 方法二:
this.$router.push({path:'/user',query:{id:123}})
//方法三:
this.$router.push('/user?id='+id)
//方法四:
<router-link :to="{ name: 'users', query: {id: 1234}}">goto Page2</router-link>
//方法五:
<router-link :to="{ name: '/user', query: {id: 1234}}">goto Page2</router-link>
<!-- 点击会跳转到 /user?id=1234 -->
参数的使用
<!-- Page2.vue -->
<template>
<!-- $route 可直接注入到模板 -->
<div>{{ $route.query.id }}</div>
</template>
传递的参数是对象或者数组
如果query方式传递的是对象或者数组,在地址栏中会被强制转换成[object Object],刷新后页面获取不到对象。
我们需要通过JSON.stringify()方法将参数转换为字符串,在获取参数时通过JSON.parse转换成对象。
let parObj = JSON.stringify(obj)
// 路由跳转
this.$router.push({
path:'/detail',
query:{
obj:parObj
}
})
// 详情页获取参数
JSON.parse(this.$route.query.obj)
注意:这样虽然可以传对象,但是如果数据多的话地址栏会很长(不太推荐)。
使用props配合组件路由解耦
如果 props
被设置为 true,route.params
将会被设置为组件属性。
// 路由配置
{
path:'/detail/:id',
name:'detail',
component:Detail,
props:true // 如果props设置为true,$route.params将被设置为组件属性
}
// 路由跳转
this.$router.push({
path:`/detail/${id}`
})
// 详情页获取参数
export default {
props:['id'], // 将路由中传递的参数id解耦到组件的props属性上
mounted(){
console.log("id",this.id);
}
}
// 路由配置
{
path:'/detail',
name:'detail',
component:Detail,
props:true // 如果props设置为true,$route.params将被设置为组件属性
}
// 路由跳转
this.$router.push({
name:'detail',
params:{
order:{
id:'123456789',
name:'商品名称'
}
}
})
// 详情页获取参数
export default {
props:['order'], // 将路由中传递的参数order解耦到组件的props属性上
mounted(){
console.log("order",this.order);
}
}
此外,数据量比较大的参数,可以使用sessionStorage或localStorage来进行存储参数来解决页面刷新参数丢失的问题,具体结合实际项目即可。
2.8 监听路由
2.8.1 方法一watch监听
这个时候,我们可以通过watch(监测变化)$route
对象,来对路由参数
的变化作出响应:
<template>
<div>
<div>Detail</div>
<div>{{$route.query.id ? '修改' : '新建'}}</div>
<div>name: <input v-model="detail.name" /></div>
<div>text: <input v-model="detail.text" /></div>
</div>
</template>
<script>
// 下面是 Vue 组件
export default {
data() {
return {
detail: {
name: "",
text: ""
}
};
},
watch: {
$route(to, from) {
// 对路由变化作出响应,更新参数
this.updateDetail();
}
},
methods: {
updateDetail() {
const id = this.$route.query.id;
if (id) {
// 传入 id 则意味着修改,需要获取并录入原先内容
this.detail = {
name: `name-${id}`,
text: `text-${id}`
};
} else {
// 未传入 id 则意味着新建,需要重置原有内容
this.detail = {
name: "",
text: ""
};
}
}
}
};
</script>
我们可以通过 watch
$route
,在每次路由更新之后,重新获取 id 然后更新对应的内容。但上面这种做法依然存在问题,如果我们的路由从detail?id=123
变成了detail?id=123&test=hahaha
,$route
会出发侦听器,但是我们的 id 其实并没有变更,而这个时候由于重新获取内容,会覆盖掉我们正在编辑的内容。为了避免这种情况,我们可以通过参数(to, from)来检测是否不一致:
export default {
watch: {
$route(to, from) {
// 对路由变化作出响应
// 只有 id 值变更的时候,才进行更新
if (to.query.id !== from.query.id) {
this.updateDetail();
}
}
}
};
2.8.2 方法组件内守卫beforeRouteUpdate
在当前路由改变时,并且该组件被复用时调用。
- 对于一个带有动态参数的路径/foo/:id,在/foo/1和foo/2之间跳转的时候,组件实例会被复用,该守卫会被调用
- 当前路由query变更时,该守卫会被调用。
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
2.9 导航守卫
导航守卫就是路由跳转过程中的一些钩子函数,
全局路由一共分为三类:全局守卫、路由独享的守卫、组件内的守卫。
2.9.1 全局守卫
所谓全局守卫可以理解为只要触发路由跳转就会触发这些钩子函数。全局守卫的钩子函数的执行顺序:beforeEach(全局前置守卫)、beforeResolve(全局解析守卫)、afterEach(全局后置守卫)。
router.beforeEach(全局前置守卫)
在路由跳转前触发,参数包括to,from,next,这个钩子函数主要用于登录验证。
回调函数中的三个参数: to进入到哪个路由、from:从哪个路由离开,next函数决定是否展示你要看到的路由页面。
在main.js中,有一个路由实例化对象router,在main.js中设置守卫是全局守卫。
router.beforeEach((to, from, next) => { //全局全局前置守卫
if(to.name != 'login'){ //如果不是登录页面
if(ISLOGIN)next() //已登录就执行跳转
else next({name:'login'}) //否则跳转到登录页面
}else{ //如果是登录页面
if(ISLOGIN)next({name:'/'}) //已登录就跳转到首页
else next() //否则正常进入登录页面
}
})
守卫方法的三个参数
每个守卫方法接收三个参数:
to: Route
: 即将要进入的目标 路由对象from:Route
:当前导航正要离开的路由next:Function
:决定是否展示你要看到的路由页面。
(1)next()
:进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed(确认的)。
(2)next(false)
:中断当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址hi重置到from路由对应的地址。
(3)next('/')
或者next({path:'/'})
:跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向next传递任意位置对象,且允许设置诸如replace:true
、name:'home'
之类的选项以及任何用在router-link
的to prop
或者router.push
中的选项
(4)next(error)
:(2.4.0+)如果传入next的参数是一个Error实例,则导航会被终止且该错误会被传递给router.onError()注册过的回调。
**确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。**这里有一个在用户未能验证身份时重定向到 /login 的示例:
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
router.beforeResolve(全局解析守卫)
路由跳转前触发,参数也是to,from,next三个,它与beforeEach的却别在于:在跳转被确认之前,同时在所有组件内守卫和异步路由组件都被解析之后,解析守卫才调用。也就是在beforeEach和beforeEnterRouter之后,afterEach之前调用。
router.afterEach(全局后置钩子)
它是在路由跳转完成后触发,参数包括to和from,没有next参数。它发生在beforeEach和beforeResolve之后。
//设置路由跳转后,返回页面顶部
vueRouter.afterEach((to,from,next) => {
window.scrollTo(0,0);
});
2.9.2 路由独享守卫
独享守卫只有一种:beforeEnter。永和全局守卫一致,只是要将独享路由守卫写进一个路由对象中。
如果都设置则在beforeEach之后紧随执行,参数to、from、next。
该守卫只在其他路由跳转至配置有beforeEnter路由表信息时才生效。
router配置文件内容:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
2.9.3 组件内守卫
组件内守卫是跳转到这个组件时要执行的钩子函数,执行顺序分别为breforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。
三者分别对应:进入该路由时执行,该路由中参数改变时执行,离开该路由时执行。
beforeRouteEnter
路由进入之前调用,参数包括to、from、next。该钩子函数在全局前置守卫(beforeEach)和全局独享守卫beforeEnter之后,全局解析守卫beforeResolve和全局防守守卫afterEach之前调用。
注意在该钩子函数内获取不到组件的实例,也就是访问this为undefined,也就是它在beforeCreate声明周期函数之前触发。
**在这个钩子函数中,可以通过传一个回调next来访问组件实例。**在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数,可以在这个守卫中请求服务端获取数据,当成功获取并能介入路由时,调用next并在回调中通过vm访问组件实例并进行赋值等操作(next函数的调用发再生mounted之后:为了确保能对组件实例的完整访问。)
beforeRouteEnter (to, from, next) {
// 这里还无法访问到组件实例,this === undefined
next( vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteUpdate
在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例,参数包括to、from、next。
- 对于一个带有动态参数的路径/foo/:id,在/foo/1和foo/2之间跳转的时候,组件实例会被复用,该守卫会被调用
- 当前路由query变更时,该守卫会被调用。
beforeRouteLeave
导航离开该组件的对应路由时调用,可以访问组件实例this,参数包括to,from,next。
2.9.4 路由跳转流程
- 1、导航被触发。
- 2、在失活的组件里调用 beforeRouteLeave 守卫。
- 3、调用全局的 beforeEach 守卫。
- 4、在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 5、在路由配置里调用 beforeEnter。
- 6、解析异步路由组件。
- 7、在被激活的组件里调用 beforeRouteEnter。
- 8、调用全局的 beforeResolve 守卫 (2.5+)。
- 9、导航被确认。
- 10、调用全局的 afterEach 钩子。
- 11、触发 DOM 更新。
- 12、用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
2.10 路由懒加载
2.10.1 为什么需要使用路由懒加载
vue & webpack 的开发模式如果网站内容特别多,会导致 SPA 打出来的包的大小非常的大,经常超过 1M,因此一般会使用 webpack 的 code spliting 把代码分割成不同的 chunk。
按需加载 vue 支持异步组件的方式,因此 vue-router 提供了路由懒加载,可以在路由切换之后再去请求相关路由组件的资源,从而在某种程度上减少 bundle 的大小。
这样可以给客户更好的客户体验,首屏组件加载速度更快一些,解决白屏问题。
2.10.2 定义
路由懒加载简单来说就是按需加载或者延迟加载,即在需要的时候进行加载。
2.10.3 使用
Vue Router 提供了很简单的配置方式,来允许我们把不同路由对应的组件分割成不同的代码块。当对应的路由被访问的时候,Vue Router 才会加载对应组件,这样就能大大减小首页的代码包大小,加快加载速度。使用方式是:
(1) 将异步组件定义为返回一个 Promise 的工厂函数,该函数返回的 Promise 需要 resolve 组件本身。
(2) 使用动态import语法来定义代码分块点(依赖了 Webpack 的代码分割功能)。
ES 提出的import方法(------最常用------)
// 不会被打包到主包中,当匹配到对应的路由时候,才会被加载
const Page2 = () => import("./Page2.vue");
如果我们需要把几个组件都打包到一起,使用相同的webpackChunkName就可以实现:
const Page1 = () => import(/* webpackChunkName: "page" */ "./Page1.vue");
const Page2 = () => import(/* webpackChunkName: "page" */ "./Page2.vue");
2.11 路由元信息
定义路由的时候可以配置 meta 字段:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
那么如何访问这个 meta 字段呢?
我们称每个路由对象为路由记录,路由记录可以嵌套。所以到我们匹配到一个路有的时候他有可能有多条路由记录。路由记录会暴露在对应路由对象上,我们可以通过
r
o
u
t
e
.
m
a
t
c
h
e
d
获
取
到
当
前
路
由
所
有
的
路
由
记
录
,
route.matched获取到当前路由所有的路由记录,
route.matched获取到当前路由所有的路由记录,route.matched[n].meta可以获取其中一个路由记录的meta字段。
2.11.1 vue-router中的元信息meta的妙用
{
path:"/test",
name:"test",
component:()=>import("@/components/test"),
meta:{
title:"测试页面", //配置title
keepAlive: true //是否缓存
}
}
1、配置此路由的标题title
//main.js中的代码
router.beforeEach((to,from,next)=>{
if(to.meta.title){
document.title=to.meta.title
}
next()
})
2、配置组件是否需要缓存
<!-- app.vue中的代码 -->
<!-- 需要被缓存的路由入口 -->
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要被缓存的路由入口 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
3 路由实现
前端路由的实现,一般包括两种模式: Hash 模式和History模式。
3.1 Hash和History模式的区别
不管那种模式,都是客户端路由的实现方式,也就是当路径发生变化的时候,不会像服务器发送请求,使用js监视路径的变化,根据不同的地址渲染不同的内容。如果需要服务端内容的话,需要发送Ajax请求来获取。
3.1.1 表现形式的区别
- Hash模式
https://music.163.com/#/playlist?id=4343424324
hash链接中带有#号,#号后面的内容作为路由地址,可以通过?携带url参数。- History模式
https://music.163.com/playlist/4343424324
history模式需要服务器端进行额外的配置支持。
3.1.2 原理的区别
参考下面
3.2 History 模式
History 的路由模式,依赖了一个关键的属性window.history
。
window.history
是一个只读属性,用来获取 History 对象的引用。History 对象提供了操作浏览器会话历史(浏览器地址栏中访问的页面,以及当前页面中通过框架加载的页面)的接口,使用window.history
我们可以实现以下与路由相关的重要能力:
(1) 在 history 中跳转。
使用window.history.back()
、window.history.forward()
和window.history.go()
方法来完成在用户历史记录中向后和向前的跳转。
(2) 添加和修改历史记录中的条目。
HTML5 引入了history.pushState()
和history.replaceState()
方法,它们分别可以添加和修改历史记录条目。这两个 API 都会操作浏览器的历史栈,而不会引起页面的刷新。区别在于,pushState()
会增加一条新的历史记录,而replaceState()
则会替换当前的历史记录:
/**
* parameters
* @state {object} 状态对象 state 是一个 JavaScript 对象,一般是JSON格式的对象字面量
* @title {string} 可以理解为 document.title,在这里是作为新页面传入参数的
* @url {string} 该参数定义了增加或改变的历史 URL 记录,可以是相对路径或者绝对路径,url的具体格式可以自定
*/
history.pushState(state, title, url); // 向浏览器历史栈中增加一条记录
history.replaceState(state, title, url); // 替换历史栈中的当前记录
(3) 监听 popstate 事件。
当同一个页面在历史记录间切换时,就会产生popstate
事件,popstate
事件会被传递给window对象,所以页面路由切换通常会与window.onpopstate
配合使用。
上面介绍的history.pushState()
或者history.replaceState()
调用不会触发popstate
事件,popstate
事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在 JavaScript 中调用history.back()、history.forward()、history.go()
方法)。所以我们可以结合popstate事件
、pushState()
和replaceState()
来完成完整的路由监听和修改能力。
如果当前处于激活状态的历史记录条目是由history.pushState()
方法创建,或者由history.replaceState()
方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝。我们来看看示例:
// 假如当前网页地址为http://example.com/example.html
window.onpopstate = function(event) {
alert(
"location: " + document.location + ", state: " + JSON.stringify(event.state)
);
};
//绑定事件处理函数
//添加并激活一个历史记录条目 http://example.com/example.html?page=1,条目索引为1
history.pushState({ page: 1 }, "title 1", "?page=1");
//添加并激活一个历史记录条目 http://example.com/example.html?page=2,条目索引为2
history.pushState({ page: 2 }, "title 2", "?page=2");
//修改当前激活的历史记录条目 http://ex..?page=2 变为 http://ex..?page=3,条目索引为3
history.replaceState({ page: 3 }, "title 3", "?page=3");
history.back(); // 弹出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // 弹出 "location: http://example.com/example.html, state: null
history.go(2); // 弹出 "location: http://example.com/example.html?page=3, state: {"page":3}
3.3 Hash模式
Hash 模式主要依赖 Location 对象的 hash 属性(location.hash)和hashchange事件,我们来分别看一下。
(1) Location 对象。
window.location
用来获取 Location
对象的引用。Location 对象存储在 Window 对象的 Location 属性中,表示当前 Web 地址。
Location属性 | 描述 | 示例:https://www.test.com/en-US/search?q=URL#search-results |
---|---|---|
hash | 设置或返回从井号(#)开始的 URL(锚) | #search-results |
host | 设置或返回主机名和当前 URL 的端口号 | www.test.com |
hostname | 设置或返回当前 URL 的主机名 | www.test.com |
href | 设置或返回完整的 URL | https://www.test.com/en-US/search?q=URL#search-results |
pathname | 设置或返回当前 URL 的路径部分 | /en-US/search |
port | 设置或返回当前 URL 的端口号 | 默认 80 端口 |
protocol | 设置或返回当前 URL 的协议 | https: |
search | 设置或返回从问号(?)开始的 URL(查询部分) ?q=URL | ?q=URL |
location.hash的设置和获取,并不会造成页面重新加载,利用这一点,我们可以记录页面关键信息的同时,提升页面体验。
(2) hashchange 事件。
当一个窗口的 hash 改变时就会触发hashchange事件。hashchange事件对象有两个属性,newURL为当前页面新的 URL,oldURL为当前页面旧的 URL。
Hash 模式通过window.onhashchange监听基于 hash 的路由变化,来进行页面更新处理的。部分浏览器不支持onhashchange事件,我们可以自行使用定时器检测和触发的方式来进行兼容:
(function(window) {
// 如果浏览器原生支持该事件,则退出
if ("onhashchange" in window.document.body) {
return;
}
var location = window.location,
oldURL = location.href,
oldHash = location.hash;
// 每隔100ms检测一下location.hash是否发生变化
setInterval(function() {
var newURL = location.href,
newHash = location.hash;
// 如果hash发生了变化,且绑定了处理函数...
if (newHash != oldHash && typeof window.onhashchange === "function") {
// 执行事件触发
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});
oldURL = newURL;
oldHash = newHash;
}
}, 100);
})(window);
3.4 路由的实现原理
3.4.1 路由实现步骤
一般来说,路由都是通过 History API、Location API 和相关的事件监听实现。我们以 Hash 模式为例,如果要实现路由变化的时候加载相应的内容,步骤大概分为三步:
(1)设置监听器,监听popstate或者hashchange事件。
(2)通过 hash(location.href.hash)获取当前的路由位置。
(3)根据当前匹配路径,判断后加载对应模块。
3.4.2 Vue Router 实现
Vue Router 甚至兼容了 Node.js 服务端的情况,它提供的路由模式包括三种:
路由模式 | 说明 | 示例 |
---|---|---|
hash | 使用 URL hash 值来作路由(支持所有浏览器,包括不支持 HTML5 History Api 的浏览器) | a.com/#/pageone |
history | 充分利用history.pushState API 来完成 URL 跳转而无须重新加载页面 | a.com/pageone |
abstract | 支持所有 JavaScript 运行环境,如 Node.js 服务器端(在 Node.js 端会自动设置该模式为默认值) | |
如果发现没有浏览器的 API,路由会自动强制进入这个模式 | - |
需要注意的地方是,History 模式需要依赖 HTML5 History API(IE10 以上)和服务器配置。**你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个index.html页面,这个页面就是你 app 依赖的页面。**当浏览器接收到index.html这个页面之后,再去判断路由地址,然后根据路由地址/login去加载login这个组件,这是它执行的一个过程。所以如果觉得 hash 片段可以接受(有些人会觉得太丑了无法接受),可以优先选择 Hash 模式来进行开发,简单方便。
vue-cli中自带的服务器,已经做了history的配置。
我们看看 Vue Router 中的 History 对象:
// HTML5 History
export class HTML5History extends History {
constructor(router: Router, base?: string) {
super(router, base);
const expectScroll = router.options.scrollBehavior;
const supportsScroll = supportsPushState && expectScroll;
if (supportsScroll) {
setupScroll();
}
const initLocation = getLocation(this.base);
// 添加事件监听
window.addEventListener("popstate", e => {
const current = this.current; // History路由因为异步防护,不会更新
// 首先避免在某些浏览器中调度第一个`popstate`事件
const location = getLocation(this.base);
if (this.current === START && location === initLocation) {
return;
}
// 该方法进行路由的更新、对应钩子逻辑的执行、处理完毕的回调
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true);
}
});
});
}
// 前进到某个位置
go(n: number) {
window.history.go(n);
}
// 添加到路由
push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this;
// 前置逻辑执行
this.transitionTo(
location,
route => {
// 添加到历史记录中
pushState(cleanPath(this.base + route.fullPath));
handleScroll(this.router, route, fromRoute, false);
// 回调
onComplete && onComplete(route);
},
onAbort
);
}
// 更新当前路由
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this;
this.transitionTo(
location,
route => {
replaceState(cleanPath(this.base + route.fullPath));
handleScroll(this.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
}
// 确认是否某个路由
ensureURL(push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath);
push ? pushState(current) : replaceState(current);
}
}
// 获取当前路由
getCurrentLocation(): string {
return getLocation(this.base);
}
}
我们再来看看 Hash 对象:
export class HashHistory extends History {
constructor(router: Router, base: ?string, fallback: boolean) {
super(router, base);
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return;
}
ensureSlash();
} // 以避免过早地触发hashchange侦听器
// 这会延迟到应用程序安装完毕
setupListeners() {
const router = this.router;
const expectScroll = router.options.scrollBehavior;
const supportsScroll = supportsPushState && expectScroll;
if (supportsScroll) {
setupScroll();
}
// 添加事件监听
// 优先使用 popstate,同时使用 hashchange 兜底
window.addEventListener(
supportsPushState ? "popstate" : "hashchange",
() => {
const current = this.current;
if (!ensureSlash()) {
return;
}
// 该方法进行路由的更新、对应钩子逻辑的执行、处理完毕的回调
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true);
}
if (!supportsPushState) {
replaceHash(route.fullPath);
}
});
}
);
}
// 前进到某个位置
go(n: number) {
window.history.go(n);
}
// 添加到路由
push(location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this;
this.transitionTo(
location,
route => {
pushHash(route.fullPath);
handleScroll(this.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
}
// 更新当前路由
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this;
this.transitionTo(
location,
route => {
replaceHash(route.fullPath);
handleScroll(this.router, route, fromRoute, false);
onComplete && onComplete(route);
},
onAbort
);
}
// 确认是否某个路由
ensureURL(push?: boolean) {
const current = this.current.fullPath;
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current);
}
}
// 获取当前路由
getCurrentLocation() {
return getHash();
}
}
3.5 Vue Router的模拟实现
实现思路:
(1)创建LVueRouter插件,静态方法install
- 判断当前插件是否已经被加载
- 当Vue加载的时候把换入的router对象挂载到Vue实例上(注意:只执行一次)
(2) 创建LVueRouter类 - 初始化options、routeMap、app(简化操作,创建Vue实例作为响应式数据记录当前路径)
- initRouteMap()遍历所有的路由信息,把组件和路由的映射记录到routeMap对象中
- 创建router-link和router-view组件
- 当路径发生改变的时候通过当前路径在routerMap对象中找到对应的组件,渲染router-view
创建LVueRouter插件
export default class VueRouter {
static install (Vue) {
static install (Vue) { // 如果插件已经安装直接返回
if (VueRouter.install.installed && _Vue === Vue) return
VueRouter.install.installed = true
_Vue = Vue
Vue.mixin({
beforeCreate () {
// 判断 router 对象是否已经挂载了 Vue 实例上
if (this.$options.router) {
// 把 router 对象注入到 Vue 实例上
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
实现 LVueRouter 类 - 构造函数
constructor (options) {
this.options = options
// 记录路径和对应的组件
this.routeMap = {}
this.app = new _Vue({
data:{
//当前默认路径
current:'/'
}
})
}
实现 LVueRouter 类 - initRouteMap()
initRouteMap () {
// routes => [{ name: '', path: '', component: }]
// 遍历所有的路由信息,记录路径和组件的映射
this.options.routes.forEach(route => {
// 记录路径和组件的映射关系
this.routeMap[route.path] = route.component
})
}
实现 LVueRouter 类 - 注册事件
initEvents(){
//当路径变化之后,重新获取当前路径并记录到 current
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange () {
this.app.current = window.location.hash.substr(1) || '/'
}
实现 LVueRouter 类 - router-link 和 router-view 组件
initComponents () {
_Vue.component('RouterLink', {
props:{
to:String
},
//需要带编译器版本的 Vue.js
//template: "<a :href='\"#\" + to'><slot></slot></a>"
//使用运行时版本的 Vue.js
render(h) {
return h('a',{
attrs:{
href:'#' + this.to
}
},[this.$slots.default])
}
})
const self = this
_Vue.component('RouterView',{
render(h) {
//根据当前路径找到对应的组件,注意 this 的问题
const component = self.routeMap[self.app.current]
return h(component)
}
})
}
注意:
vue-cli创建的项目默认使用的运行时版本的Vue.js
如果想要切换成编译器版本的Vue.js,需要修改vue-cli配置
项目根目录创建vue.config.js文件,添加runtimeCompiler
module.exports = {
runtimeCompiler : true
}
实现LVueRouter类–init
init() {
this.initRouteMap()
this.initEvents()
this.initComponents()
}
//插件的install方法中调用init()初始化
if(this.$options.router){
_Vue.prototype.$router = this.$option.router
//初始化插件的时候,调用init
this.$options.router.init()
}