例子:vue3+vite+router创建多级导航菜单

第一部分

1、初始化项目

npm init vite@latest

npm run dev :运行项目

q+Enter:退出运行

image

image

 

 

 2、安装路由依赖

npm install vue-router@4  # Vue3 对应 vue-router 4.x 版本

 

第二部分:

创建页面组件

在 src/views/home/analytics 目录下创建两个页面组件:

日统计(daily.vue)

<template>
  <div class="page home-page">
    <h2>首页</h2>
    <p>这是网站首页内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.home-page {
  background-color: #f0f9ff;
}
</style>

 

周统计(weekly.vue)

<template>
  <div class="page home-page">
    <h2>首页</h2>
    <p>这是网站首页内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.home-page {
  background-color: #f0fff4;
}
</style>

 

在 src/views/home/dashboard 目录下创建三个页面组件:

概览 overview.vue

<template>
  <div class="page home-page">
    <h2>首页</h2>
    <p>这是网站首页内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.home-page {
  background-color: #f0f9ff;
}
</style>

 

趋势分析 trend.vue

<template>
  <div class="page home-page">
    <h2>首页</h2>
    <p>这是网站首页内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.home-page {
  background-color: #f0fff4;
}
</style>

 

来源分析 source.vue

<template>
  <div class="page home-page">
    <h2>首页</h2>
    <p>这是网站首页内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.home-page {
  background-color: #f0fff4;
}
</style>

 

在 src/views/user/permissions 目录下创建两个页面组件:

角色配置 roles.vue

<template>
  <div class="page about-page">
    <h2>角色管理</h2>
    <p>这是角色管理页面内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.about-page {
  background-color: #f0f9ff;
}
</style>

 

权限分配 assign.vue

<template>
  <div class="page about-page">
    <h2>权限设置</h2>
    <p>这是权限设置页面内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.about-page {
  background-color: #f0fff4;
}
</style>

 

在 src/views/user 目录下创建一个页面组件:

用户列表 list.vue

<template>
  <div class="page about-page">
    <h2>用户列表</h2>
    <p>这是用户列表页面内容</p>
  </div>
</template>

<style scoped>
.page {
  padding: 20px;
}
.about-page {
  background-color: #fff0f0;
}
</style>

创建导航组件

在 src/components 目录下创建导航栏组件 SubMenuRecursive.vue
<template>
  <ul class="menu-list">
    <li v-for="item in menuList" :key="item.name" class="menu-item">
      <!-- 菜单项内容 -->
      <div 
        class="menu-link" 
        :class="{ active: isActive(item) }"
        @click="handleClick(item)"
      >
        <span class="icon">{{ item.icon }}</span>
        <span class="text">{{ item.name }}</span>
        <!-- 三级菜单展开箭头:仅 SubMenu 可能有 children -->
        <span 
          class="arrow" 
          v-if="isSubMenu(item) && item.children && item.children.length"
          :class="{ 'arrow-open': isOpen(item) }"
        ></span>
      </div>

      <!-- 递归渲染三级菜单:仅 SubMenu 可能有 children -->
      <template v-if="isSubMenu(item) && item.children && item.children.length">
        <SubMenuRecursive 
          :menu-list="item.children" 
          :parent-path="getItemPath(item)"
          v-show="isOpen(item)"
        />
      </template>
    </li>
  </ul>
</template>

<script setup lang="ts">
import { ref } from "vue"; // 删除未使用的 computed
import { useRoute, useRouter } from "vue-router";
import type { SubMenu, ThirdMenu } from "../router/menu";
import SubMenuRecursive from "./SubMenuRecursive.vue"; // 补充自身导入(递归组件需要)

// 类型守卫:严格判断是否为 SubMenu(有 children 属性)
const isSubMenu = (item: SubMenu | ThirdMenu): item is SubMenu => {
  return 'children' in item && Array.isArray(item.children);
};

// 接收父组件传入的菜单列表和父路径
const props = defineProps<{
  menuList: (SubMenu | ThirdMenu)[];
  parentPath?: string;
}>();

const route = useRoute();
const router = useRouter();

// 记录展开状态(针对有子菜单的项)
const expandedKeys = ref<string[]>([]);

// 判断菜单项是否激活
const isActive = (item: SubMenu | ThirdMenu) => {
  const itemPath = getItemPath(item);
  return itemPath && route.path.startsWith(itemPath);
};

