Router路由组件底层原理原来这么有趣!

271 阅读6分钟

引言

众所周知路由 Router 在Vue中是特别重要的,是我们构建单页应用(SPA)必不可少的。我们经常使用但可能对其底层是如何实现的所知甚少。在本篇文章,我们将去解读Router的底层原理,手动去实现Router-linkRouter-view组件。

初始化Vue应用

首先,我们先在 main.js 中引入和使用我们要实现的 router

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index'


const app =  createApp(App)
// use 方法挂载了路由
// vue 只负责 组件思想, mvvm , 响应式 等核心
// 其他的交给生态系统 一起开源 vue-router 是vue 生态中的路由模块
// vue 和它生态的对接呢? 就是这个use 方法
 app.use(router)
    .mount('#app')
    

app.use() 调用是 Vue 插件系统的核心,它的作用是将实例挂载到整个 Vue 应用上。这里实现了路由的注册和全局组件的使用。也正是因为use接口功能强大,极大地丰富了Vue的生态。

然后我们在src目录下新建一个pages文件夹用于存放视图组件。

Home.vue

<template>
    <div>
            Home
    </div>
</template>

<script setup>

</script>

<style lang="scss" scoped>

</style>

About.vue

<template>
    <div>
            About
    </div>
</template>

<script setup>

</script>

<style lang="scss" scoped>

</style>

Router路由

Router 初始化

我们在src目录下新建一个router文件夹,在该目录下新建一个index.js用于配置路由表

import { createRouter, createWebHashHistory } from './grouter/index'
import Home from '../pages/Home.vue'
import About from '../pages/About.vue'



const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    }, 
    {
        path: '/about',
        name: 'About',
        component: About
    }
]

const router = createRouter({
    history: createWebHashHistory(),
    routes
})

export default router   

我们定义了路由配置并创建了路由实例。

  • routes:定义了一个包含两个路由对象的数组,每个对象代表一个路由配置。path 表示路径,name 表示路由名称,component 表示该路径对应的组件。

  • createRouter:使用 createRouter 函数创建路由实例,并传入 history 对象和路由配置。

Router 构建

然后在该目录下再新建一个 grouter 文件夹,新建两个vue文件。

RouterLink.vue

RouterView.vue

router-link 实现

<template>
    <a :href="'#' + props.to">
        <!-- 插槽 -->
        <slot />  
    </a>
</template>

<script setup>
        import {defineProps} from "vue";
const props = defineProps({
    to: {
        type: String,
        required: true

    }
})

</script>

<style lang="scss" scoped>

</style>
  • <a :href="'#' + props.to">:生成一个 a 标签,href 属性的值是 props.to 前面加上 #。这意味着该链接会导航到指定的哈希路径。

  • <slot />:插槽允许在使用 router-link 组件时,自定义链接的内容。就是渲染双标签中间的文本内容。

router-view 实现

再新建一个index.js文件。

首先我们定义一个Router 对象

class Router {
    constructor(options) {
        
        this.history = options.history
        this.routes = options.routes
        this.current = ref(this.history.url)
        this.history.bindEvents(() => {
            // console.log('//////////')
            this.current.value = window.location.hash.slice(1) 


        })
    }

    // use 调用 插件install
    install(app) {
        // 全局声明有一个router  全局使用的对象
        app.provide(ROUTER_KEY, this)
        console.log('准备与vue 对接', app)
        app.component('router-link', RouterLink)
        app.component('router-view', RouterView)

    }
}

constructor(options):构造函数接收一个 options 参数,用于初始化路由实例。this.history 保存了历史记录对象,this.routes 保存了路由配置,this.current 是一个响应式引用,保存当前的 URL。然后绑定了 hashchange 事件,当 URL 的 hash 部分发生变化时,更新 this.current 的值。

这里的 install(app) 是 Vue 插件的标准方法。当我们使用 app.use(router) 时,这个方法会被调用。

  • app.provide(ROUTER_KEY, this):通过 provide 方法,我们可以将 Router 实例注入到整个应用中,这样任何组件都可以通过 inject 方法访问到路由实例。

  • app.component('router-link', RouterLink)app.component('router-view', RouterView):注册全局组件 router-linkrouter-view,使它们在整个应用中都可以使用。

// 标记一下  router 要向全世界暴露  常量所以名称要大写
const ROUTER_KEY = '__router__'

// use 开头的是一派 hooks 函数式编程
export const useRouter = () => {
        return inject(ROUTER_KEY)
}

在 Vue.js 应用中提供一个便捷的方式来访问 Router 实例。通过调用这个函数,用户可以在任何组件中获取到 Router 实例,并进而访问实例上的属性和方法。

