前言
最近有朋友正在准备面试,刚到公司就收到他的消息,接连几个 vue-router 也把我给问懵了 😶
这大早上的人都还没醒就已经梦游到工位上了,眼睛是迷离的,脑子也是糊的,怎么会有人一大早就开始学习的呢?
反正我是不行的😂,这一开头还那么劲爆,直接给我来了几道面试题,求求了 放过我吧😭 让我摸会儿鱼吧
盯着屏幕上的题过了好一会。。。
我猛然发觉不正是我之前整理过的知识点嘛 !!!于是我就这么把库存发了出来。
朋友啊 你自己看去吧,我得赶紧码别的文去了,不然 六月更文计划 要接不上了😂。
这已经我本月更文的第15篇啦,已经赛程即将过半,参加更文的友友们加油啊,坚持就是胜利 💪!!!
好了不说了赶紧开始今天的学习吧😜
前端路由
前端路由是后来发展到SPA(单页应用)时才出现的概念。 SPA 就是一个WEB项目只有一个 HTML 页面,一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转。
前端路由在SPA项目中是必不可少的,页面的跳转、刷新都与路由有关,通过不同的url显示相应的页面。
优点:前后端的彻底分离,不刷新页面,用户体验较好,页面持久性较好。
后端路由
当在地址栏切换不同的url时,都会向服务器发送一个请求,服务器接收并响应这个请求,在服务端拼接好html文件返回给页面来展示。
优点:减轻了前端的压力,html都由后端拼接;
缺点:依赖于网络,网速慢,用户体验很差,项目比较庞大时,服务器端压力较大,
不能在地址栏输入指定的url访问相应的模块,前后端不分离。
路由模式
前端路由实现起来其实很简单,本质是监听 URL 的变化,然后匹配路由规则,在不刷新的情况下显示相应的页面。
hash模式(对应HashHistory)
- 把前端路由的路径用井号 # 拼接在真实 url 后面的模式,但是会覆盖锚点定位元素的功能,通过监听 URL 的哈希部分变化,相应地更新页面的内容。
- 前端路由的处理完全在客户端进行,在路由发生变化时,只会改变 URL 中的哈希部分(井号 # 后面的路径),且不会向服务器发送新的请求,而是触发 onhashchange 事件。
- hash 只有#符号之前的内容才会包含在请求中被发送到后端,如果 nginx 没有匹配得到当前的 url 也没关系。hash 永远不会提交到 server 端。
- hash值的改变,都会在浏览器的访问历史中增加一个记录,所以可以通过浏览器的回退、前进按钮控制hash的切换。
- hash 路由不会造成 404 页面的问题,因为所有路由信息都在客户端进行解析和处理,服务器只负责提供应用的初始 HTML 页面和静态资源,不需要关心路由的匹配问题。
// onhashchage事件,可以在window对象上监听这个事件
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL)
let hash = location.hash.slice(1)
}
- 通过location.hash修改hash值,触发更新。
- 通过监听hashchange事件监听浏览器前进或者后退,触发更新。
history模式 (对应HTML5History)
- 是 html5 新推出的功能,比 Hash url 更美观
- 在 history 模式下浏览器在刷新页面时,会按照路径发送真实的资源请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。
- 在使用 history 模式时,需要通过服务端支持允许地址可访问,如果没有设置,就很容易导致出现 404 的局面。
- 改变url: history 提供了 pushState 和 replaceState 两个方法来记录路由状态,这两个方法只改变 URL 不会引起页面刷新。
- 监听url变化:通过 onpopstate 事件监听history变化,在点击浏览器的前进或者后退功能时触发,在onpopstate 事件中根据状态信息加载对应的页面内容。
history.replaceState({}, null, '/b') // 替换路由
history.pushState({}, null, '/a') // 路由压栈,记录浏览器的历史栈 不刷新页面
history.back() // 返回
history.forward() // 前进
history.go(-2) // 后退2次
history.pushState 修改浏览器地址,而页面的加载是通过 onpopstate 事件监听实现,加载对应的页面内容,完成页面更新。
// 页面加载完毕 first.html
history.pushState({page: 1}, "", "first.html");
window.onpopstate = function(event) {
// 根据当前 URL 加载对应页面
loadPage(location.pathname);
};
// 点击跳转到 second.html
history.pushState({page: 2}, "", "second.html");
function loadPage(url) {
// 加载 url 对应页面内容
// 渲染页面
}
onpopstate 事件是浏览器历史导航的核心事件,它标识了页面状态的变化时机。通过监听这个时机,根据最新的状态信息更新页面
当使用 history.pushState() 或 history.replaceState() 方法修改浏览器的历史记录时,不会直接触发 onpopstate 事件。
但是,可以在调用这些方法时将数据存储在历史记录条目的状态对象中, onpopstate 事件在处理程序中访问该状态对象。这样,就可以在不触发 onpopstate 事件的情况下更新页面内容,并获取到相应的状态值。
history 模式下 404 页面的处理
在 history 模式下,浏览器会向服务器发起请求,服务器根据请求的路径进行匹配:
如果服务器无法找到与请求路径匹配的资源或路由处理器,服务器可以返回 /404 路由,跳转到项目中配置的 404 页面,指示该路径未找到。
对于使用历史路由模式的单页应用(SPA),通常会在服务器配置中添加一个通配符路由,将所有非静态资源的请求都重定向到主页或一个自定义的 404 页面,以保证在前端处理路由时不会出现真正的 404 错误页面。
在项目中配置对应的 404 页面:
export const publicRoutes = [
{
path: '/404',
component: () => import('src/views/404/index'),
},
]
vueRouter
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,允许你在 Vue 应用中构建单页面应用(SPA),并且提供了灵活的路由配置和导航功能。让用 Vue.js 构建单页应用变得轻而易举。功能包括:
- 路由映射:可以将 url 映射到 Vue组件,实现不同 url 对应不同的页面内容。
- 嵌套路由映射:可以在路由下定义子路由,实现更复杂的页面结构和嵌套组件的渲染。
- 动态路由:通过路由参数传递数据。你可以在路由配置中定义带有参数的路由路径,并通过 $route.params 获取传递的参数。
- 模块化、基于组件的路由配置:路由配置是基于组件的,每个路由都可以指定一个 Vue 组件作为其页面内容,将路由配置拆分为多个模块,在需要的地方引入。。
- 路由参数、查询、通配符:通过路由参数传递数据,实现页面间的数据传递和动态展示。
- 导航守卫:Vue Router 提供了全局的导航守卫和路由级别的导航守卫,可以在路由跳转前后执行一些操作,如验证用户权限、加载数据等。
- 展示由 Vue.js 的过渡系统提供的过渡效果:可以为路由组件添加过渡效果,使页面切换更加平滑和有动感。
- 细致的导航控制:可以通过编程式导航(通过 JavaScript 控制路由跳转)和声明式导航(通过 组件实现跳转)实现页面的跳转。
- 路由模式设置:Vue Router 支持两种路由模式:HTML5 history 模式或 hash 模式
- 可定制的滚动行为:当页面切换时,Vue Router 可以自动处理滚动位置。定制滚动行为,例如滚动到页面顶部或指定的元素位置。
- URL 的正确编码:Vue Router 会自动对 URL 进行正确的编码
路由组件
- **router-link:**通过 router-link 创建链接 其本质是
a
标签,这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码。 - **router-view:**router-view 将显示与 url 对应的组件。
$router
、$route
$route: 是当前路由信息对象,获取和当前路由有关的信息。 route 为属性是只读的,里面的属性是 immutable (不可变) 的,不过可以通过 watch 监听路由的变化。
fullPath: "" // 当前路由完整路径,包含查询参数和 hash 的完整路径
hash: "" // 当前路由的 hash 值 (锚点)
matched: [] // 包含当前路由的所有嵌套路径片段的路由记录
meta: {} // 路由文件中自赋值的meta信息
name: "" // 路由名称
params: {} // 一个 key/value 对象,包含了动态片段和全匹配片段就是一个空对象。
path: "" // 字符串,对应当前路由的路径
query: {} // 一个 key/value 对象,表示 URL 查询参数。跟随在路径后用'?'带的参数
$router 是 vueRouter 实例对象,是一个全局路由对象,通过 this.$router 访问路由器, 可以获取整个路由文件或使用路由提供的方法。
// 导航守卫
router.beforeEach((to, from, next) => {
/* 必须调用 `next` */
})
router.beforeResolve((to, from, next) => {
/* 必须调用 `next` */
})
router.afterEach((to, from) => {})
动态导航到新路由
router.push
router.replace
router.go
router.back
router.forward
routes 是 router 路由实例用来配置路由对象 可以使用路由懒加载(动态加载路由)的方式
- 把不同路由对应的组件分割成不同的代码块,当路由被访问时才去加载对应的组件 即为路由的懒加载,可以加快项目的加载速度,提高效率
const router = new VueRouter({
routes: [
{
path: '/home',
name: 'Home',
component:() = import('../views/home')
}
]
})
vueRouter的使用
页面中路由展示位置
<div id="app">
<!-- 添加路由 -->
<!-- 会被渲染为 <a href="#/home"></a> -->
<router-link to="/home">Home</router-link>
<router-link to="/login">Login</router-link>
<!-- 展示路由的内容 -->
<router-view></router-view>
</div>
路由模块 引入 vue-router,使用 Vue.use(VueRouter) 注册路由插件 定义路由数组,并将数组传入VueRouter 实例,并将实例暴露出去
import Vue from 'vue'
import VueRouter from 'vue-router'
import { hasVisitPermission, isWhiteList } from './permission'
// 注册路由组件
Vue.use(VueRouter)
// 创建路由: 每一个路由规则都是一个对象
const routers =[
// path 路由的地址
// component 路由的所展示的组件
{
path: '/',
// 当访问 '/'的时候 路由重定向 到新的地址 '/home'
redirect: '/home',
},
{
path: '/home',
component: home,
},
{
path: '/login',
component: login,
},
],
// 实例化 VueRouter 路由
const router = new VueRouter({
mode: 'history',
base: '/',
routers
})
// 路由守卫
router.beforeEach(async (to, from, next) => {
// 清除面包屑导航数据
store.commit('common/SET_BREAD_NAV', [])
// 是否白名单
if (isWhiteList(to)) {
next()
} else {
// 未登录,先登录
try {
if (!store.state.user.userInfo) {
await store.dispatch('user/getUserInfo')
}
// 登录后判断,是否有访问页面的权限
if (!hasVisitPermission(to, store.state.user.userInfo)) {
next({ path: '/404' })
} else {
next()
}
} catch (err) {
$error(err)
}
}
})
export default router
在 main.js 上挂载路由 将VueRouter实例引入到main.js,并注册到根Vue实例上
import router from './router'
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
动态路由
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。我们可以在 vueRrouter 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果。
- 动态路由的创建,主要是使用 path 属性过程中,使用动态路径参数,路径参数 用冒号 : 表示。
当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.query 的形式暴露出来。因此,我们可以通过更新 User 的模板来呈现当前的用户 ID:
const routes = [
{
path: '/user/:id'
name: 'User'
components: User
}
]
_vue-router _通过配置 _params _和 _query _来实现动态路由
params 传参
-
必须使用 命名路由 name 传值
-
参数不会显示在 url 上
-
浏览器强制刷新时传参会被清空
// 传递参数
this.$router.push({
name: Home,
params: {
number: 1 ,
code: '999'
}
})
// 接收参数
const p = this.$route.params
query 传参
- 可以用 name 也可以使用 path 传参
- 传递的参数会显示在 url 上
- 页面刷新是传参不会丢失
// 方式一:路由拼接
this.$router.push('/home?username=xixi&age=18')
// 方式二:name + query 传参
this.$router.push({
name: Home,
query: {
username: 'xixi',
age: 18
}
})
// 方式三:path + name 传参
this.$router.push({
path: '/home',
query: {
username: 'xixi',
age: 18
}
})
// 接收参数
const q = this.$route.query
keep-alive
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
keep-alive 可以设置以下props属性:
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
- max - 数字。最多可以缓存多少组件实例
在不缓存组件实例的情况下,每次切换都会重新 render,执行整个生命周期,每次切换时,重新 render,重新请求,必然不满足需求。 会消耗大量的性能
keep-alive 的基本使用
只是在进入当前路由的第一次render,来回切换不会重新执行生命周期,且能缓存router-view的数据。 通过 include 来判断是否匹配缓存的组件名称: 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配
<keep-alive>
<router-view></router-view>
</keep-alive>
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
路由配置 keepAlive
在路由中设置 keepAlive 属性判断是否需要缓存
{
path: 'list',
name: 'itemList', // 列表页
component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
compName: 'ItemList'
title: '列表页'
}
}
{
path: 'management/class_detail/:id/:activeIndex/:status',
name: 'class_detail',
meta: {
title: '开班详情',
keepAlive: true,
compName: 'ClassInfoDetail',
hideInMenu: true,
},
component: () => import('src/views/classManage/class_detail.vue'),
},
使用
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
keepAlive 对生命周期的影响
设置缓存后组件加载的生命周期会新增 actived 与 deactived
- 首次进入组件时也会触发 actived 钩子函数:beforeRouteEnter > beforeCreate > created> beforeMount > beforeRouteEnter 的 next 回调> mounted > activated > ... ... > beforeRouteLeave > deactivated
- 再次进入组件时直接获取actived的组件内容:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated
keep-alive 组件监听 include 及 exclude 的缓存规则,若发生变化则执行 pruneCache (遍历cache 的name判断是否需要缓存,否则将其剔除) 且 keep-alive 中没有 template,而是用了 render,在组件渲染的时候会自动执行 render 函数,
- 若命中缓存则直接从缓存中拿 vnode 的组件实例,
- 若未命中缓存且未被缓存过则将该组件存入缓存,
- 当缓存数量超出最大缓存数量时,删除缓存中的第一个组件。
动态路由缓存的的具体表现在:
- 由动态路由配置的路由只能缓存一份数据。
- keep-alive 动态路由只有第一个会有完整的生命周期,之后的路由只会触发 actived 和 deactivated这两个钩子。
- 一旦更改动态路由的某个路由数据,期所有同路由下的动态路由数据都会同步更新。
如何删除 keep-alive 中的缓存
vue2 中清除路由缓存
在组件内可以通过 this 获取 vuerouter 的缓存
vm.$vnode.parent.componentInstance.cache
或者通过 ref 获取 外级 dom
<template>
<el-container id="app-wrapper">
<Aside />
<el-container>
<el-header id="app-header" height="45px">
<Header @removeCacheRoute="removeCacheRoute" />
</el-header>
<!-- {{ includeViews }} -->
<el-main id="app-main">
<keep-alive :include="includeViews">
<router-view ref="routerViewRef" :key="key" />
</keep-alive>
</el-main>
</el-container>
</el-container>
</template>
<script>
import Aside from './components/Aside'
import Header from './components/Header'
import { mapGetters } from 'vuex'
export default {
name: 'Layout',
components: {
Aside,
Header,
},
data () {
return {
}
},
computed: {
...mapGetters(['cacheRoute', 'excludeRoute']),
includeViews () {
return this.cacheRoute.map(item => item.compName)
},
key () {
return this.$route.fullPath
},
},
methods: {
removeCacheRoute (fullPath) {
const cache = this.$refs.routerViewRef.$vnode.parent.componentInstance.cache
delete cache[fullPath]
},
},
}
</script>
路由守卫
导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
通俗来讲:路由守卫就是路由跳转过程中的一些生命周期函数(钩子函数),我们可以利用这些钩子函数帮我们实现一些需求。
路由守卫又具体分为 全局路由守卫、独享守卫 及 组件路由守卫。
全局路由守卫
- 全局前置守卫router.beforeEach
- 全局解析守卫:router.beforeResolve
- 全局后置守卫:router.afterEach
beforeEach(to,from, next)
在路由跳转前触发,参数包括to,from,next 三个,这个钩子作用主要是用于登录验证。
前置守卫也可以理解为一个路由拦截器,也就是说所有的路由在跳转前都要先被前置守卫拦截。
router.beforeEach(async (to, from, next) => {
// 清除面包屑导航数据
store.commit('common/SET_BREAD_NAV', [])
// 是否白名单
if (isWhiteList(to)) {
next()
} else {
// 未登录,先登录
try {
if (!store.state.user.userInfo) {
await store.dispatch('user/getUserInfo')
// 登录后判断,是否有角色, 无角色 到平台默认页
if (!store.state.user.userInfo.permissions || !store.state.user.userInfo.permissions.length) {
next({ path: '/noPermission' })
}
}
// 登录后判断,是否有访问页面的权限
if (!hasVisitPermission(to, store.state.user.userInfo)) {
next({ path: '/404' })
} else {
next()
}
} catch (err) {
$error(err)
}
}
})
beforeResolve(to,from, next)
在每次导航时都会触发,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。
即在 beforeEach 和 组件内 beforeRouteEnter 之后,afterEach之前调用。
router.beforeResolve 是获取数据或执行任何其他操作的理想位置
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
afterEach(to,from)
和beforeEach相反,他是在路由跳转完成后触发,参数包括to, from 由于此时路由已经完成跳转 所以不会再有next。
全局后置守卫对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from) => {
// 在路由完成跳转后执行,实现分析、更改页面标题、声明页面等辅助功能
sendToAnalytics(to.fullPath)
})
独享路由守卫
beforeEnter(to,from, next) 独享路由守卫可以直接在路由配置上定义,但是它只在进入路由时触发,不会在 params、query 或 hash 改变时触发。
const routes = [
{
path: '/users/:id',
component: UserDetails,
// 在路由配置中定义守卫
beforeEnter: (to, from,next) => {
next()
},
},
]
或是使用数组的方式传递给 beforeEnter ,有利于实现路由守卫的重用
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
组件路由守卫
在组件内使用的钩子函数,类似于组件的生命周期, 钩子函数执行的顺序包括
- beforeRouteEnter(to,from, next) -- 进入前
- beforeRouteUpdate(to,from, next) -- 路由变化时
- beforeRouteLeave(to,from, next) -- 离开后
组件内路由守卫的执行时机:
<template>
...
</template>
export default{
data(){
//...
},
// 在渲染该组件的对应路由被验证前调用
beforeRouteEnter (to, from, next) {
// 此时 不能获取组件实例 this
// 因为当守卫执行前,组件实例还没被创建
next((vm)=>{
// next 回调 在 组件 beforeMount 之后执行 此时组件实例已创建,
// 可以通过 vm 访问组件实例
console.log('A组件中的路由守卫==>> beforeRouteEnter 中next 回调 vm', vm)
)
},
// 可用于检测路由的变化
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用 此时组件已挂载完可以访问组件实例 `this`
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
console.log('组件中的路由守卫==>> beforeRouteUpdate')
next()
},
// 在导航离开渲染该组件的对应路由时调用
beforeRouteLeave (to, from, next) {
// 可以访问组件实例 `this`
console.log('A组件中的路由守卫==>> beforeRouteLeave')
next()
}
}
<style>
...
</style>
注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持 传递回调,因为没有必要了
路由守卫触发流程
页面加载时路由守卫触发顺序:
- 触发全局的路由守卫 beforeEach
- 组件在路由配置的独享路由 beforeEnter
- 进入组件中的 beforeRouteEnter,此时无法获取组件对象
- 触发全局解析守卫 beforeResolve
- 此时路由完成跳转 触发全局后置守卫 afterEach
- 组件的挂载 beforeCreate --> created --> beforeMount
- 路由守卫 beforeRouterEnter 中的 next回调, 此时能够获取到组件实例 vm
- 完成组件的挂载 mounted
当点击切换路由时: A页面跳转至B页面触发的生命周期及路由守卫顺序:
- 导航被触发进入其他路由。
- 在离开的路由组件中调用 beforeRouteLeave 。
- 调用全局的前置路由守卫 beforeEach 。
- 在重用的组件里调用 beforeRouteUpdate 守卫。
- 调用被激活组件的路由配置中调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件中调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫。
- 导航被确认。
- 调用全局后置路由 afterEach 钩子。
- 触发 DOM 更新,激活组件的创建及挂载 beforeCreate (新)-->created (新)-->beforeMount(新) 。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
- 失活组件的销毁 beforeDestory(旧)-->destoryed(旧)
- 激活组件的挂载 mounted(新)
路由守卫的触发顺序 beforeRouterLeave-->beforeEach-->beforeEnter-->beforeRouteEnter-->beforeResolve-->afterEach--> beforeCreate (新)-->created (新)-->beforeMount(新) -->beforeRouteEnter中的next回调 -->beforeDestory(旧)-->destoryed(旧)-->mounted(新)
当路由更新时:触发 beforeRouteUpdate
注意: 但凡涉及到有next参数的钩子,必须调用next() 才能继续往下执行下一个钩子,否则路由跳转等会停止。
vueRouter 实现原理
vueRouter 实现的原理就是 监听浏览器中 url 的 hash值变化,并切换对应的组件
1.路由注册
通过vue.use()安装vue-router插件,会执行install方法,并将Vue当做参数传入install方法 Vue.use(VueRouter) === VueRouter.install()
src/install.js
export function install (Vue) {
// 确保 install 调用一次
if (install.installed && _Vue === Vue) return
install.installed = true
// 把 Vue 赋值给全局变量
_Vue = Vue
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 为每个组件混入 beforeCreate 钩子
// 在 `beforeCreate` 钩子执行时 会初始化路由
Vue.mixin({
beforeCreate () {
// 判断组件是否存在 router 对象,该对象只在根组件上有
if (isDef(this.$options.router)) {
// 根路由设置为自己
this._routerRoot = this
// this.$options.router就是挂在根组件上的 VueRouter 实例
this._router = this.$options.router
// 执行VueRouter实例上的init方法,初始化路由
this._router.init(this)
// 很重要,为 _route 做了响应式处理
// 即访问vm._route时会先向dep收集依赖, 而修改_router 会触发组件渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 用于 router-view 层级判断
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
/* 在Vue的prototype上面绑定 $router,
这样可以在任意Vue对象中使用this.$router访问,同时经过Object.defineProperty,将 $router 代理到 Vue
访问this.$router 即访问this._routerRoot._router */
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
/* 同理,访问this.$route即访问this._routerRoot._route */
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 全局注册组件 router-link 和 router-view
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
}
- 使用 Vue.mixin 为每个组件混入 beforeCreate 钩子,全局混入添加组件选项 挂载 router 配置项
- 通过 defineReactive 为vue实例实现数据劫持 让_router能够及时响应页面更新
- 将 route 代理到 Vue 原型上
- 全局注册 router-view 及 router-link 组件
2. VueRouter 实例化
在安装插件后,对 VueRouter 进行实例化。
//用户定义的路由配置数组
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 3. Create the router
const router = new VueRouter({
mode: 'hash',
base: __dirname,
routes: [
{ path: '/', component: Home }, // all paths are defined without the hash.
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
})
VueRouter 构造函数
src/index.js
// VueRouter 的构造函数
constructor(options: RouterOptions = {}) {
// ...
// 路由匹配对象 -- 路由映射表
this.matcher = createMatcher(options.routes || [], this)
// 根据 mode 采取不同的路由方式
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
在实例化 vueRouter 的过程中 通过 createMatcher 创建路由匹配对象(路由映射表),并且根据 mode 来采取不同的路由方式。
3.创建路由匹配对象
src/create-matcher.js
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 创建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配 找到对应的路由
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
//...
}
return {
match,
addRoutes
}
}
createMatcher
函数的作用就是创建路由映射表,然后通过闭包的方式让 addRoutes
和 match
函数能够使用路由映射表的几个对象,最后返回一个 Matcher
对象。
在createMatcher中通过使用 createRouteMap() 根据用户配置的路由规则来创建对应的路由映射表,返回对应的 pathList, pathMap, nameMap
createRouteMap 构造函数 主要用于创建映射表,根据用户的路由配置规则创建对应的路由映射表
src/create-route-map.js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
// 创建映射表
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历路由配置,为每个配置添加路由记录
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// 确保通配符在最后
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
// 添加路由记录
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
// 获得路由配置下的属性
const { path, name } = route
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// 格式化 url,替换 /
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
// 生成记录对象
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
if (route.children) {
// 递归路由配置的 children 属性,添加路由记录
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 如果路由有别名的话
// 给别名也添加路由记录
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
// 更新映射表
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 命名路由添加记录
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
4.路由初始化 init
当根组件调用 beforeCreate
钩子函数时,会执行插件安装阶段注入的 beforeCreate 函数
beforeCreate () {
// 在option上面存在router则代表是根组件
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
// 执行_router实例的 init 方法 在 VueRouter 构造函数中的 init()
this._router.init(this)
// 为 vue 实例定义数据劫持 让 _router 的变化能及时响应页面的更新
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根组件则直接从父组件中获取
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 通过 registerInstance(this, this)这个方法来实现对router-view的挂载操作:主要用于注册及销毁实例
registerInstance(this, this)
},
在根组件中进行挂载,非根组件从父级中获取,保证全局只有一个 路由实例 初始化时执行,保证页面再刷新时也会进行渲染
init() -- vueRouter 构造函数中的路由初始化
src/index.js
init(app: any /* Vue component instance */) {
// 将当前vm实例保存在app中,保存组件实例
this.apps.push(app)
// 如果根组件已经有了就返回
if (this.app) {
return
}
/* this.app保存当前vm实例 */
this.app = app
// 赋值路由模式
const history = this.history
// 判断路由模式,以哈希模式为例
if (history instanceof HTML5History) {
// 路由跳转
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
// 添加 hashchange 监听
const setupHashListener = () => {
history.setupListeners()
}
// 路由跳转
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 该回调会在 transitionTo 中调用
// 对组件的 _route 属性进行赋值,触发组件渲染
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
init() 核心就是进行路由的跳转,改变 URL 然后渲染对应的组件。 路由初始化:
- 在Vue调用init进行初始化时会调用beforeCreate钩子函数
- init方法中调用了transationTo 路由跳转
- 在transationTo方法中又调用了confirmTransation 确认跳转路由,最终在这里执行了runQueue方法,
- runQueue 会把队列 queue 中的所有函数调用执行,其中就包括 路由守卫钩子函数 的执行
5.路由跳转
transitionTo
src/history/base.js
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 获取匹配的路由信息
const route = this.router.match(location, this.current)
// 确认切换路由
this.confirmTransition(route, () => {
// 以下为切换路由成功或失败的回调
// 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
// 调用 afterHooks 中的钩子函数
this.updateRoute(route)
// 添加 hashchange 监听
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只执行一次 ready 回调
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
// 错误处理
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
updateRoute (route: Route) {
// 更新当前路由信息 对组件的 _route 属性进行赋值,触发组件渲染
const prev = this.current
this.current = route
this.cb && this.cb(route)
// 路由跳转完成 调用 afterHooks 中的钩子函数
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
在路由跳转前要先匹配路由信息,在确认切换路由后更新路由信息,触发组件的渲染,最后更新 url
Matcher 中的 match() 在路由配置中匹配到相应的路由则创建对应的路由信息
src/create-matcher.js
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 序列化 url
// 比如对于该 url 来说 /abc?foo=bar&baz=qux#hello
// 会序列化路径为 /abc
// 哈希为 #hello
// 参数为 foo: 'bar', baz: 'qux'
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 如果是命名路由,就判断记录中是否有该命名路由配置
if (name) {
const record = nameMap[name]
// 没找到表示没有匹配的路由
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
// 参数处理
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
// 非命名路由处理
location.params = {}
for (let i = 0; i < pathList.length; i++) {
// 查找记录
const path = pathList[i]
const record = pathMap[path]
// 如果匹配路由,则创建路由
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// 没有匹配的路由 返回空的路由
return _createRoute(null, location)
}
通过matcher的match方法(有name匹配name,没有就匹配path,然后返回,默认重新生成一条路由返回) 解析用户的路由配置并按照route类型返回,然后路由切换就按照这个route来。
根据匹配的条件创建路由 _createRoute()
src/create-matcher.js
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// 根据条件创建不同的路由
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
createRoute ()
src/util/route.js
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
// 深拷贝
query = clone(query)
} catch (e) {}
// 创建路由对象
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
// 通过Object.freeze定义的只读对象 route
return Object.freeze(route)
}
// 获得包含当前路由的所有嵌套路径片段的路由记录
// 包含从根路由到当前路由的匹配记录,从上至下
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
6. 确认跳转
至此匹配路由已经完成,我们回到 transitionTo 函数中,接下来执行 confirmTransition
confirmTransition(route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
// 中断跳转路由函数
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// 如果是相同的路由就不跳转
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
// 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
function resolveQueue(
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
// 当前路由路径和跳转路由路径不同时跳出遍历
if (current[i] !== next[i]) {
break
}
}
return {
// 可复用的组件对应路由
updated: next.slice(0, i),
// 需要渲染的组件对应路由
activated: next.slice(i),
// 失活的组件对应路由
deactivated: current.slice(i)
}
}
// 导航守卫数组
const queue: Array<?NavigationGuard> = [].concat(
// 失活的组件钩子
extractLeaveGuards(deactivated),
// 全局 beforeEach 钩子
this.router.beforeHooks,
// 在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
// 需要渲染组件 enter 守卫钩子
activated.map(m => m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
// 保存路由
this.pending = route
// 迭代器,用于执行 queue 中的导航守卫钩子
const iterator = (hook: NavigationGuard, next) => {
// 路由不相等就不跳转路由
if (this.pending !== route) {
return abort()
}
try {
// 执行钩子
hook(route, current, (to: any) => {
// 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
// 否则会暂停跳转
// 以下逻辑是在判断 next() 中的传参
if (to === false || isError(to)) {
// next(false)
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') 或者 next({ path: '/' }) -> 重定向
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 这里执行 next
// 通过 runQueue 中的 step(index+1) 执行 next()
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 经典的同步执行异步函数
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
// 接下来执行 需要渲染组件中的 beforeRouteEnter 导航守卫钩子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// beforeResolve 解析路由钩子
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
// 跳转完成
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
// 队列中的函数都执行完毕,就执行回调函数
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
// 执行迭代器,用户在钩子函数中执行 next() 回调
// 回调中判断传参,没有问题就执行 next(),也就是 fn 函数中的第二个参数
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
// 取出队列中第一个钩子函数
step(0)
}
7. 导航守卫
导航守卫在 确认路由跳转中出现
const queue: Array<?NavigationGuard> = [].concat(
// 失活的组件钩子
/*
* 找出组件中对应的钩子函数, 给每个钩子函数添加上下文对象为组件自身
* 数组降维,并且判断是否需要翻转数组,因为某些钩子函数需要从子执行到父,
* 获得钩子函数数组
*/
extractLeaveGuards(deactivated),
// 全局 beforeEach 钩子, 将函数 push 进 beforeHooks 中。
this.router.beforeHooks,
// 在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
// 需要渲染组件 beforeEnter 守卫钩子
activated.map(m => m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
- 先执行失活组件 deactivated 的钩子函数 ,找出对应组件中的钩子函数
function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
// 传入需要执行的钩子函数名 失活组件触发 beforeRouteLeave
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
function extractGuards(
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, key) => {
// 找出组件中对应的钩子函数
const guard = extractGuard(def, name)
if (guard) {
// 给每个钩子函数添加上下文对象为组件自身
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
// 数组降维,并且判断是否需要翻转数组
// 因为某些钩子函数需要从子执行到父
return flatten(reverse ? guards.reverse() : guards)
}
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
// 数组降维
return flatten(matched.map(m => {
// 将组件中的对象传入回调函数中,获得钩子函数数组
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
- 执行全局 beforeEach 钩子函数, 将函数 push 进 beforeHooks 中。
beforeEach(fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
function registerHook(list: Array<any>, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
- 执行 beforeRouteUpdate 钩子函数 与 deactivated 实现类似
- 执行 beforeEnter 独享路由钩子
- 解析异步组件
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
return (to, from, next) => {
let hasAsync = false
let pending = 0
let error = null
// 扁平化数组 获取 组件中的钩子函数数组
flatMapComponents(matched, (def, _, match, key) => {
// 判断是否是异步组件
if (typeof def === 'function' && def.cid === undefined) {
// 异步组件
hasAsync = true
pending++
// 成功回调
// once 函数确保异步组件只加载一次
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
// 判断是否是构造函数
// 不是的话通过 Vue 来生成组件构造函数
def.resolved = typeof resolvedDef === 'function'
? resolvedDef
: _Vue.extend(resolvedDef)
// 赋值组件
// 如果组件全部解析完毕,继续下一步
match.components[key] = resolvedDef
pending--
if (pending <= 0) {
next()
}
})
// 失败回调
const reject = once(reason => {
const msg = `Failed to resolve async component ${key}: ${reason}`
process.env.NODE_ENV !== 'production' && warn(false, msg)
if (!error) {
error = isError(reason)
? reason
: new Error(msg)
next(error)
}
})
let res
try {
// 执行异步组件函数
res = def(resolve, reject)
} catch (e) {
reject(e)
}
if (res) {
// 下载完成执行回调
if (typeof res.then === 'function') {
res.then(resolve, reject)
} else {
const comp = res.component
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject)
}
}
}
}
})
// 不是异步组件直接下一步
if (!hasAsync) next()
}
}
异步组件解析后会执行 runQueue 中的回调函数
// 经典的同步执行异步函数
runQueue(queue, iterator, () => {
const postEnterCbs = [] // 存放beforeRouteEnter 中的回调函数
const isValid = () => this.current === route
// 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
// 接下来执行 需要渲染组件中的 beforeRouteEnter 导航守卫钩子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// beforeResolve 导航守卫钩子
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
// 跳转完成
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
- 执行 beforeRouterEnter ,因为在 beforeRouterEnter 在路由确认之前组件还未渲染,所以此时无法访问到组件的 this 。
但是该钩子函数在路由确认执行,是唯一一个支持在 next 回调中获取 this 对象的函数。
// beforeRouteEnter 钩子函数
function extractEnterGuards (
activated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: () => boolean
): Array<?Function> {
return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
return bindEnterGuard(guard, match, key, cbs, isValid)
})
}
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: () => boolean
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
next(cb)
if (typeof cb === 'function') {
// 判断 cb 是否是函数
// 是的话就 push 进 postEnterCbs
cbs.push(() => {
// #750
// if a router-view is wrapped with an out-in transition,
// the instance may not have been registered at this time.
// we will need to poll for registration until current route
// is no longer valid.
// 循环直到拿到组件实例
poll(cb, match.instances, key, isValid)
})
}
})
}
}
// 该函数是为了解决 issus #750
// 当 router-view 外面包裹了 mode 为 out-in 的 transition 组件
// 会在组件初次导航到时获得不到组件实例对象
function poll (
cb: any, // somehow flow cannot infer this is a function
instances: Object,
key: string,
isValid: () => boolean
) {
if (instances[key]) {
cb(instances[key])
} else if (isValid()) {
// setTimeout 16ms 作用和 nextTick 基本相同
setTimeout(() => {
poll(cb, instances, key, isValid)
}, 16)
}
}
- 执行 beforeResolve 导航守卫钩子,如果注册了全局 beforeResolve 钩子就会在这里执行。
- 导航确认完成后 updateRoute 切换路由,更新路由信息后 调用 afterEach 导航守卫钩子
updateRoute (route: Route) {
// 更新当前路由信息 对组件的 _route 属性进行赋值,触发组件渲染
const prev = this.current
this.current = route
this.cb && this.cb(route) // 实际执行 init传入的回调, app._route = route 对组件的 _route 属性进行赋值
// 路由跳转完成 调用 afterHooks 中的钩子函数
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
this.cb 是怎么来的呢? 其实 this.cb 是通过 History.listen 实现的,在VueRouter 的初始化 init 过程中对 this.cb 进行了赋值
// History 类中 的listen 方法对this.cb 进行赋值
listen (cb: Function) {
this.cb = cb
}
// init 中执行了 history.listen,将回调函数赋值给 this.cb
init (app: any /* Vue component instance */) {
this.apps.push(app)
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
- 触发组件的渲染
当app._router 发生变化时触发 vue 的响应式调用render() 将路由相应的组件渲染到中
app._route = route
hash 模式的实现
hash模式的原理是监听浏览器url中hash值的变化,并切换对应的组件
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()
}
// 监听 hash 的变化
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
// 传入当前的 hash 并触发跳转
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
}
// 如果浏览器没有 # 则自动补充 /#/
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
export default HashHistory
如果手动刷新页面的话,是不会触发hashchange事件的,也就是找不出组件来,那咋办呢?刷新页面肯定会使路由重新初始化,咱们只需要在初始化函数init 上执行一次原地跳转就行。
router-view 组件渲染
组件渲染的关键在于 router-view ,将路由变化时匹配到的组件进行渲染。 routerView是一个函数式组件,函数式组件没有data,没有组件实例。 因此使用了父组件中的$createElement函数,用以渲染组件,并且在组件渲染的各个时期注册了hook 如果被 keep-alive 包裹则直接使用缓存的 vnode 通过 depth 实现路由嵌套, 循环向上级访问,直到访问到根组件,得到路由的 depth 深度
export default {
name: 'RouterView',
/*
https://cn.vuejs.org/v2/api/#functional
使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
*/
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
/* 标记位,标记是route-view组件 */
data.routerView = true
/* 直接使用父组件的createElement函数 因此router-view渲染的组件可以解析命名槽*/
const h = parent.$createElement
/* props的name,默认'default' */
const name = props.name
/* option中的VueRouter对象 */
const route = parent.$route
/* 在parent上建立一个缓存对象 */
const cache = parent._routerViewCache || (parent._routerViewCache = {})
/* 记录组件深度 用于实现路由嵌套 */
let depth = 0
/* 标记是否是待用(非alive状态)) */
let inactive = false
/* _routerRoot中中存放了根组件的势力,这边循环向上级访问,直到访问到根组件,得到depth深度 */
// 用 depth 帮助找到对应的 RouterRecord
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
// 遇到其他的 router-view 组件则路由深度+1
depth++
}
/* 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态) */
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
/* 存放route-view组件的深度 */
data.routerViewDepth = depth
/* 如果inactive为true说明在keep-alive组件中,直接从缓存中取 */
if (inactive) {
return h(cache[name], data, children)
}
// depth 帮助 route.matched 找到对应的路由记录
const matched = route.matched[depth]
/* 如果没有匹配到的路由,则渲染一个空节点 */
if (!matched) {
cache[name] = null
return h()
}
/* 从成功匹配到的路由中取出组件 */
const component = cache[name] = matched.components[name]
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
/* 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用 */
data.registerRouteInstance = (vm, val) => {
/* 第二个值不存在的时候为注销 */
// val could be undefined for unregistration
/* 获取组件实例 */
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
/* 这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val(这个时候其实是一个undefined)赋给instances */
matched.instances[name] = val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// resolve props
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
return h(component, data, children)
}
}
嵌套路由的实现
routerView的render函数通过定义一个depth参数,来判断当前嵌套的路由是位于matched函数层级,然后取出对应的record对象,渲染器对应的组件。
router-link 组件
router-link 的本质是 a 标签,在标签上绑定了click事件,然后执行对应的VueRouter实例的push()实现的
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean, // 当点击时会调用router.replace()而不是router.push(),这样导航后不会留下history记录
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click' // 默认为 click 事件
}
},
render (h: Function) {
// 获取 $router 实例
const router = this.$router
// 获取当前路由对象
const current = this.$route
// 要跳转的地址
const { location, route, href } = router.resolve(this.to, current, this.append)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// Support global empty active class
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler = e => {
// 绑定点击事件
// 若设置了 replace 属性则使用 router.replace 切换路由
// 否则使用 router.push 更新路由
if (guardEvent(e)) {
if (this.replace) {
// router.replace() 导航后不会留下history记录
router.replace(location)
} else {
router.push(location)
}
}
}
const on = { click: guardEvent } // <router-link> 组件默认都支持的click事件
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
const data: any = {
class: classes
}
if (this.tag === 'a') { // 如果是 a 标签会绑定监听事件
data.on = on // 监听自身
data.attrs = { href }
} else {
// find the first <a> child and apply listener and href
const a = findAnchor(this.$slots.default) // 如果不是 a标签则会 找到第一个 a 标签
if (a) {
// in case the <a> is a static node // 找到第一个 a 标签
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
// doesn't have <a> child, apply listener to self
data.on = on // 如果没找到 a 标签就监听自身
}
}
//最后调用$createElement去创建该Vnode
return h(this.tag, data, this.$slots.default)
}
}
// 阻止浏览器的默认事件,所有的事件都是通过 VueRouter 内置代码实现的
function guardEvent (e) {
// don't redirect with control keys
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// don't redirect when preventDefault called
if (e.defaultPrevented) return
// don't redirect on right click
if (e.button !== undefined && e.button !== 0) return
// don't redirect if `target="_blank"`
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
// this may be a Weex event which doesn't have this method
if (e.preventDefault) {
e.preventDefault()
}
return true
}
何时触发视图更新
在混入 beforeCreate 时 对 _route 作了响应式处理,即访问vm._route时会先向dep收集依赖
beforeCreate () {
// 判断组件是否存在 router 对象,该对象只在根组件上有
if (isDef(this.$options.router)) {
// 根路由设置为自己
this._routerRoot = this
// this.$options.router就是挂在根组件上的 VueRouter 实例
this._router = this.$options.router
// 执行VueRouter实例上的init方法,初始化路由
this._router.init(this)
// 很重要,为 _route 做了响应式处理
// 即访问vm._route时会先向dep收集依赖, 而修改 _router 会触发组件渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 用于 router-view 层级判断
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
// 访问vm._route时会先向dep收集依赖
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
访问 $router 时触发依赖收集
- 在组件中使用 this.$router
- router-link 组件内部
何时触发 dep.notify 呢? 路由导航实际执行的history.push方法 会触发 tansitionTo
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)
}
在确认路由后执行回调时会通过 updateRoute 触发 this.$route 的修改
updateRoute (route: Route) {
// 更新当前路由信息 对组件的 _route 属性进行赋值,触发组件渲染
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
其中 this.cb 在路由初始化过程中 通过history.listen 保存的
// VueRouter 路由初始化时设置的 listen 回调
history.listen(route => {
this.apps.forEach((app) => {
// $router 的更新 ==>> app._route=route则触发了set,即触发dep.notify向watcher派发更新
app._route = route
})
})
// history 类中 cb的取值
listen (cb: Function) {
this.cb = cb
}
当组件重新渲染, vue 通过 router-view 渲染到指定位置 综上所述 路由触发组件更新依旧是沿用的vue组件的响应式核心, 在执行transitionTo 前手动触发依赖收集, 在路由transitionTo 过程中手动触发更新派发以达到watcher的重新update; 而之所以路由能正确的显示对应的组件,则得益于路由映射表中保存的路由树形关系
$router.push 切换路由的过程
vue-router 通过 vue.mixin 方法注入 beforeCreate 钩子,该混合在 beforeCreate 钩子中通过 Vue.util.defineReactive() 定义了响应式的 _route 。所谓响应式属性,即当 _route 值改变时,会自动调用 Vue 实例的 render() 方法,更新视图。 vm.render()是根据当前的_route 的 path,nam 等属性,来将路由对应的组件渲染到 router-view 中
- $router.push() //显式调用方法
- HashHistory.push() //根据hash模式调用, 设置hash并添加到浏览器历史记录(window.location.hash= XXX)
- History.transitionTo() // ==>> const route = this.router.match(location, this.current) 找到当前路由对应的组件
- History.confirmTransition() // 确认路由,在确认页面跳转后 触发路由守卫,并执行相应回调
- History.updateRoute() //更新路由
- {app._route= route} // 路由的更改派发更新 触发页面的更新
- vm.render() // 在 中进行 render 更新视图
- window.location.hash = route.fullpath (浏览器地址栏显示新的路由的path)
History.replace()
在 hash 模式下
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)
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
通过 window.location.replace 替换当前路由,这样不会将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由。
history模式下
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)
}
监听地址栏
在地址栏修改 url 时 vueRouter 会发生什么变化
当路由采用 hash 模式时,监听了浏览器 hashChange 事件,在路由发生变化后调用 replaceHash()
// 监听 hash 的变化
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
// 传入当前的 hash 并触发跳转
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
在路由初始化的时候会添加事件 setupHashListener 来监听 hashchange 或 popstate;当路由变化时,会触发对应的 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。 所以在浏览器地址栏中直接输入路由相当于代码调用了replace()方法,将路由替换成输入的 url。
在 history 模式下的路由监听是在构造函数中执行的,对 HTML5History 的 popstate 事件进行监听
window.addEventListener('popstate', e => {
const current = this.current
const location = getLocation(this.base)
if (this.current === START && location === initLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
小结
页面渲染
1、Vue.use(Router) 注册 2、注册时调用 install 方法混入生命周期,定义 router 和 route 属性,注册 router-view 和 router-link 组件 3、生成 router 实例,根据配置数组(传入的routes)生成路由配置记录表,根据不同模式生成监控路由变化的History对象 4、生成 vue 实例,将 router 实例挂载到 vue 实例上面,挂载的时候 router 会执行最开始混入的生命周期函数 5、初始化结束,显示默认页面
路由点击更新
1、 router-link 绑定 click 方法,触发 history.push 或 history.replace ,从而触发 history.transitionTo 方法 2、ransitionTo 用于处理路由转换,其中包含了 updateRoute 用于更新 _route 3、在 beforeCreate 中有劫持 _route 的方法,当 _route 变化后,触发 router-view 的变化
地址变化路由更新
1、HashHistory 和 HTML5History 会分别监控 hashchange 和 popstate 来对路由变化作对用的处理 2、HashHistory 和 HTML5History 捕获到变化后会对应执行 push 或 replace 方法,从而调用 transitionTo 3、然后更新 _route 触发 router-view 的变化
路由相关问题
1. vue-router响应路由参数的变化
- 通过 watch 监听 route 对象
// 监听当前路由发生变化的时候执行
watch: {
$route(to, from){
console.log(to.path)
// 对路由变化做出响应
}
}
- 组件中的 beforeRouteUpdate 路由守卫
在组件被复用的情况下,在同一组件中路由动态传参的变化 如: 动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
beforeRouteUpdate(to, from, next){
// to do somethings
}
2. keep-alive 缓存后获取数据
- beforeRouteEnter
在每次组件渲染时执行 beforeRouterEnter
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
- actived
在 keep-alive 组件被激活时都会执行 actived 钩子
服务器端渲染期间 avtived 不被调用
activated(){
this.getData() // 获取数据
},
总结
当时在写这篇文的时候就是想着尽量能把各个知识点都串联上,建立完善的知识体系
这不写着写着就成了长文😂, 一旦开始就无法停下,那就硬着头皮继续吧
不过这篇长文真的是有够长的,哈哈哈哈,能坚持看到这里的同学我都感到佩服😘
如果觉得还有哪里缺失的点可以及时告诉我哦
那么今天就先到这啦😜