// 判断子菜单是否展开(仅 SubMenu 有效)
const isOpen = (item: SubMenu | ThirdMenu) => {
  return isSubMenu(item) && expandedKeys.value.includes(item.name);
};

// 获取菜单项的路径(用于路由跳转)
const getItemPath = (item: SubMenu | ThirdMenu) => {
  if (isSubMenu(item) && item.children && item.children.length > 0) { // 增加 length > 0 判断
    return item.children[0]!.path; // 此时 children[0] 一定存在
  }
  return item.path;
};

// 点击菜单项
const handleClick = (item: SubMenu | ThirdMenu) => {
  const itemPath = getItemPath(item);
  if (itemPath) {
    router.push(itemPath); // 跳转路由
  }
  // 若为 SubMenu 且有子菜单,切换展开状态
  if (isSubMenu(item) && item.children && item.children.length) {
    const index = expandedKeys.value.indexOf(item.name);
    if (index > -1) {
      expandedKeys.value.splice(index, 1);
    } else {
      expandedKeys.value.push(item.name);
    }
  }
};

// 初始化:默认展开当前路由对应的父菜单
const initExpanded = () => {
  const findParent = (list: (SubMenu | ThirdMenu)[]) => {
    list.forEach((item) => {
      // 仅处理 SubMenu 类型
      if (isSubMenu(item) && item.children && item.children.length) {
        const hasActiveChild = item.children.some(
          (child) => route.path.startsWith(child.path)
        );
        if (hasActiveChild) {
          expandedKeys.value.push(item.name);
        }
        findParent(item.children); // 递归查找子菜单
      }
    });
  };
  findParent(props.menuList);
};

initExpanded();
</script>

<style scoped>
/* 样式保持不变 */
.menu-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.menu-item {
  margin: 2px 0;
}

.menu-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 20px;
  color: #ecf0f1;
  cursor: pointer;
  transition: background-color 0.2s;
}

.menu-link:hover, .menu-link.active {
  background-color: #2c3e50;
}

.icon {
  width: 20px;
  text-align: center;
}

.arrow {
  margin-left: auto;
  font-size: 12px;
  transition: transform 0.2s;
}

.arrow-open {
  transform: rotate(90deg);
}

/* 三级菜单缩进 */
.menu-list .menu-list {
  padding-left: 20px;
  border-left: 1px dashed #4a6988;
}
</style>

 

在 src/layouts 目录下创建导航栏组件 MainLayout.vue
<template>
  <div class="layout-container">
    <!-- 顶部导航(一级菜单) -->
    <header class="top-nav">
      <div class="logo">Admin Panel</div>
      <nav class="main-menu">
        <router-link
          v-for="menu in mainMenus"
          :key="menu.path"
          :to="menu.path"
          class="main-menu-item"
          :class="{ active: isMainMenuActive(menu) }"
        >
          <span class="icon">{{ menu.icon }}</span>
          <span class="text">{{ menu.name }}</span>
        </router-link>
      </nav>
    </header>

    <div class="content-wrapper">
      <!-- 侧边栏(二级/三级菜单) -->
      <aside class="sidebar">
        <div class="sidebar-header">
          <h3>{{ currentMainMenu?.name }} 菜单</h3>
        </div>
        <nav class="sub-menu">
          <!-- 递归渲染二级/三级菜单 -->
          <SubMenuRecursive 
            :menu-list="currentSubMenus" 
          />
        </nav>
      </aside>

      <!-- 主内容区 -->
      <main class="main-content">
        <router-view />
      </main>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
import { useRoute } from "vue-router";
import type { MainMenu, SubMenu } from "../router/menu";
import { mainMenus } from "../router/menu";
import SubMenuRecursive from "../components/SubMenuRecursive.vue";

const route = useRoute();
// 先获取第一个菜单,不存在则用默认值
const firstMenu = mainMenus[0] || { name: "", path: "", subMenus: [] } as MainMenu;

// 用 firstMenu 初始化,避免 undefined
const currentMainMenu = ref<MainMenu | null>(firstMenu);
const currentSubMenus = ref<SubMenu[]>(firstMenu.subMenus);

// 检查一级菜单是否激活
const isMainMenuActive = (menu: MainMenu) => {
  return route.path.startsWith(menu.path);
};

