【Vue源码】带你手搓一个路由

329 阅读4分钟

前言

路由大家都很熟悉,是vue构建单页应用的核心。使用方法也比较的容易,那么我们如何打造一个自己的路由呢?

正文

初始化

先简单的创建的一个vue项目,安装路由的依赖。

npm init vite
npm i 
npm i vue-router@4

我们就以一个首页和关于页面来切入。并分别给它们配置路由。

1. src/views/Home.vue

<template>
    <div>
        home page
    </div>
</template>

<script setup>
</script>

<style lang="css" scoped>
</style>

2. src/views/About.vue

<template>
    <div>
        关于页面
    </div>
</template>

<script setup>
</script>

<style lang="css" scoped>
</style>

3. src/router/index.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(), //路由模式:history、hash模式
    routes
})

export default router

4. 再去main.js目录下use掉

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'


const app = createApp(App)
app
.use(router)
.mount('#app')

3. 最后去根组件App.vue

<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>

打造路由库

这样路由就配置好了,那么我们现在的目标就是打造一个路由库,所以创建一个新的目录:src/router/myRouter/index.js

那么我们在这个库中会打造三个重要路由方法: useRoutercreateRoutercreateWebHashHistory

1. 打造 createRouter 、createWebHashHistory函数

从配置的过程中,我们可以发现,createRouter方法用来创建路由实例,并接受一个对象作为参数。且对象中的history属性用于指定路由的模式,然后通过 routes 数组定义了路由规则,每个路由规则都包含 pathname和对应的组件 component

那么既然是创建路由实例,我们可以先创建一个路由的构造函数,构造函数定义几个实例变量储存传入的参数:

class Router {
    constructor (options){
        this.history = options.history
        this.routes = options.routes
    }
}

export function createRouter(options) {
    return new Router(options)
}

那么如何去管理路由的变化?通过createWebHashHistory方法创建一个基于hash模式的路由历史对象。所以我们在这个方法中要监听Hash值的变更:

export function createWebHashHistory() {
    function bindEvents(fn) {
        window.addEventListener('hashchange',fn)
    }
    return {
        bindEvents,
        url: window.location.hash.slice(1) || '/'
    }
}

window.addEventListener('hashchange',fn) 监听浏览器hash值的变更,一变就执行回调函数中的逻辑。

url: window.location.hash.slice(1) || '/' 获取当前浏览器的hash值,并通过slice'#'去除,如果当前页面没有 hash 值,则默认为根路径 /

再去构造函数中调用bindEvents方法:

class Router {
    constructor (options){
        this.history = options.history
        this.routes = options.routes
        this.current = ref(options.history.url) //获取hash值
        options.history.bindEvents(() => {
            this.current.value = window.location.hash.slice(1) //记录实时位置
        })
    }
}

current用来储存当前的hash值,hash值一改变就给current重新赋值。

2. 全局安装路由插件

在 Vue中,每个可以被 use 方法安装的插件都应该包含一个名为 install 的方法,该方法会在插件被安装时被调用。且该方法可以接受app(Vue实例)作为参数,并访问其身上的方法。

所以我们在路由构造函数内部添加一个 install 方法:

const ROUTER_KEY = '_router_'

class Router {
    constructor (options){
        this.history = options.history
        this.routes = options.routes
    }
    
    install(app) {
        app.provide(ROUTER_KEY,this)
    }
}

app.provide(ROUTER_KEY,this)使用vue内置的provide方法,将插件实例赋值给ROUTER_KEY这个变量,目的是使得在整个应用程序中都可以通过该全局变量访问到插件的实例。所以该方法可以用于打造useRouter方法。

useRouter方法用于在 Vue 组件中获取路由实例,内部通过inject方法注入组件中:

export function useRouter() {
    return inject(ROUTER_KEY)
}

打造router-view和router-link组件

在myRouter目录下创建两个vue组件

router-link组件

该组件用于路由跳转,且本质就是一个a标签。因为在父组件中该组件标签中可以写入内容,所以该组件具有一个插槽。并且我们可以通过defineProps接受父组件传入的参数to

<template>
    <a :href="'#' + to">
        <slot />
    </a>
</template>

<script setup>
defineProps({
    to: {
        type: String,
        required: true //必传
    }
})
</script>

router-view组件

该组件用于显示当前路由对应的组件内容:

<template>
    <component  :is="component"></component>
</template>

<script setup>
import { computed } from 'vue';
import { useRouter } from '../myRouter/index.js'

const router = useRouter() //在当前组件注入了router

const component = computed(() => {
    const route = router.routes.find((route) => {
        return route.path === router.current.value //路径相同
    })
    // console.log(route);
    return route ? route.component : null
})
</script>

使用component标签动态渲染组件,其中 :is 是动态组件的关键字,"component" 是一个表示组件名称的变量。

通过useRouter获取路由实例,访问路由配置数组routesroute.path === router.current.value是组件渲染的条件,就是当前页面的路径是否与某个路由配置对象中的路径完全匹配

找到匹配的对象就返回该对象中的对应的组件(route.component),没有找到就返回一个null。注意一定要用computed包裹,以至于每次路由跳转后都会重新执行一遍回调函数。

最后我们再去全局注册这两个组件 :src / router / myRouter / index.js目录下,调用app身上的component方法。

class Router {
    constructor(options) {
        this.history =  options.history
        this.routes = options.routes
        this.current =  ref(this.history.url)

        this.history.bindEvents(() => { //当hash值改变后
            this.current.value =  window.location.hash.slice(1) //记录实时位置
        })
    }

    install(app) {
        app.provide(ROUTER_KEY, this)

        //全局注册
        app.component('router-link',RouterLink)
        app.component('router-view',RouterView)
    }
}

现在,这个路由算是打造好了,我们再去把配置路由中引入的官方路由库改成我们自己打造的路由库,就能正常使用了!

最后

恭喜你对路由的理解又更加深刻了,如果觉得有帮助欢迎点赞关注!

源码地址