面试官:手写一个vue-router Hash模式

前阵子发了一篇文章,讲的是前端路由的实现原理,反响还不错。今天我们继续来手写一个vue-routerhash模式,前端路由包括了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)

我们会在这个构造函数中传入的对象有historyroutes两个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成功还需要满足一定的条件

所有能被vueuse掉的方法,内部一定要有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长什么样子

我们需要用上里面的componentprovide方法

provide涉及到父子组件传参,因为这里需要有一个点击homerouter-link跳转到对应的组件中,需要携带哈希值参数。子组件可以通过inject来获取参数。provide接收一个对象,key我们命名为ROUTER_KEY,值就是thisclass中的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中,能注入是因为有了provideprovide提供了带有整个Routerkey

这个时候已经可以被use掉了,但是还是没有效果,因为RouerLinkRouterView也在vue-router中,还得自己手搓

router-link的本质就是个a标签,router-link组件中可以插入内容,那么这个组件一定用上了插槽slot,并且router-link含有to属性。其实这就是父组件向子组件传值,App.vueRouterLink传值

父子组件传值还不熟悉可以阅读我这篇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哈希模式的效果,这里有个需要注意的点,就是目前的写法需要homeabout的引入在外部引入,用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,然后需要最终的效果再封装了两个组件routerlinkrouterview

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!

本次学习代码已上传至本人GitHub学习仓库:github.com/DolphinFeng...

相关推荐
程序媛小果11 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
吖秧吖2 小时前
three.js 杂记
开发语言·前端·javascript
前端小超超2 小时前
vue3 ts项目结合vant4 复选框+气泡弹框实现一个类似Select样式的下拉选择功能
前端·javascript·vue.js