// 单例的责任
export const createRouter = (options) => {
    return  new Router(options)
}

// 提供一种灵活的方式来注册和响应浏览器的 hashchange 事件
export const createWebHashHistory = () => {
    function bindEvents(fn) {
        window.addEventListener('hashchange', fn)
    }
    // history 对象
    return {
        url: window.location.hash.slice(1) || '/',
        bindEvents
    }
}
  • createRouter 函数:该函数用于创建一个 Router 实例,接收一个 options 参数,该参数包含了路由的配置。

  • createWebHashHistory 函数:该函数用于创建一个带有 hash 路由模式的历史记录对象。bindEvents 方法用于绑定 hashchange 事件,这样当 URL 的 hash 部分发生变化时,可以触发相应的回调函数。

然后我们再回到 RouterView.vue

<template>
    <!-- 动态组件 -->
    <component :is="component" />
</template>

<script setup>
    import {computed} from 'vue' 
    // ref 是私有的     props 是父组件向子组件传递数据       computed 根据其他数据派生新数据,有个计算过程,且这个属性会根据其他数据变化而变化
    import Home from '../../pages/Home.vue'
    import About from '../../pages/About.vue'
    import { useRouter } from './index.js';
    const component = computed(() =>{
       const route = router.routes.find(
        (route) => route.path == router.current.value
       )
       console.log(route,'////');
       return route? route.component: null
       // 错误的route则不渲染

    })

    // router-view 动态组件 展示 依赖于 url 的变化
    // 响应式 router.current  设置为ref

const router = useRouter();
console.log(router);



</script>

<style lang="scss" scoped>

</style>

这里使用 Vue 的动态组件<component :is="component" />功能,根据 component 变量的值来渲染不同的组件。

  • useRouter:通过 useRouter 函数获取路由实例。

  • computed:定义了一个计算属性 component,根据当前的路径 (router.current.value),查找匹配的路由对象,并返回相应的组件。如果找不到匹配的路由,则返回 null

完整代码

grouter中的 index.js

import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'
import {ref, inject} from 'vue'


// 单例的责任
export const createRouter = (options) => {
    return  new Router(options)
}

// 提供一种灵活的方式来注册和响应浏览器的 hashchange 事件
export const createWebHashHistory = () => {
    function bindEvents(fn) {
        window.addEventListener('hashchange', fn)
    }
    // history 对象
    return {
        url: window.location.hash.slice(1) || '/',
        bindEvents
    }
}


// export const createWebHistory = () => {
//     return {
//         url: window.location.pathname,
//         bindEvents(fn) {
//             window.addEventListener('popstate', fn)
//         }
//     }
// }


// 标记一下  router 要向全世界暴露  常量所以名称要大写
const ROUTER_KEY = '__router__'

// use 开头的是一派 hooks 函数式编程
// 在 Vue.js 应用中提供一个便捷的方式来访问 Router 实例。通过调用这个函数,用户可以在任何组件中获取到 Router 实例,并进而访问实例上的属性和方法。
export const useRouter = () => {
        return inject(ROUTER_KEY)
}


class Router {
    constructor(options) {
        
        this.history = options.history
        this.routes = options.routes
        this.current = ref(this.history.url)
        this.history.bindEvents(() => {
            // console.log('//////////')
            this.current.value = window.location.hash.slice(1) 


        })
    }

    // use 调用 插件install
    install(app) {
        // 全局声明有一个router  全局使用的对象
        app.provide(ROUTER_KEY, this)
        console.log('准备与vue 对接', app)
        app.component('router-link', RouterLink)
        app.component('router-view', RouterView)

    }
}

效果展示

最后我们在 App.vue 中使用我们注册到全局的 <router-link><router-view>组件

<script setup>

</script>

<template>
  <header>
    <nav>
      <router-link to="/">首页</router-link>    
      <router-link to="/about">About</router-link>
      <!-- 其中首页和About就是插槽对应的 -->
    </nav>
  </header>
  <main>
    <router-view />
  </main>
  <footer>
  </footer>
</template>

<style scoped>

</style>

2v1qx-tp6yv.gif

总结

我们通过讲解逐步实现一个简单的 Vue 路由系统,展示了 router-linkrouter-view 两个核心组件的实现过程。我们从 main.js 开始,讲解了 app.use(router) 的作用,并深入分析了 router/grouter/index.jsRouter 类的实现细节。通过自定义的 Router 类,我们实现了 URL 的响应式管理和路由组件的动态渲染。最终,我们结合具体代码示例,详细说明了 router-linkrouter-view 组件的实现方式。