一文弄懂前端路由,扫清面试障碍
前端路由是面试过程中必问的题目,为了弄清楚其核心原理,本文分别对 hash 和 history两种模式,手写了 vue-router,核心代码总共不到100行,但足以说清楚前端路由的原理。欢迎阅读。
前端路由的演变
在 jQuery 时代,对于大部分 Web 项目而言,前端都是不能控制路由的,而是需要依赖后端项目的路由系统。
通常,前端项目会存放在后端项目某个文件夹中,随后台一起部署,整个项目执行的示意图如下:

这种开发方式有很多缺点,最重要的有两点:
- 前后端杂糅在一起,无法分离,无法适应现代前端大规模项目;
- 页面跳转由于需要重新刷新整个页面、等待时间较长,交互体验差;
为了提高页面的交互体验,前端大佬们做了不同的尝试。现在,前端的开发模式和项目结构都发生了变化。下图所示的,是在目前的前端开发中,用户访问页面后代码执行的过程。

整个流程如下:
- 用户访问路由后,无论是什么 URL 地址,都直接渲染一个前端的入口文件
index.html; index.html文件中加载整个项目所需要的JS和CSS文件;- 通过
JS代码获取当前的页面地址,根据地址找到当前路由匹配的组件,再去动态渲染当前页面; - 用户在页面上进行点击操作时,不需要刷新页面,直接通过
JS代码重新计算出匹配的路由渲染即可;
在这个架构下,前端获得了路由的控制权,在 JS 代码中控制路由系统。
因此,页面跳转的时候就不需要刷新页面,网页的浏览体验也得到了提高。
这种所有路由都渲染一个前端入口文件的方式,是单页面应用程序(SPA,single page application)应用的雏形。
SPA 应用相比于模板的开发方式(JSP),除了交互体验也更加丝滑等优点外,更重要的是,前端项目终于可以独立出来单独部署了,这为后面的前端大繁荣奠定了基础。
前端路由实现原理
类似于服务端路由,前端路由的原理类似,就是匹配不同的 URL 路径,进行解析,然后动态地渲染出区域 HTML 内容。
但是这样存在一个问题,就是 URL 每次变化的时候,都会造成页面的刷新,即向后台发送http请求。解决这一问题的思路便是在改变 URL 的情况下,保证页面的不刷新。
在 2014 年之前,都是通过 hash 来实现前端路由,类似于这样的地址http://www.xxx.com/#/login。
在进行页面跳转的操作时,hash 值的变化并不会导致浏览器页面的刷新,只是会触发 hashchange 事件。然后,通过对 hashchange 事件的监听,我们就可以在 fn 函数内部进行动态地页面切换。
window.addEventListener('hashchange',fn)
这种模式叫hash 模式。
2014 年之后,因为 HTML5 标准发布,浏览器多了两个 API:pushState 和 replaceState。通过这两个 API ,改变 URL 地址,浏览器不会向后端发送请求,这样我们就能用另外一种方式实现前端路由了。
window.addEventListener('popstate', fn)
这种模式叫history 模式。
这两个不同的原理,在 vue-router 中对应两个函数,分别是 createWebHashHistory 和 createWebHistory。

