今天,我来带大家刨析Vue路由原理!
我们知道Vue路由有两种模式:history模式和hash模式
在history模式下,我们的页面路径为:
在hash模式下,我们的页面路径为:
这个就是两个模式的区别,其实hash模式实现路由的手段更加的方便,但是hash在浏览器路径上会多出一个#
的符号,不是那么美观。
那么,我们要自己实现一个路由该怎么实现呢?
vue-router项目准备
首先我们可以安装一个vue项目,这里我选择的是使用vite
脚手架开始一个新项目!再进行配置文件的安装,这里我们手动安装一下vue-router
js
npm install vue-router@4
yarn add vue-router@4
pnpm add vue-router@4
三种安装方式:npm,yarn,pnpm
安装完成之后,我们来一个最简单的效果:Home-Page
我们要新建两个页面:Home.vue和About.vue
在src目录下,新建一个views文件夹,在文件夹内新建页面
配置好vue-router的配置文件
在src目录下,新建一个router文件夹,在文件夹内新建一个index。js文件,并配置
js
import {createRouter,createWebHashHistory} from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{
path:'/',
name:'home',
component:Home
},
{
path:'/about',
name:'about',
component:About
}
]
const router = createRouter({
history:createWebHashHistory(),//hash模式
routes
})
export default router
我们可以发现,这个配置文件是通过引入vue-router
中的两个函数createRouter
和createWebHashHistory
createRouter
:用于创建路由的函数
createWebHashHistory
:创建哈希模式的函数
整个配置文件可以让我们通过以数组形式的routes
将路径和组件进行一一映射!
实现Vue-router的Hash模式
打造createRouter,createWebHashHistory
通过观察vue-router的配置文件,我们可以发现,其主要功能依赖于引入的两个函数createRouter
和createWebHashHistory
所以,我们要自己实现一个路由Hash源码,也就是对这两个函数功能的实现!
我们可以在原有项目的基础上,在router文件夹中新建一个文件夹myRouter
用于我们自己的路由源码,在myRouter
文件中再新建一个index.js
文件
我们再修改路由的配置文件
将:import {createRouter,createWebHashHistory} from 'vue-router'
替换为:import {createRouter,createWebHashHistory} from './myRouter'
接下来,我们进行配置,这里我们引入了ref
动态得绑定数据,实现同步更新
js
import {ref} from 'vue'
function createRouter(options) {
return new Router(options)//调用就返回一个实例对象
}
function createWebHashHistory(){
}
export {
createRouter,
createWebHashHistory
}
基本的结构我们就配置好了,两个函数createRouter
和createWebHashHistory
,并且将这两个函数抛出!
通过观察路由配置文件
我们可以发现createRouter
接收的是一个对象,并且返回的是一个实例对象,在这个构造函数当中,我们在对象里面接收两个key值:history
和routes
所以我们可以通过使用ES6中的class写一个构造函数Router
js
// es6 构造方法
class Router{
constructor(options){
// 拿到两个参数
this.history = options.history
this.routes = options.routes
}
}
我们接着可以发现history
是createWebHashHistory
的执行结果,这个方法的调用就是启用哈希模式。
于是接下来,我们可以这样编写createWebHashHistory
js
function createWebHashHistory(){
// 绑定事件,绑定一个fn回调函数
function bindEvents(fn){
window.addEventListener('hashchange', fn)//hash改变,fn回调函数触发
}
return {
bindEvents,//函数
//url是当前的路径
url:window.location.hash.slice(1) || '/'
}
}
createWebHashHistory
函数执行返回一个对象,返回的对象当中包含一个函数bindEvents
和一个属性url
当前地址
bindEvents
函数绑定一个事件,负责监听hash值的变化,当hash地址发生变化时,fn
回调函数就会被触发!这里绑定的事件是js自带的'hashchange'
能够捕获到hash值的变更。
此时我们编写一下我们的构造方法Router
js
// es6 构造方法
class Router{
constructor(options){
this.history = options.history
this.routes = options.routes
// 路由要能读到当前的路径
this.current = ref(this.history.url)
// 在函数初始化的时候直接调用掉history的bindEvents函数
this.history.bindEvents(() => {
// 这个箭头函数就是fn回调函数
this.current.value = window.location.hash.slice(1)
})
}
}
这样我们的构造函数就要拿到三个参数了,新增加了一个this.current
读到当前的路径,同时我们也配置了bindEvents
的回调函数fn
,一旦fn
触发,就会将当前新的路径赋值给this.current
实现能够被vue给use成功
实现到这里,我们还需要一个条件,也就是我们写出来的路由,要能够被我们的vue项目use掉!
所以,我们来到构造方法Router
中
js
// es6 构造方法
class Router{
constructor(options){
// 拿到两个参数
this.history = options.history
this.routes = options.routes
// 路由要能读到当前的路径
this.current = this.history.url
// 在函数初始化的时候直接调用掉history的bindEvents函数
this.history.bindEvents(() => {
// 这个箭头函数就是fn回调函数
this.current.value= window.location.hash.slice(1)
})
}
// vue内定的官方方法,第三方插件一定要具备install方法
install(app){
console.log(app);
}
}
我们在构造方法中,新添加了一个install
方法,这是vue
官方内定的一个方法,第三方的插件想要被vue
能够use掉,就一定要具备这个install
方法,我们来看看app
的打印结果!
这个其实就是vue
的实例对象,其中包含了vue的各种方法!
我们拿到其中的provide
方法和component
provide
:提供一个路由的实例对象,负责与inject
进行配合,在进行组件RouterView
的配置当中,我们需要拿到Router
中配置的数组,跳转对应path
的组件中,使用一个标记ROUTER_KEY
(自己声明的),能够通过inject
将这个标记对应的this
,将Router
注入到子组件当中,而这个this
指向的就是所在类本身也就是Router
component
:负责注册全局组件。就比如app.component('router-link',RouterLink)
也就意味着,我们注册一个router-link
的全局组件
拿到vue
的实例对象后,接下来就可以开始我们的正式工作了!
实现router-link全局组件
要实现这样一个组件,我们就要有这样一个组件,我们知道官方的的标签是这样的
html
<router-link to="/">Home</router-link>|
<router-link to="/about">About</router-link>
可以发现,我们要接收一个to
参数,并且组件的标签之间可以放东西,所以,我们打造这个组件需要插槽,并且要接收一个参数to
,将标签转换为a
标签,实现url的跳转。
所以,我们可以声明一个组件RouterLink.vue
html
<template>
<a :href="'#'+to">
<!-- 插槽 -->
<slot />
</a>
</template>
<script setup>
defineProps({
to:{
type:String,
required:true,//必须传
}
})
</script>
在这个组件当中,我们会拿到父组件传过来的to
地址,拼接到a
标签对应的路径当中。
回到我们自己的定义的配置文件index.js
中,我们引入这个组件
js
import RouterLink from './RouterLink.vue'
...
install(app){
// 注册全局组件
app.component('router-link',RouterLink)
}
现在,看看我们这个组件的效果!
注:我们需要修改路由的配置文件
将: import {createRouter,createWebHistory,createWebHashHistory} from 'vue-router'
修改为:import {createRouter,createWebHashHistory} from './myRouter'
使用我们自己配置的路由!
实现router-view全局组件
要实现router-view这样一个效果,我们需要拿到vue-router
的Router
的实例对象,并且根据当前路径,与配置文件routes
中的path
进行比对,找到对应的路径,然后展示相应的组件,也就是页面!
重要!!!我们的页面需要是响应式的,所以我们要引入ref
进行响应式设计!
来到我们的配置文件
js
//引入组件
import RouterView from './RouterView.vue'
// 注入,需要provide
import {inject} from 'vue'
//声明一个标记
const ROUTER_KEY = '_router_'//这是一个标记
...
function useRouter(){
return inject(ROUTER_KEY)
}
...
install(app){
console.log(app);
// ROUTER_KEY是我们声明的标记,this指向的是Router类
app.provide(ROUTER_KEY,this)
// 注册全局组件
app.component('router-link',RouterLink)
app.component('router-view',RouterView)
}
}
//抛出
export {
createRouter,
createWebHashHistory,
useRouter
}
- 在我们的配置文件引入了
inject和组件RouterView
,同时定义了一个标记ROUTER_KEY
- 我们声明了一个
useRouter
函数,返回的是inject
注入,标识ROUTER_KEY
,子组件可以通过调用这个函数来注入router
app.provide
提供一个路由的实例对象,通过POUTER_KEY
这个标识,返回一个this
指向Router
的实例对象!- 通过
app.component('router-view',RouterView)
声明全局组件
RouterView
组件设计
html
<template>
<!-- vue自带的组件 -->
<component :is="component"></component>
</template>
<script setup>
import {computed} from 'vue'
import {useRouter} from '../myRouter/index.js'
const router = useRouter() //在当前组件注入了router
console.log(router)
const component = computed(()=>{
// 找到对应路径的path返回相应的组件 映射关系
const route = router.routes.find((route)=>{
return route.path === router.current.value
})
return route ? route.component :null
})
</script>
在组件设计当中,我们使用到了vue
自带的组件component
标签,可以通过is
属性来设置要展示的组件!
js
部分:
- 引入了计算属性
computed
和配置文件中的useRouter
方法 - 定义一个变量
router
等于useRouter
的执行结果,我们就实现了router
的注入!我们可以看到router
的打印结果 - 定义一个变量
component
进行组件映射- 通过计算属性执行一个回调函数,定义一个变量
route
为router.routes.find
返回的结果, find
是官方自带的一个方法,可以找到符合逻辑return route.path === router.current.value
的结果,通过return
返回出来!return route ? route.component :null
通过三元表达式判断route
是否存在,存在则返回route.component
返回对应的组件,否则返回空,并且赋值给component
- 通过计算属性执行一个回调函数,定义一个变量
- 通过对
component
标签使用v-bind
绑定is
属性,并且赋值为变量component
,就实现了一个简单的vue-router
路由Hash
模式的源码!
看看效果!
全部代码
router配置文件index.js
js
import {createRouter,createWebHashHistory} from './myRouter'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{
path:'/',
name:'home',
component:Home
},
{
path:'/about',
name:'about',
component:About
}
]
const router = createRouter({
history:createWebHashHistory(),//hash模式
routes
})
export default router
myRouter配置文件index.js
js
import {ref} from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
// 注入,需要provide
import {inject} from 'vue'
const ROUTER_KEY = '_router_'//这是一个标记
function useRouter(){
return inject(ROUTER_KEY)
}
function createRouter(options) {
return new Router(options)//调用就返回一个实例对象
}
function createWebHashHistory(){
// 绑定事件,绑定一个fn回调函数
function bindEvents(fn){
//监听Hash变化
window.addEventListener('hashchange', fn)
}
// 返回了一个对象,对象里面有一个函数
return {
// 这是一个函数
bindEvents,
// window.location.hash.slice(1) || '/' 代表当前的路径,如果当前的路径为空,就默认为'/'
url:window.location.hash.slice(1) || '/'
}
}
// es6 构造方法
class Router{
constructor(options){
// 拿到两个参数
this.history = options.history
this.routes = options.routes
// 路由要能读到当前的路径
this.current = ref(this.history.url)
// 在函数初始化的时候直接调用掉history的bindEvents函数
this.history.bindEvents(() => {
// 这个箭头函数就是fn回调函数
this.current.value= window.location.hash.slice(1)
})
}
// vue内定的官方方法,第三方插件一定要具备install方法
install(app){
console.log(app);
// ROUTER_KEY是我们声明的标记,this指向的是Router类
app.provide(ROUTER_KEY,this)
// 注册全局组件
app.component('router-link',RouterLink)
app.component('router-view',RouterView)
}
}
//抛出
export {
createRouter,
createWebHashHistory,
useRouter
}
RouterLink.vue
html
<template>
<a :href="'#'+to">
<!-- 插槽 -->
<slot />
</a>
</template>
<script setup>
defineProps({
to:{
type:String,
required:true,//必须传
}
})
</script>
RouterView.vue
html
<template>
<!-- vue自带的组件 -->
<component :is="component"></component>
</template>
<script setup>
import {computed} from 'vue'
import {useRouter} from '../myRouter/index.js'
const router = useRouter() //在当前组件注入了router
console.log(router)
const component = computed(()=>{
// 找到对应路径的path返回相应的组件 组件映射关系
// 如果当前url是'/'就返回Home.vue
const route = router.routes.find((route)=>{
return route.path === router.current.value
})
return route ? route.component :null
})
</script>
最后
到这里我们已经完成了vue
源码系列的路由之Hash模式精简版代码的实现了。
这个其实算是vue
源码中逻辑算是比较简单的一部分了
总体来看,我们通过手动打造了createRouter,createWebHashHistory,useRouter
等一系列方法,定义了构造函数,通过install
让我们的配置文件能够被vue
给use掉,再定义全局组件,实现父子组件传值等操作实现了这样一个路由!
假如你也和我一样,在准备冲刺春招,欢迎大家加微信shunwuyun,我们这里有好多小伙伴和各种大佬可以相互鼓励,分析信息,模拟面试,共读源码,齐刷算法,手撕面经。加油!友友们!
如果,你觉得这篇文章有帮助的话,可以帮博主点赞+评论+收藏,三连一波!感谢!
往后,我还会持续输出vue相关的文章,如vue路由原理,node相关的内置模块,koa的使用等等文章,感兴趣的小伙伴可以关注一波!
代码已经上传至个人Github:一个修远君的库之vue路由源码Hash模式