手撕 Vue-Router,City不City (上)

317 阅读7分钟

让面试官为你停留 —— Vue 应用中如何实现简易版的 Vue-Router(上篇)

(一)介绍

在前端开发中,路由管理是维持页面状态、导航和组件渲染的核心功能之一。虽然 Vue.js 生态系统中 Vue Router 是处理路由的官方库,但理解其底层原理是提高开发技能的重要步骤。在这篇文章中,我们将逐步构建一个简化版的 Vue 路由器(grouter),帮助读者更深入地理解路由机制。

实现思路:

1. 规划路由器核心功能
  • 路由注册与初始化
  • 路由响应与视图更新
  • History管理(我们选择了hash模式进行实现)
2. 设计组件接口
  • RouterLink:处理导航链接。
  • RouterView:根据当前路由动态渲染对应组件。
3. 开发History管理器
  • 封装变更URL和监听URL变化的功能。
4. 实现Router类
  • 管理路由表。
  • 监听URL变化并更新视图。
5. 集成到Vue中
  • 提供插件安装方法,使用app.use(router)
接下来,让我们开启自定义 Vue 路由器的实现吧。

(二)项目结构与主要文件

在这个示例中,我们构建了一个简化版的Vue-Router,包括以下几个关键文件:

  1. grouter/index.js - 定义和创建路由实例。
  2. App.vue - 根组件,展示如何使用路由链接与视图。
  3. main.js - Vue应用入口文件,初始化Vue应用并使用路由。
  4. RouterLink.vueRouterView.vue - 表现层组件,分别对应路由的导航与显示。
  5. router/grouter/index.js - 手写的简易路由核心逻辑。

(三)导入关键组件 和 Vue API工具

router\grouter\index.js 页面

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

(四)前期准备工作

router\grouter\index.js 页面

const ROUTER_KEY = '__router__'

// 在任何地方,就可以拿到 router 对象(简洁方便)
const useRouter = () => {
    return inject(ROUTER_KEY)
}
// 封装 VueRouter
const createRouter = (options) => {
    return new Router(options)
}

1. Composition API

  • useRouter 函数:这是一个组合式函数,它使用了Vue的inject函数。inject允许组件从父组件或祖先组件中注入一个值,而无需显式地通过props传递。在这个例子中,useRouter函数返回了注入的ROUTER_KEY对应的值,即Vue Router的实例。

2. 依赖注入(Dependency Injection)

依赖注入是一种设计模式,用于管理组件之间的依赖关系。在Vue中,依赖注入是通过provideinjectAPI实现的:

  • ROUTER_KEY 常量:定义了一个依赖注入的键,用于在组件树中标识Vue Router的实例。
  • inject 函数:在useRouter函数中使用,从组件的上下文中检索与ROUTER_KEY相关联的值。

3. 类的实例化

  • createRouter 函数:这个函数用于创建和返回一个新的Router实例。在Vue Router中,Router类负责管理路由规则、历史记录等。createRouter函数接收一个配置对象options,并使用这些选项来创建一个具体的Router实例。

(五)创建 Router 实例

router\grouter\index.js 页面

首先,我们需要定义 Router 类与 createRouter 函数。Router 类负责处理路由的安装和当前路由的管理,而 createRouter 是用来实例化路由对象的工厂函数:

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) {
        app.provide('__router__', this);
        app.component('router-link', RouterLink);
        app.component('router-view', RouterView);
    }
}

1. 使用 Composition API 进行状态管理

  • ref 的使用ref 是 Vue 3 Composition API 的一部分,用于创建响应式引用。在这段代码中,ref 用于创建一个响应式的 current 属性,它将根据 URL 的 hash 变化而自动更新。这是 MVVM 模式中“ViewModel”层的一部分,它负责将 Model(即 URL hash)的变化反映到 View 中。

2. 事件监听与响应

  • bindEvents 方法的调用:通过 this.history.bindEvents 方法,代码注册了一个事件监听器,监听 hashchange 事件。当 URL 的 hash 发生变化时,事件监听器将被触发,更新 current 的值。这展示了如何在 Vue.js 中处理外部事件并保持状态的响应式更新。

3. Vue Router 的核心概念

  • 路由规则this.routes 存储了路由规则,这些规则将被用于决定在给定的 URL 下应该显示哪个组件。这是 Vue Router 的核心功能之一。

  • 历史管理this.history 对象负责管理浏览器的历史记录,包括 URL 的变化。Vue Router 支持两种历史模式:hash 和 history。这里使用的是 hash 模式。