// 路由变化时更新当前菜单
watch(
  () => route.path,
  (newPath) => {
    const matchedMainMenu = mainMenus.find((menu) => 
      newPath.startsWith(menu.path)
    );
    if (matchedMainMenu) {
      currentMainMenu.value = matchedMainMenu;
      currentSubMenus.value = matchedMainMenu.subMenus;
    }
  },
  { immediate: true }
);
</script>

<style scoped>
.layout-container {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  color: #333;
}

/* 顶部导航 */
.top-nav {
  display: flex;
  align-items: center;
  height: 60px;
  background-color: #2c3e50;
  color: white;
  padding: 0 20px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.logo {
  font-size: 1.2rem;
  font-weight: bold;
  margin-right: 30px;
}

.main-menu {
  display: flex;
  gap: 2px;
}

.main-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 15px;
  height: 60px;
  color: #ecf0f1;
  text-decoration: none;
  transition: background-color 0.2s;
}

.main-menu-item:hover, .main-menu-item.active {
  background-color: #34495e;
}

/* 内容区 */
.content-wrapper {
  display: flex;
  flex: 1;
}

/* 侧边栏 */
.sidebar {
  width: 220px;
  background-color: #34495e;
  color: white;
  padding: 20px 0;
}

.sidebar-header {
  padding: 0 20px 15px;
  border-bottom: 1px solid #2c3e50;
  margin-bottom: 15px;
}

.sidebar-header h3 {
  font-size: 0.9rem;
  color: #bdc3c7;
  margin: 0;
}

/* 主内容区 */
.main-content {
  flex: 1;
  padding: 20px;
  background-color: #f5f7fa;
  overflow-y: auto;
}
</style>

配置路由

在 src 目录下创建 router 文件夹,新建 index.ts 和menu.ts路由配置文件:

menu.ts

// 1. 三级菜单接口(直接导出)
export interface ThirdMenu {
  name: string;
  path: string;
  icon?: string;
}

// 2. 二级菜单接口(直接导出)
export interface SubMenu {
  name: string;
  path?: string;
  icon?: string;
  children?: ThirdMenu[];
}

// 3. 一级菜单接口(直接导出)
export interface MainMenu {
  name: string;
  path: string;
  icon?: string;
  subMenus: SubMenu[];
}

// 4. 菜单数据(直接使用接口类型标注)
export const mainMenus: MainMenu[] = [
  {
    name: "首页",
    path: "/home",
    icon: "📊",
    subMenus: [
      { 
        name: "数据中心", 
        icon: "📈",
        children: [
          { name: "概览", path: "/home/dashboard/overview" },
          { name: "趋势分析", path: "/home/dashboard/trend" },
          { name: "来源分布", path: "/home/dashboard/source" }
        ]
      },
      { 
        name: "访问统计", 
        icon: "📉",
        children: [
          { name: "日统计", path: "/home/analytics/daily" },
          { name: "周统计", path: "/home/analytics/weekly" }
        ]
      }
    ]
  },
  {
    name: "用户管理",
    path: "/user",
    icon: "👥",
    subMenus: [
      { 
        name: "用户列表", 
        path: "/user/list",
        icon: "👤"
      },
      { 
        name: "权限管理", 
        icon: "🔑",
        children: [
          { name: "角色配置", path: "/user/permissions/roles" },
          { name: "权限分配", path: "/user/permissions/assign" }
        ]
      }
    ]
  }
];

 

index.ts

import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
import Layout from "../layouts/MainLayout.vue";
import type { MainMenu, SubMenu, ThirdMenu } from "./menu";
import {mainMenus} from "./menu";

// 导入所有视图组件(非动态导入方式)
import HomeOverview from "../views/home/dashboard/overview.vue";
import HomeTrend from "../views/home/dashboard/trend.vue";
import HomeSource from "../views/home/dashboard/source.vue";
import AnalyticsDaily from "../views/home/analytics/daily.vue";
import AnalyticsWeekly from "../views/home/analytics/weekly.vue";
import UserList from "../views/user/list.vue";
import RolesConfig from "../views/user/permissions/roles.vue";
import PermissionsAssign from "../views/user/permissions/assign.vue";

