手撕vue-router - 源码分析
前言
vue-router
是Vue.js官方提供的路由管理器,专门设计用于构建单页面应用(Single Page Application, SPA)。在单页面应用中,多个“页面”状态可以在同一个HTML文档中呈现,无需重新加载整个页面。
那么vue-router
是如何实现的呢,通过它的源码我们来手写一下vue-router包(实现router-link、router-view
组件
vue-router
使用步骤
1. 安装 vue-router
如果你使用的是Vue创建的项目,可以通过以下命令安装vue-router
:
npm install vue-router
# 或者使用yarn
yarn add vue-router
2. 配置路由
在你的项目中创建一个router
目录,并在其中创建一个index.js
或index.ts
文件(如果你的项目支持TypeScript)作为入口文件。
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(),
routes,
});
export default router;
在这个文件中,你需要导入createRouter
和createWebHistory
/createWebHashHistory
函数,并定义你的路由。
routes
数组包含了所有的路由配置。每个路由配置对象至少包含一个path
和一个component
属性。
createWebHistory
和createWebHashHistory
的区别
在Vue Router中,createWebHistory
和createWebHashHistory
是用于创建不同路由模式的函数。这两种模式的主要区别在于它们如何处理和呈现URL,以及它们对服务器配置的要求。
-
createWebHashHistory
:-
这个模式使用URL的哈希部分(即#后面的部分)来保存路由信息。例如,如果你的页面是在
https://example.com/
,并且你导航到一个内部路由如/about
,URL会变成https://example.com/#/about
。 -
由于URL的哈希部分不会被发送到服务器,这意味着在服务器上不需要任何特殊的配置来处理这些路由。即使服务器不知道这些路由的存在,页面也能正确加载。
-
哈希模式通常在服务器端没有正确配置的情况下使用,或者在无法控制服务器配置的环境中使用,比如GitHub Pages。
-
-
createWebHistory
:-
这个模式使用HTML5的
History API
(包括pushState
和replaceState
)来改变浏览器的URL,并且URL中不会包含哈希符号。例如,对于相同的/about
路由,URL会显示为https://example.com/about
。 -
使用历史模式时,服务器需要配置来处理这些URL,因为它们看起来像普通的文件或目录路径。如果没有正确的配置,尝试访问这些URL可能会导致404错误,因为服务器可能试图查找并不存在的文件或目录。
-
历史模式提供了更干净的URL,但是需要服务器端的配合才能工作得当。
-
3. 在主应用中使用路由
在你的main.js
或main.ts
文件中,导入刚刚创建的路由器,并将其添加到Vue应用的实例中。
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");
手撕vue-router —— grouter
上述我们已知通过下载一个vue-router
包,现在我们来仿写一个这样的包(grouter
)实现路由功能。
在router/index.js
入口文件中改变导入包资源,不再使用vue-router,采用仿写的grouter.

index.js(grouter入口文件)
// 导入Vue组件和Vue提供的响应式工具
import RouterLink from "./RouterLink.vue";
import RouterView from "./RouterView.vue";
import { ref, inject } from "vue";
// 定义一个常量用于提供和注入router对象的key
const ROUTER_KEY = "router";
// 在任何地方,使用 useRouter 就可以拿到router 对象
const useRouter = () => {
return inject(ROUTER_KEY); // 使用Vue的inject函数获取到注入的router对象
};
// 创建路由器的工厂函数,接收配置选项并返回一个新的Router实例
const createRouter = (options) => {
return new Router(options); // 返回一个新的Router实例
};
// 创建基于Web Hash的历史记录对象的工厂函数
const createWebHashHistory = () => {
// 内部函数,用于绑定hashchange事件
function bindEvents(fn) {
window.addEventListener("hashchange", fn); // 监听浏览器地址栏hash变化事件
}
// 返回一个对象,包含当前URL和绑定事件的方法
return {
url: window.location.hash.slice(1) || "/", // 获取当前的hash值,作为初始url,如果不存在则默认为"/"
bindEvents, // 返回绑定事件的方法
};
};
// 定义一个Router类,用于管理路由
class Router {
constructor(options) {
console.log(options); // 输出传入的配置选项
this.history = options.history; // 设置路由历史管理对象
this.routes = options.routes; // 设置路由配置数组
// 当前URL的状态,使用Vue的ref函数创建响应式引用
this.current = ref(this.history.url);
// 绑定hashchange事件,监听URL变化并更新当前URL的状态
this.history.bindEvents(() => {
this.current.value = window.location.hash.slice(1); // 更新当前URL的值
});
}
// 安装方法,用于将路由器集成到Vue应用中
install(app) {
console.log("vue 对接"); // 输出日志,表示开始集成到Vue应用中
// 提供router对象,使其可以在应用的任何地方通过useRouter获取到
app.provide(ROUTER_KEY, this);
// 声明全局组件,使得<router-link>和<router-view>可以在应用中使用
app.component("router-link", RouterLink);
app.component("router-view", RouterView);
}
}
// 导出模块中定义的函数和类,供其他地方使用
export { createRouter, createWebHashHistory, useRouter };
-
响应式 URL 状态管理:
- 使用 Vue 的
ref
函数创建响应式的current
属性,用于跟踪当前的 URL。 - 当 URL 发生变化时,通过监听
hashchange
事件更新current
的值。
- 使用 Vue 的
-
依赖注入:
- 使用
provide
和inject
实现依赖注入,使得router
对象可以在 Vue 应用的任何地方使用。
- 使用
-
全局组件注册:
- 在
Router
类的install
方法中,使用app.component
注册全局组件<router-link>
和<router-view>
,使得这些组件可以在应用中随处使用。
- 在
-
路由器工厂函数:
- 提供
createRouter
和createWebHashHistory
工厂函数,方便创建和配置路由器实例及历史记录对象。
- 提供
-
事件绑定:
- 通过
bindEvents
函数绑定hashchange
事件,实时监听 URL 的变化并做出相应更新。
- 通过
Router对象:
router-link
<template>
<!-- 动态生成的链接,根据传入的 'to' 属性设置 href -->
<a :href="'#' + props.to">
<!-- 插槽,用于插入内容 -->
<slot></slot>
</a>
</template>
<script setup>
import { defineProps } from "vue";
// 定义组件的 props
const props = defineProps({
to: {
type: String,
required: true, // 'to' 属性是必须传入的
},
});
</script>
<style lang="css" scoped></style>
自定义的 router-link
组件实现:
-
动态生成链接
- 使用绑定语法
:href="'#' + props.to"
,可以根据传入的to
属性动态生成链接。这使得组件可以灵活地用于不同的链接目标,不需要在使用时写死链接路径。
- 使用绑定语法
-
插槽的使用
- 插槽
<slot></slot>
允许组件用户在使用组件时插入任意内容。这样,用户可以在组件内自定义链接文本或嵌入其他 HTML 元素,增强组件的灵活性和可重用性。
- 插槽
-
定义属性
- 使用
defineProps
函数定义组件的props
,确保组件可以接收和验证外部传入的属性。通过设置required: true
,确保组件在使用时必须传入to
属性,从而避免因为缺少必要属性导致的错误。
- 使用
router-view
<template>
<!-- 动态组件渲染,根据计算属性 'component' 动态渲染对应的组件 -->
<component :is="component"></component>
</template>
<script setup>
import { computed } from "vue"; // 导入 Vue 的 computed 函数
import { useRouter } from "./index"; // 导入自定义的 useRouter 函数
// 获取全局路由对象
let router = useRouter();
// 动态响应式的计算属性,根据当前 URL 动态计算出要渲染的组件
const component = computed(() => {
// 在路由配置中查找当前路径匹配的路由
const route = router.routes.find(
(route) => route.path === router.current.value
);
// 返回匹配的组件,如果没有匹配则返回 null
return route ? route.component : null;
});
</script>
<style lang="css" scoped></style>
自定义 router-view
组件的实现结合了 Vue 3 的 Composition API 和动态组件渲染,使得视图能够根据当前 URL 动态渲染对应的组件。
-
动态组件渲染: 根据当前 URL 动态渲染对应的组件,使用
<component>
标签和计算属性component
实现。 -
响应式路由管理: 使用 Vue 的 Composition API 中的
computed
函数创建响应式计算属性,根据路由对象的当前状态动态更新视图。 -
依赖注入: 使用
useRouter
函数通过依赖注入获取全局路由对象,使得路由对象在任何组件中都可以方便地访问和使用。
效果
总结
最后,我们就手写实现了grouter
包(router-link、router-view
组件),实现简单的路由功能。但是实际的实现更为复杂精细,有兴趣的小伙伴可以去阅读全部的源码。深入阅读和理解一个完整的路由器的源码可以帮助开发者扩展视野,学习设计模式和最佳实践,并且理解复杂系统的组织和实现方式。
转载自:https://juejin.cn/post/7390666931428638731