3-6、Vue Router-核心源码讲解

官方文档:https://router.vuejs.org/zh/guide/

路由的演变

之前,部署到服务器的前端项目是由多个 HTML 文件组成,每个 HTML 都有对应服务器路径,前端称其为路由,路由之间使用location.href跳转,跳转路径就是另一个 HTML 的服务器地址。这时候的路由是由后端来管理的
后面单页应用流行,部署到服务器的前端项目就只有一个 HTML 文件,对应一个服务器路径。这时候为满足不同页面的展示,就需要借助框架提供的路由能力,至此路由的管理转移到前端身上。

路由的组成

location的组成:
location.protocal协议
location.host 域名
location.port 端口(多数省略了)
location.pathname 路径
location.search 参数,[? 后面,# 之前)的内容
location.hash 锚点,# 后面的内容

路由的分类

单页应用下,分为:hash、history
hash:
路由上带 #,内容为 # 后面,用它来区分页面;
不需要服务端配合。

history:
路由上不带 #,内容为[域名后面,? 之前),用它来区分页面;
需要服务端配合。因为部署到服务器后,该模式实际上访问服务器的资源,但单页应用只有一个指向 html 的路径,所以这样访问会返回 404,一般需要配置让其指向 html 的路径

路由实现的核心原理

核心原理:监听路径的变化,找到该路径对应的组件,然后渲染到相应位置,并注入 router 等上下文。其中的对应关系就是我们常写的路由配置项。

Vue 路由

基础使用

初始化

Vue2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]

const router = new VueRouter({
routes
})

export default router

// main.ts
import Vue from 'vue'
import App from './App.vue'
import router from './router' // ⭐️

Vue.config.productionTip = false

new Vue({
router, // ⭐️
render: h => h(App)
}).$mount('#app')

Vue3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})

export default router

// main.ts
import { createApp } from 'vue'

import App from './App.vue'
import router from './router' // ⭐️

const app = createApp(App)

app.use(router) // ⭐️

app.mount('#app')

页面中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div id="app">
<nav>
// ⭐️
<router-link to="/">Home</router-link>
<router-link @click="bandleNavClick">About</router-link>
</nav>
<router-view/> // ⭐️
</div>
</template>

<script>
export default {
computed: {
username() {
// this.$route 当前路由
return this.$route.params.username
},
},
methods: {
bandleNavClick() {
// this.$router 路由实例
this.$router.push({
path: '/about'
})
}
}
}
</script>

路由守卫

全局:beforeEach、beforeResolve、afterEach
路由配置:beforeEnter
组件内:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

源码解析

Vue-Router 3.x

对应 Vue2.x
VueRouter部分源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
export default class VueRouter {
static install(Vue) {
// ...

// 通过 mixin 生命周期来注册实例的
Vue.mixin({
// 每个 .vue 都会调用
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this // this 为 .vue 实例

// $options.router 来源于 new Vue({ router }) 时传入的 router
// router 为 new VueRouter(...) 出来的实例
this._router = this.$options.router

// 初始化应用、设置历史滚动位置等等
this._router.init(this)

// 调用 Vue.util 的 上的 defineReactive
// 将 _route 定义为 .vue 实例的响应式属性,值为当前的路由信息
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})

// ...

// 将 $router 挂载到 Vue 原型链上,所以才支持 this.$router
// $router:当前路由实例(全局唯一),主要用提供的方法:push、back 等
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})

// 将 $route 挂载到 Vue 原型链上,所以才支持 this.$route
// $route:当前路由,主要获取当前路由信息:路径、地址栏参数等
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})

// 注册 Vue 组件:RouterView
Vue.component('RouterView', View)

// 注册 Vue 组件:RouterLink
Vue.component('RouterLink', Link)
},
constructor (options: RouterOptions = {}) {
// ...

// new VueRouter() 时传入的参数:routers、mode 等
this.options = options

// ...

switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
// ...
}
}
}

RouterView源码解析
本质上注册了一个 Vue 动态组件,根据路由配置,找到对应component值,然后将其渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
/*
_: 这个参数通常用作占位符,表示这个参数在函数体中不会被使用。
props: 传递给当前组件的属性。
children: 当前组件的子组件。
parent: 当前组件的父组件。
data: 当前组件的数据对象。
*/
render (_, { props, children, parent, data }) {
const h = parent.$createElement
const name = props.name
const route = parent.$route

// ...

const matched = route.matched[depth]
// 找组件 component 的逻辑
const component = matched && matched.components[name]

// ...

return h(component, data, children)
}
}

下面的代码可以粗略表述上述源码逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  router-view.vue
<template>
<component :is="componentContent" />
</template>
<script>
export default {
computed: {
componentContent() {
// matched: 指已匹配到的路由配置信息
return this.$route.matched[0]?.components
}
}
}
</script>