// 组件映射表(路径 -> 组件)
const componentMap: Record<string, any> = {
  "/home/dashboard/overview": HomeOverview,
  "/home/dashboard/trend": HomeTrend,
  "/home/dashboard/source": HomeSource,
  "/home/analytics/daily": AnalyticsDaily,
  "/home/analytics/weekly": AnalyticsWeekly,
  "/user/list": UserList,
  "/user/permissions/roles": RolesConfig,
  "/user/permissions/assign": PermissionsAssign,
};

// 递归生成路由(支持三级菜单)
const generateRoutes = (): RouteRecordRaw[] => {
  const routes: RouteRecordRaw[] = [
    {
      path: "/",
      component: Layout,
      children: [],
    },
  ];

  // 处理一级菜单
  mainMenus.forEach((mainMenu: MainMenu) => {
    // 一级菜单默认重定向到第一个三级菜单
    const firstSub = mainMenu.subMenus[0]!;
    const firstThird = firstSub.children?.[0] || firstSub;
    routes[0]!.children!.push({
      path: mainMenu.path,
      redirect: firstThird.path!,
    });

    // 处理二级和三级菜单
    mainMenu.subMenus.forEach((subMenu: SubMenu) => {
      // 若二级菜单有三级菜单,生成二级路由组
      if (subMenu.children && subMenu.children.length > 0) {
        subMenu.children.forEach((thirdMenu: ThirdMenu) => {
          routes[0]!.children!.push({
            path: thirdMenu.path!,
            component: componentMap[thirdMenu.path!],
          });
        });
      } else if (subMenu.path) {
        // 若二级菜单无三级菜单,直接生成路由
        routes[0]!.children!.push({
          path: subMenu.path,
          component: componentMap[subMenu.path],
        });
      }
    });
  });

  return routes;
};

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: generateRoutes(),
});

export default router;

 

修改根组件(App.vue) 

<template>
  <router-view />
</template>

<script setup lang="ts"></script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Segoe UI', sans-serif;
}

body {
  background-color: #f5f7fa;
}
</style>

 

配置入口文件(main.ts)

确保已正确挂载路由(通常初始化项目时已配置,确认即可):
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router/index' // 导入路由配置

createApp(App)
  .use(router) // 挂载路由
  .mount('#app')

 

清空项目自带的src/style.css文件,执行 npm run dev 启动项目,访问 http://localhost:5173 会看到: 

  • 顶部有黑色导航栏,包含「首页」「用户管理」两个链接
  • 点击不同链接,下方左侧区域会切换对应子菜单(无需刷新)

image

 


 

 

项目笔记

1、index.ts有如下代码,导入时,什么时候用type修饰?

import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
import Layout from "../layouts/MainLayout.vue";
import type { MainMenu, SubMenu, ThirdMenu } from "./menu";
import {mainMenus} from "./menu";

import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"

    • createRouter:Vue Router 的核心函数,用于创建路由实例(路由的 “总控制器”),后续会通过它配置路由规则、历史模式等。
    • createWebHistory:用于配置路由的 “历史模式”(HTML5 History 模式),使用这种模式时,URL 中不会有 # 符号(如 http://example.com/home 而非 http://example.com/#/home),是生产环境推荐的模式。
    • type RouteRecordRaw:Vue Router 提供的类型接口,用于约束路由规则的结构(比如路由的 pathcomponentchildren 等属性的类型),确保定义的路由符合规范。
      • 这里用 type 关键字导入,表明这是一个类型(非值),仅用于 TypeScript 类型检查

import type { MainMenu, SubMenu, ThirdMenu } from "./menu"

    • 从 ./menu 文件中导入菜单相关的类型接口(MainMenu/SubMenu/ThirdMenu),用于约束后续处理菜单数据时的类型。
    • 同样用 type 关键字,表明这些是类型,仅用于 TypeScript 类型检查(比如标注 mainMenus 的类型、循环处理菜单时的变量类型)。

import { mainMenus } from "./menu"

    • 从 ./menu 文件中导入实际的菜单数据 mainMenus(即之前定义的一级菜单数组,包含所有菜单的名称、路径、层级关系等)。
    • 后续会通过解析 mainMenus 中的数据,自动生成对应的路由规则(比如将菜单的 path 与页面组件关联),实现 “菜单与路由联动”(点击菜单跳转对应页面)。

 