手写一个vue-router
hash模式
import { inject, ref } from 'vue'
const ROUTER_KEY = '__router__'
function createRouter(options) {
return new Router(options)
}
// 在页面中使用useRouter()后,把router实例注册到该页面组件中
function useRouter() {
return inject(ROUTER_KEY)
}
function createWebHashHistory() {
function bindEvents(fn) {
window.addEventListener('hashchange', fn)
}
return {
bindEvents,
url: window.location.hash.slice(1) || '/'
}
}
// 路由的构造函数
class Router {
constructor(options) {
this.history = options.history
this.routes = options.routes
this.current = ref(this.history.url)
this.history.bindEvents(() => {
this.current.value = window.location.hash.slice(1)
})
}
// 往vue实例上添加一个路由实例
install(app) {
app.provide(ROUTER_KEY, this)
}
}
export { createRouter, useRouter, createWebHashHistory }
- 创建一个
Router类去管理路由 - 创建
createWebHashHistory来返回hash模式相关的监听代码,以及返回当前 URL 和监听hashchange事件的方法; - 通过
Router类的install方法注册了Router的实例,并对外暴露createRouter方法去创建Router实例; - 创建
useRouter方法,去获取路由实例;
下一步,需要注册两个内置组件 router-view 和 router-link。
在 createRouter 创建的 Router 实例上,current 返回当前的路由地址,并且使用 ref 包裹成响应式的数据。
router-view 组件的功能,就是 current 发生变化的时候,去匹配 current 地址对应的组件,然后动态渲染到 router-view 就可以了。
// RouterView.vue代码
<template>
<slot :Component="comp">
// 插槽里面默认的内容
<component :is="comp"></component>
</slot>
</template>
<script setup>
import {computed } from 'vue'
import { useRouter } from '../router/index'
let router = useRouter()
const comp = computed(()=>{
const route = router.routes.find(
// router.current是响应式数据,当其变化时,comp也会发生变化
(route) => route.path === router.current.value
)
return route ? route.component : null
})
</script>
注意,这里使用了作用域插槽,这样就可以把comp返回给用户,让用户自己定义如何渲染页面。比如,像下面代码这样,在页面加载和退出时做一个缓动动画效果。
<router-view v-slot="{ Component }">
<transition name="route" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
有了 RouterView 组件后,再来实现 router-link 组件。
<template>
<a :href="`#${to}`">
<slot></slot>
</a>
</template>
<script setup>
defineProps({
to: {
type: String,
required: true
}
})
</script>
最后需要全局注册 router-link 和 router-view 这两个组件, 这样 hash 模式的迷你 vue-router 就算实现了。
history模式
上面用hash模式实现了vue-router,下面用history模式实现vue-router:
RouterLink.vue代码改造如下:
<template>
<a @click.prevent="jump">
<slot></slot>
</a>
</template>
<script setup>
let props = defineProps({
to: {
type: String,
required: true
}
})
function jump() {
window.history.pushState({}, '', props.to)
// 自定义一个事件
const event = new CustomEvent('customPopState', {
// 传递的参数需要放在detail属性中
detail: {
path: props.to
}
})
window.dispatchEvent(event)
}
</script>
index.js改造如下:
function createWebHistory() {
function bindEvents(fn) {
// 监听自定义的事件,因为当主动调用window.history.pushState时,并不会触发popState事件
window.addEventListener('customPopState', fn)
}
return {
bindEvents,
url: window.location.pathname || '/'
}
}
class Router {
constructor(options) {
this.history = options.history
this.routes = options.routes
this.current = ref(this.history.url)
this.history.bindEvents((el) => {
this.current.value = el.detail.path
})
}
install(app) {
app.provide(ROUTER_KEY, this)
app.component('router-view', RouterView)
app.component('router-link', RouterLink)
}
}
这里 URL 的改变是通过window.history.pushState({}, '', props.to)实现的,URL 地址就没有了#,类似于http://localhost:5173/about。
但是当主动指定history.pushState并不会触发原生的popstate事件,所以,我们自定义了一个事件customPopState,然后监听它进行改变current的值。
这样就完成了history模式下的vue-router。
总结
首先回顾了前段路由的发展历史,从依赖后端路由到前端自己控制路由,于是就产生了SPA模式,网页的浏览体验也得到了提高。
接着,通过手写一个迷你的vue-router加深了对前端路由的理解,它们的原理都是当 URL 改变时能获取到最新的值,然后从注册的路由组件中匹配,最后用动态组件component来渲染。
当然,真实的vue-router会复杂的多,包括动态路由,路由守卫等功能。
转载自:https://juejin.cn/post/7269590220940525583