原文链接(格式更好):《3-6 Vue Router-核心源码讲解》
官方文档: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 的路径
Vue 路由
基础使用
初始化
Vue2
// 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
// 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')
页面中使用
<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
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
源码解析
Vue-Router 3.x
对应 Vue2.x
VueRouter部分源码解析:
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值,然后将其渲染
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)
}
}
下面的代码可以粗略表述上述源码逻辑
// router-view.vue
<template>
<component :is="componentContent" />
</template>
<script>
export default {
computed: {
componentContent() {
// matched: 指已匹配到的路由配置信息
return this.$route.matched[0]?.components
}
}
}
</script>
RouterLink源码解析
本质上注册了一个 Vue 组件,该组件最终渲染为a 标签
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)
}
}
比如:<router-link to="/about">About</router-link>
渲染:<a href="#/about" class="">About</a>
Vue-Router 4.x
对应 Vue3.x
// 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源码解析
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.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);
}
}
<!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>