2、index.ts中有如下代码,children的作用是什么?

  const routes: RouteRecordRaw[] = [
    {
      path: "/",
      component: Layout,
      children: [],
    },
  ];
在这段路由配置中,children 是 Vue Router 中用于定义嵌套路由的核心属性,作用是让子路由的组件渲染到父路由组件的 <router-view> 中,实现页面布局的复用和层级化展示。结合代码具体解释如下:

1. children 的本质:嵌套路由容器

children 是一个数组,里面存放的是 “子路由规则”。当一个路由规则中包含 children 时,意味着它是一个 “父路由”,children 中的路由是它的 “子路由”。
在这段代码中:
    • 父路由是 { path: "/", component: Layout, children: [] }(根路由),对应的组件是 Layout(公共布局组件,包含侧边栏、顶部导航等)。
    • children: [] 表示这个根路由目前没有子路由,后续会通过代码动态添加(比如页面路由、重定向规则等)。

2. children 的核心作用:实现 “布局复用 + 页面嵌套”

Layout 组件(父路由组件)中通常会包含一个 <router-view> 标签,这个标签是 “子路由组件的占位符”。当访问子路由时,子路由对应的组件会被渲染到这个 <router-view> 中。
例如,假设后续向 children 中添加一个子路由: 
{ path: "/home/dashboard/overview", component: HomeOverview }
    • 当用户访问 /home/dashboard/overview 时,路由系统会:
      1. 先渲染父路由组件 Layout(显示侧边栏、顶部导航);
      2. 再将子路由组件 HomeOverview(概览页面)渲染到 Layout 组件的 <router-view> 中。
这样就实现了 “公共布局不变,仅页面内容动态切换” 的效果,避免每个页面都重复编写侧边栏等公共部分。

3. 为什么根路由需要 children

根路由(path: "/")是整个路由系统的顶层容器,所有页面路由(如 /home/.../user/...)都必须作为它的 children 存在,原因是:
    • 确保所有页面都共享 Layout 公共布局(这是后台管理系统、多页面应用的常见设计)。
    • 形成 “根路由 → 页面路由” 的层级关系,符合 URL 路径的层级逻辑(如 /home/dashboard 是 / 的子路径)。

总结

children 的核心作用是定义嵌套路由,让子路由组件能在父路由组件的 <router-view> 中渲染,从而实现 “公共布局复用 + 页面内容动态切换” 的效果。在这段代码中,children: [] 作为初始空数组,为后续动态添加所有页面路由和重定向规则预留了位置,是构建完整路由系统的基础。

 

 

3、index.ts中,有箭头函数如下,其用法是?

