手搓vue-router,拿捏其工作机制

620 阅读4分钟

在单页应用流行的当下,路由的使用是非常优雅非常的爽。在这段时间的学习里,对它的工作机制有了一定的认识,所以我准备结合之前介绍的哈希路由手搓一个router。

话不多说,开干。

目录结构

image.png

首先,先设置配置两个路由页面,并在单页上可以点击跳转。

App.vue

<template>
  <header>
    <nav>
      <router-link to="/">首页</router-link>
      <router-link to="/about">About</router-link>
    </nav>
  </header>
  <router-view/>
</template>

<script setup>
// import { RouterView } from 'vue-router';
</script>
<style lang="css" scoped>
</style>

router/index.js

import { createRouter,createHashHistory } from './grouter/index'
import Home from '../pages/Home.vue'
import About from '../pages/About.vue'
const router = createRouter(
  {
  history:  createHashHistory(),
  routes: [
    {
      path:'/',
      name:'Home',
      component:Home
    },
    {
      path:'/about',
      name:'About',
      component:About
    }
  ]
}
)
export default router

理解app.use(router)

以上的准备工作做完之后,就该是引入正题了,为让路由全局实现,我们应在main.js中引入路由插件。那么为了实现手写router我们就应该了解到,app.use(router)干了什么,我们来到router官网vue官网上查看。

image.png

image.png

创建路由管理器

理解app.use(router)的作用后,再结合我们平时使用的useRoutercreateRouter,可以发现只有一个router对象对全局路由进行管理(即单例模式),并且具有install()方法和构造函数初始化路由。除了这些外,路由管理文件中应该还需要抛出createRoutercreateHashHistoryuseRouter

  • install():这个方法是Router类的一个实例方法,用于将Router实例安装到vue应用中。它使用provideRouter实例提供给子组件,以及利用component注册RouterLinkRouterView组件到vue应用中,使它们成为全局可用的组件。

  • createRouter:这个函数用于创建一个新的Router实例。它接收一个包含路由配置的Options对象作为参数,并返回一个新创建的Router实例。

  • createHashHistory:这个函数用于创建一个基于浏览器哈希历史的路由历史对象。它绑定事件监听器以监听hashchange事件,当URL的哈希部分发生改变时,事件处理器会被触发,更新当前路由的URL。

  • useRouter:这个函数用于从vue应用中获取Router实例。它使用inject函数来获取在应用中被提供的Router实例(实例将在router对象中的install方法中利用provide将实例曝光在全局中)。

grouter/index.vue完整代码

// 导入 RouterLink 组件
import RouterLink from "./RouterLink.vue";
// 导入 routerView 组件
import routerView from "./RouterView.vue";
// 导入 inject 和 ref 函数
import { inject, ref } from "vue";

// 单例的责任
// 创建一个 Router 实例的工厂函数
export const createRouter = (Options) => {
  return new Router(Options);
}

// 创建一个处理 URL 哈希变化的 history 对象
export const createHashHistory = () => {
  // 绑定事件处理函数的辅助函数
  function bindEvents(fn) {
    window.addEventListener('hashchange', fn);
  }
  // history 对象
  return {
    url: window.location.hash.slice(1) || '/',
    bindEvents
  }
}
// 标记一下 router 要向全世界暴露
const ROUTER_KEY = "__router__";
// use 开头的是一派 hooks 函数式编程

// 获取全局注入的 Router 实例的 hooks 函数
export const useRouter = () => {
  return inject(ROUTER_KEY);
}

// Router 类
class Router {
  constructor(Options) {
    // 初始化 history 对象
    this.history = Options.history;
    // 初始化路由表
    this.routes = Options.routes;
    // 使用 ref 来创建一个响应式的数据 current
    this.current = ref(this.history.url);
    // 绑定 hashchange 事件,更新 current
    this.history.bindEvents(() => {
      // console.log('///');
      this.current.value = window.location.hash.slice(1) || '/';
    })
  }

  // 用于在 Vue 应用中安装 Router 实例的方法
  install(app) {
    // 提供一个全局的 Router 实例,供所有组件注入和使用
    app.provide(ROUTER_KEY, this);
    console.log('准备与路由vue 对接', app);
    // 在应用中注册 router-link 组件
    app.component('router-link', RouterLink);
    // 在应用中注册 router-view 组件
    app.component('router-view', routerView);
  }
}

RouterLink组件实现

router-link组件能够处理客户端路由,当用户点击链接时,它会更新浏览器的URL并重新渲染对应的组件,而不需要重新加载整个页面。所以我们需要获取到需要跳转的链接,使用defineProps接收父组件传来的链接地址,再利用<slot />显示父组件传递给这个链接组件的内容。

grouter/RouterLink.vue完整代码

<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="css" scoped>
</style>

RouterView组件实现

router-view是实现路由跳转后的页面渲染。所以我们使用Vue中的动态组件语法,用:is绑定告诉Vue要渲染的组件是由component计算属性决定的。当component的值改变时,这个组件会重新渲染,显示与当前路由相匹配的组件。

<template>
  <!-- 使用 :is 指令动态渲染组件 -->
  <component :is="component"></component>
</template>

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

const router = useRouter();
// router-view 动态组件 展示 依赖于url的变化

// 响应式 router.current 设置为 ref
const component = computed(() => {
  // 在路由表中查找匹配当前 URL 的路由记录
  const routes = router.routes.find(
    // 如果有路由的路径与当前的 URL 匹配,则返回该条记录
    (route) => route.path == router.current.value
  )
  console.log(routes);
  // 如果找到匹配的路由记录,则返回该记录的组件,否则返回 null
  return routes? routes.component : null;
})
</script>

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

总结

虽然上述代码只是一个简化的示例,但它展示了构建一个基本的路由系统的步骤。在实际项目中,vue-router提供了更丰富的功能,如嵌套路由、命名视图、历史模式等,这些都是手写版本所不具备的。然而,通过手写实现,我们可以更好地理解路由背后的机制,这对于深入学习框架的高级特性和优化应用性能都大有裨益。