RouterLink源码解析
本质上注册了一个 Vue 组件,该组件最终渲染为a 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
// ...
},

render (h: Function) {
const router = this.$router
const current = this.$route

// ...

// 根据 props 与一些逻辑,往 tagAttrs 里面扔 a 标签的一些属性
const tagAttrs = {}

return h(this.tag, tagAttrs, this.$slots.default)
}
}
1
2
比如:<router-link to="/about">About</router-link>
渲染:<a href="#/about" class="">About</a>

Vue-Router 4.x

对应 Vue3.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})

export default router

// main.ts
import { createApp } from 'vue'

import App from './App.vue'
import router from './router' // ⭐️

const app = createApp(App)

app.use(router) // ⭐️

app.mount('#app')

createRouter源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
export function createRouter(options: RouterOptions): Router {
// 基于传入的 routes,生成 matcher,便于内部的查找
const matcher = createRouterMatcher(options.routes, options)

// ...

const router: Router = {
// ...

push,
replace,
go,
back: () => go(-1),
forward: () => go(1),

// vue.use 时调用
install(app: App) {
const router = this

// 注册 Vue 组件:RouterView
app.component('RouterLink', RouterLink)

// 注册 Vue 组件:RouterView
app.component('RouterView', RouterView)

// 将 $router 挂载到 Vue 原型链上,所以才支持 this.$router
// $router:当前路由实例(全局唯一),主要用提供的方法:push、back 等
app.config.globalProperties.$router = router


// 将 $route 挂载到 Vue 原型链上,所以才支持 this.$route
// $route:当前路由,主要获取当前路由信息:路径、地址栏参数等
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})

// 定义响应式变量:reactiveRoute
const reactiveRoute = {} as RouteLocationNormalizedLoaded
for (const key in START_LOCATION_NORMALIZED) {
Object.defineProperty(reactiveRoute, key, {
get: () => currentRoute.value[key as keyof RouteLocationNormalized],
enumerable: true,
})
}

// 通过 Vue 的 provide
// 将 路由实例、浅响应式的reactiveRoute、当前路由currentRoute 进行注入
app.provide(routerKey, router)
app.provide(routeLocationKey, shallowReactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)

// ...
},
}

return router
}

共同点

为啥跳转页面并不会刷新?

不管路由模式是hash、history,最终跳转页面时Vue Router都是用的window.history.pushState,用该 API 改变地址,页面将不会刷新。
路由模式差异体现在window.history.pushState的传参url上,带不带#而已

并且#的变化本身也不会引起页面的刷新

面试题

手写路由(简单版)

核心原理:监听路径的变化,找到该路径对应的组件,然后渲染到相应位置,并注入 router 等上下文。其中的对应关系就是我们常写的路由配置项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// router.js

class Router {
constructor(options) {
this._options = options;

this._routes = options.routes;

this.routerHistory = [];
this.currentIndex = -1;
this.currentPath = "";

this.init();
}

init() {
window.addEventListener("hashchange", this.refresh.bind(this));
window.addEventListener("load", this.refresh.bind(this));
}

refresh() {
let _path = Router.getPath();

this.routerHistory = this.routerHistory.slice(0, this.currentIndex + 1);
this.routerHistory.push(_path);
this.currentIndex++;

let { component, path } = this.findRoute(_path);

if (!component) {
path = "/404";
component = this.findRoute(path).component || "404";
}

document.querySelector(".router-view-wrapper").innerHTML = component;

Router.changeHash(path);
}

push(options) {
if (options.path) {
Router.changeHash(options.path);
} else if (options.name) {
let { path } = this.findRoute(options.name, "name");
Router.changeHash(path);
}
}

findRoute(value, key = "path") {
let _findRoute = this._routes.find((item) => item[key] === value) || {};

if (_findRoute.rederict) {
return this.findRoute(_findRoute.rederict, key);
}

return _findRoute;
}

static getPath() {
let path = window.location.hash;
if (path) return path.replace("#", "/");
else return "/";
}

static changeHash(path) {
window.location.hash = path.slice(1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>custom-router</title>
</head>
<body>
<div id="app">
<div>
<a onclick="router.push({name:'home'})">Home</a>
<a onclick="router.push({name:'about'})">About</a>
</div>
<div class="router-view-wrapper"></div>
</div>
<script src="./router.js"></script>
<script>
const routes = [
{
path: "/",
rederict: "/home",
},
{
path: "/home",
name: "home",
component: `<div>Home Content</div>`,
},
{
path: "/about",
name: "about",
component: `<div>About Content</div>`,
},
{
path: "/404",
name: "404",
component: `<div>Error 404</div>`,
},
];

const router = new Router({ routes });
</script>
</body>
</html>

3-6、Vue Router-核心源码讲解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/3-6、Vue Router-核心源码讲解/
作者
黄智强
发布于
2024年1月13日
许可协议