// 递归生成路由(支持三级菜单)
const generateRoutes = (): RouteRecordRaw[] => {

箭头函数定义

箭头函数的类型定义与函数表达式类似,更简洁。
// 明确参数和返回值类型
const sum = (a: number, b: number): number => a + b;

// 无返回值(void)
const greet = (name: string): void => {
  console.log(`Hello, ${name}`);
};

 

4、index.ts中,有回调函数如下,其用法是?

  mainMenus.forEach((mainMenu: MainMenu) => {
    // 一级菜单默认重定向到第一个三级菜单
    const firstSub = mainMenu.subMenus[0]!;
    const firstThird = firstSub.children?.[0] || firstSub;
    routes[0]!.children!.push({
      path: mainMenu.path,
      redirect: firstThird.path!,
    });

回调函数的核心用法:“将函数作为参数传递给另一个函数,由另一个函数在合适时机执行”

一、forEach 中的回调函数:最典型的回调场景

forEach 是数组的遍历方法,它的参数就是一个回调函数,语法为:数组.forEach(回调函数)其中,回调函数会被 forEach 内部逻辑调用,每次遍历数组元素时执行一次,且会将当前元素、索引等信息作为参数传入回调函数。

二、代码中回调函数的具体使用分析

这段代码有三层 forEach 嵌套,每层都使用了回调函数,我们逐层拆解:

1. 第一层:遍历一级菜单的回调函数

mainMenus.forEach((mainMenu: MainMenu) => { 
  // 处理当前一级菜单的逻辑...
});
    • 回调函数定义:(mainMenu: MainMenu) => { ... }
      • 参数:mainMenu(当前遍历到的一级菜单对象,类型为 MainMenu)。
      • 作用:对每个一级菜单执行 “设置重定向” 和 “处理子菜单” 的逻辑。
    • forEach 如何调用回调:forEach 会自动遍历 mainMenus 数组中的每个元素,每次取出一个一级菜单对象,作为参数传入回调函数并执行。例如:
      • 第一次遍历:传入 “首页” 对象,执行回调处理 “首页”;
      • 第二次遍历:传入 “用户管理” 对象,执行回调处理 “用户管理”。

2. 第二层:遍历二级菜单的回调函数

mainMenu.subMenus.forEach((subMenu: SubMenu) => { 
  // 处理当前二级菜单的逻辑...
});
    • 回调函数定义:(subMenu: SubMenu) => { ... }
      • 参数:subMenu(当前遍历到的二级菜单对象,类型为 SubMenu)。
      • 作用:对每个二级菜单判断 “是否有三级菜单”,并生成对应的路由规则。
    • 调用逻辑:forEach 遍历当前一级菜单的 subMenus 数组(二级菜单集合),每次取出一个二级菜单对象传入回调。例如,处理 “首页” 时,会依次传入 “数据中心”“访问统计” 两个二级菜单对象执行回调。

3. 第三层:遍历三级菜单的回调函数

subMenu.children.forEach((thirdMenu: ThirdMenu) => { 
  // 处理当前三级菜单的逻辑...
});
    • 回调函数定义:(thirdMenu: ThirdMenu) => { ... }
      • 参数:thirdMenu(当前遍历到的三级菜单对象,类型为 ThirdMenu)。
      • 作用:为每个三级菜单生成对应的路由规则(关联组件)。
    • 调用逻辑:当二级菜单有 children(三级菜单)时,forEach 遍历 children 数组,每次取出一个三级菜单对象传入回调。例如,处理 “数据中心” 时,会依次传入 “概览”“趋势分析”“来源分布” 三个三级菜单对象执行回调。

 

5、SubMenuRecursive.vue组件中,有如下用法,是什么作用?

const isSubMenu = (item: SubMenu | ThirdMenu): item is SubMenu => {
  return 'children' in item && Array.isArray(item.children);
};
这段代码定义了一个类型守卫函数(Type Guard),用于在 TypeScript 中精确判断一个对象属于 SubMenu 类型还是 ThirdMenu 类型。我们来详细解析:

1. 函数定义与参数

    • 参数:item 的类型是联合类型 SubMenu | ThirdMenu,表示它可能是这两种类型中的一种。
    • 返回值:item is SubMenu 是 TypeScript 的类型谓词(Type Predicate),表示函数返回 true 时,item 会被 TypeScript 编译器推断为 SubMenu 类型;返回 false 时,则推断为 ThirdMenu 类型。

2. 判断逻辑

函数通过两个条件判断 item 是否为 SubMenu
    • 'children' in item:检查 item 对象是否包含 children 属性(SubMenu 有子菜单,而 ThirdMenu 通常没有)。
    • Array.isArray(item.children):进一步确认 children 属性的值是一个数组(确保子菜单结构有效)。
只有同时满足这两个条件,才会返回 true,即判定为 SubMenu 类型。

3. 作用与意义

在 TypeScript 中,当变量是联合类型时,编译器无法确定它具体属于哪种类型,可能导致访问类型特有属性时出错(例如 SubMenu 有 children,而 ThirdMenu 可能没有)。
这个类型守卫的作用是:
    • 类型收窄:在条件判断中使用 isSubMenu(item) 后,TypeScript 会自动将 item 的类型精确到 SubMenu 或 ThirdMenu,避免类型错误。
    • 代码安全性:确保只有 SubMenu 类型的对象才会执行与子菜单相关的操作(例如展开 / 折叠、递归渲染子菜单)。

举个例子

在组件中使用时,类型守卫会让 TypeScript 自动识别类型:
if (isSubMenu(item)) {
  // 这里 TypeScript 会自动推断 item 为 SubMenu 类型,可以安全访问 item.children
  console.log(item.children); 
} else {
  // 这里 item 会被推断为 ThirdMenu 类型,无需处理 children
  console.log(item.path);
}

这是 TypeScript 中处理联合类型的常用技巧,既保证了类型安全,又增强了代码的可读性和可维护性。

 

 

 

6、

7、

 

posted @ 2025-10-22 16:48  超级宝宝11  阅读(35)  评论(0)    收藏  举报