4. Vue 应用的扩展与组件注册

  • app.provide 的使用:通过 app.provide 方法,将 Router 实例注入到 Vue 应用的上下文中。这使得任何组件都可以通过 inject 接收这个路由器实例,从而访问其功能,如导航和路由信息。

  • 全局组件注册app.component 方法用于注册全局组件。在这段代码中,注册了 router-linkrouter-view 组件,它们是 Vue Router 中用于创建导航链接和渲染路由组件的关键组件。

(六)定义 Hash 历史

router\grouter\index.js 页面

以下这段代码定义了一个函数 createWebHashHistory,用于创建一个基于浏览器 URL hash 的历史管理器。这个管理器主要用于处理和监控 URL hash 的变化,路由的 hash 模式利用了 URL 中的 hash(井号标记)来模拟完整的 URL 路径,这对于构建使用 hash 模式的历史跟踪的单页应用(SPA)是非常重要的。

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

1. 模块化与封装

  • 这段代码定义了一个 createWebHashHistory 函数,该函数返回一个对象,封装了与浏览器 URL hash 相关的操作。这种封装使得代码更易于管理和复用,同时也隐藏了实现细节,提供了清晰的接口。

2. 事件监听

  • bindEvents 函数用于绑定 hashchange 事件。这个事件在 URL 的 hash 部分发生改变时触发。通过使用 window.addEventListener 方法,代码能够监听到这个事件,并执行传入的回调函数 fn。这展示了如何在 JavaScript 中处理事件,以及如何将事件处理逻辑与其它业务逻辑分离。

3. 对象字面量与方法暴露

  • 返回的对象包含两个属性:url 和 bindEvents 方法。

    • url 属性获取当前 URL 的 hash 部分(如果存在),并将其作为字符串返回,如果没有 hash,则返回 '/'。这展示了如何从 window.location 对象中提取信息。
    • bindEvents 方法则被直接定义为对象的一个属性,它允许外部代码通过这个对象来绑定 hashchange 事件的监听器。

4. 字符串操作

  • 使用 window.location.hash.slice(1) 来获取 URL 的 hash 部分。
    • window.location.hash:这返回当前 URL 的 hash 部分,也就是 URL 中 # 符号后面的部分。
    • .slice(1):这个方法从字符串的第二个字符开始截取,直到字符串结束。在这个上下文中,它会移除 hash 部分的开头 # 字符,留下 /home
  • || '/':这是一个逻辑或运算符。如果 window.location.hash.slice(1) 返回的是一个非空字符串,那么这个表达式将返回这个字符串。但如果它是空字符串(意味着 URL 没有 hash 部分),那么它将返回 '/'。这确保了即使没有 hash 部分,应用也能有一个默认的路由路径。

(七)总结

如此以来,我们就完成了手写Vue-Router中最核心的板块 —— 手写简易路由核心逻辑。

这段代码实现了基本的 Vue Router 功能,包括创建 Router 实例、监听 URL hash 变化、定义路由规则、提供 Router 实例给 Vue 应用、以及注册必要的组件。通过使用 Composition API 和 Vue Router 的核心组件,这段代码允许你创建和管理单页应用中的路由。

(八)源码

router/grouter/index.js

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

const ROUTER_KEY = '__router__'

// 在任何地方,就可以拿到 router 对象(简洁方便)
const useRouter = () => {
    return inject(ROUTER_KEY)
}
// 封装 VueRouter
const createRouter = (options) => {
    return new Router(options)
}
// 返回一个 hash 路由对象
// url #/about 
// hashChange
const createWebHashHistory = () => {
    function bindEvents(fn) {
        window.addEventListener('hashchange', fn)
    }
    return {
        url: window.location.hash.slice(1) || '/',
        bindEvents
    }
}
class Router {
    constructor(options) {
        this.history = options.history
        this.routes = options.routes
        console.log(options, '////');
        // 当前的 url 状态,是 router-view component 计算属性的依赖
        this.current = ref(this.history.url)
        this.history.bindEvents(() => {
            // this 指向 Router
            this.current.value = window.location.hash.slice(1)
            console.log(this.current.value);
        })
    }
    install(app) {
        console.log(app);
        // console.log('vue 要对接 vue-router');
        // 全局组件的声明
        // 全局提供 router对象
        app.provide(ROUTER_KEY,this)
        app.component('router-link', RouterLink)
        app.component('router-view', RouterView)
    }
}
export {
    createRouter,
    createWebHashHistory,
    useRouter
}