前阵子发了一篇文章,讲的是前端路由的实现原理,反响还不错。今天我们继续来手写一个vue-router
之hash
模式,前端路由包括了vue-router
,但是vue-router
里面又封装了许多方法,所以实现起来稍微复杂点
面试官:谈谈前端路由的实现原理【hash&history】 - 掘金 (juejin.cn)
先回顾下vue-router
如何使用,这里以hash
模式为例
vue-router的使用
这里我用的是vite
脚手架创建的项目,需要自己额外安装路由
这里简单实现一下路由效果:点击home
,显示home
内容,点击about
,显示about
内容
安装
css
npm install vue-router@4
来到src
目录下新建一个router
文件夹,再在router
文件夹中新建一个index.js
文件,进行如下配置
javascript
import { createWebHashHistory, createRouter } 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(),
routes: routes,
})
export default router
来到main.js
中引入并use
掉
javascript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App)
.use(router)
.mount('#app')
来到App.vue
中给一个入口router-view
xml
<template>
<div class="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view></router-view>
</template>
<script setup>
</script>
<style lang="css" scoped>
</style>
从这里我们可以看出vue
给路由提供了什么功能:它提供了一个创建路由的函数createRouter
,以及创建哈希模式的函数createWebHashHistory
,这样它就允许我们通过数组的形式将路径和组件一一映射
打造vue-router之hash模式
vue-router
的实现原理就在于它引入的两个函数,更准确地说是在引入的vue-router
中,因此,我们自己手搓一个路由源码,从这里引入如果能实现这个效果就证明没有问题
我在router文件夹中新建一个myRouter
文件夹,我们在这里面写路由源码,先新建一个文件取名为index.js
,于是在原来的index.js文件中引入自己写的源码
python
// import { createWebHistory, createRouter } from 'vue-router'
import { createRouter, createWebHashHistory } from './myRouter'
开始手搓
既然要引入这两个函数,就需要抛出先
php
function createRouter (options) {
return new Router(options)
}
function createWebHashHistory () {
//..................
}
export {
createRouter,
createWebHashHistory
}
从原index.js
中可以看到createRouter
里面接收一个对象,并且可以返回一个实例对象,所以我们还要先自己写一个构造函数,这里我采用es6新语法class来写构造函数
不清楚class语法的可以翻看这篇文章字节面试官:手搓一个发布订阅 【从影子dom谈到es6Class】 - 掘金 (juejin.cn)
我们会在这个构造函数中传入的对象有history
,routes
两个key,并且还需要获取当前hash
值,并且hash
值需要响应式变化,这个值需要让哈希模式来给,哈希模式就是options.history
形参
kotlin
import { ref } from 'vue'
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)
})
}
}
哈希模式createWebHashHistory
函数是负责监听哈希值的变化,并且返回一个对象,对象里面返回一个函数,这个函数接收一个函数,接收到的函数fn
的触发条件就是hashchange
,再返回当前的哈希值给到this.current
,然后再进行赋值
javascript
function createWebHashHistory () { // hash模式
function bindEvents(fn) {
window.addEventListener('hashchange',fn) // 只要hash变了, fn就会触发
}
return {
bindEvents,
url: window.location.hash.slice(1) || '/'
}
}
哈希值变化就会触发fn
的执行,fn
触发就会触发类中的箭头函数
好了,此时已经打造完了两个函数,如果这时你就去试试效果肯定是不行的,因为我们的写的源码最终在原index.js
抛到main.js
引入,想要被use
成功还需要满足一定的条件
所有能被vue
给use
掉的方法,内部一定要有install
方法,并且这个方法自带一个形参,就是vue
,我们一般叫成app
install
方法写入到构造函数中
kotlin
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)
})
}
install (app) {
console.log(app);
}
}
我们再来打印看看这个app长什么样子
我们需要用上里面的component
和provide
方法
provide
涉及到父子组件传参,因为这里需要有一个点击home
让router-link
跳转到对应的组件中,需要携带哈希值参数。子组件可以通过inject
来获取参数。provide
接收一个对象,key
我们命名为ROUTER_KEY
,值就是this
,class
中的this都是指向的class
本身,也就是构造函数Router
component
是让你注册组件用的。在App.vue
中,我们写了一个router-link
和一个router-view
。这两个其实就是组件,为何没有看到引入的代码,因为在路由中引入过了,只要路由生效,二者就生效
javascript
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
import { inject, ref } from 'vue'
const ROUTER_KEY = '_router_'
function useRouter() {
return inject(ROUTER_KEY)// 提供路由实例对象
}
class Router { // es6写构造函数
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)
})
}
install (app) {
console.log(app);
app.provide(ROUTER_KEY, this)
// 注册全局组件
app.component('router-link', RouterLink)
app.component('router-view', RouterView)
}
}
export {
createRouter,
createWebHashHistory,
useRouter,
}
useRouter
负责生成实例对象,inject
是将整个实例对象注入到了useRouter
中,能注入是因为有了provide
,provide
提供了带有整个Router
的key
这个时候已经可以被use
掉了,但是还是没有效果,因为RouerLink
和RouterView
也在vue-router
中,还得自己手搓
router-link
的本质就是个a
标签,router-link
组件中可以插入内容,那么这个组件一定用上了插槽slot
,并且router-link
含有to
属性。其实这就是父组件向子组件传值,App.vue
向RouterLink
传值
父子组件传值还不熟悉可以阅读我这篇vue组件通信【父子,子父,vuex】 - 掘金 (juejin.cn)
myRouter/RouerLink.vue
xml
<template>
<a :href="'#'+to">
<slot />
</a>
</template>
<script setup>
defineProps({
to: {
type: String,
required: true // 表示to为必传项
}
})
</script>
好了,接下来就是router-view
了,router-view
的作用就是负责让具体组件生效,路径映射组件
想要清楚当前路径,就需要引入自己刚刚写完的index.js
,用上它的useRouter
方法,提供路由实例对象,映射就用computed
计算属性来写,映射这里我用数组的find
方法来找,只要路径是当前的路径,就把组件返回出来
myRouter/RouterView.vue
xml
<template>
<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(() => {
// 数组映射关系
const route = router.routes.find((route) => {
return route.path === router.current.value
})
return route ? route.component : null
})
</script>
组件的使用有两种,一般都是直接写组件名,另一种就是这样写,component里面的is是组件名,这个写法常见于封装组件
此时已经完成了vue-router
哈希模式的效果,这里有个需要注意的点,就是目前的写法需要home
和about
的引入在外部引入,用component
得话,返回的是一个promise
对象,写法会麻烦点
完成
最后贴下代码,源码目录
myRouter/index.js
javascript
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
import { inject, ref } from 'vue'
const ROUTER_KEY = '_router_'
function useRouter() { // hooks高阶函数
return inject(ROUTER_KEY)// 提供路由实例对象
}
function createRouter (options) { // 创建路由实例对象,有push,go等方法
return new Router(options)
}
function createWebHashHistory () { // hash模式
function bindEvents(fn) {
window.addEventListener('hashchange',fn) // 只要hash变了, fn就会触发
}
return {
bindEvents,
url: window.location.hash.slice(1) || '/'
}
}
class Router { // es6写构造函数
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)
})
}
install (app) { // 所有能被vue use掉的
app.provide(ROUTER_KEY, this) // 类中,所有的this都指向类
// 注册全局组件
app.component('router-link', RouterLink)
app.component('router-view', RouterView)
}
}
export {
createRouter,
createWebHashHistory,
useRouter,
}
RouterLink.vue
xml
<template>
<a :href="'#'+to">
<slot />
</a>
</template>
<script setup>
defineProps({
to: {
type: String,
required: true // 该值必须传
}
})
</script>
RouterView.vue
xml
<template>
<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(() => {
// 数组映射关系
const route = router.routes.find((route) => {
return route.path === router.current.value
})
return route ? route.component : null
})
</script>
像是push
等方法这里就没写了,核心代码就以上这些,拿去面试是没有问题的
最后
vue
源码系列,路由是算是入门级的,实现起来算是比较简单
其实总结下就是写了两个函数,然后需要让其能够被use而写了个install,然后需要最终的效果再封装了两个组件routerlink
和routerview
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!
本次学习代码已上传至本人GitHub学习仓库:github.com/DolphinFeng...