策略模式在现代后台管理系统中的应用:基于Vue3的布局切换实现

441 阅读5分钟

引言

前面已经实现了基本布局,然后又添加了多级菜单的支持(忽略文章介绍),现在给项目加点料,实现多布局设置,文章只提供思路,具体代码见仓库

在现代前端开发中,灵活的页面布局切换 (主要还是菜单的布局的切换) 已成为提升用户体验和适应多场景需求的重要手段。参考了其他开源项目后,大都是没有使用同一的菜单组件,代码的可维护性和扩展性会显著下降。为此,策略模式(Strategy Pattern)作为一种经典的设计模式,为布局切换提供了优雅且高内聚的解决方案。本文以实际项目为例,系统阐述如何在 Vue3 + TypeScript 项目中应用策略模式实现多布局切换。

策略模式简介

策略模式是一种行为型设计模式,旨在定义一系列算法(或策略),将每个算法封装起来,并使它们可以互换。客户端可在运行时选择不同的策略,从而实现算法的灵活切换。其核心优势在于将行为的定义与使用解耦,便于扩展和维护。

策略模式的典型结构包含以下组件:

  1. 策略接口(Strategy Interface):定义所有支持的算法的公共接口
  2. 具体策略(Concrete Strategy):实现策略接口的具体算法类
  3. 上下文(Context):维护一个对策略对象的引用,负责委托具体策略执行算法

项目结构与布局需求

本项目支持多种布局模式,包括左侧菜单、右侧菜单、顶部菜单和混合布局。相关目录结构如下

src/
  layout/
    strategies/
      LayoutStrategy.ts
      LayoutStrategyFactory.ts
      LeftSidebarStrategy.ts
      RightSidebarStrategy.ts
      TopMenuStrategy.ts
      MixedStrategy.ts
    index.vue
  hooks/
    useLayoutStrategy.ts
  stores/
    config.ts

此架构充分体现了策略模式的核心思想:将布局算法封装到不同策略类中,通过工厂方法动态创建并使用合适的策略。

策略模式的实现

1. 抽象策略接口

LayoutStrategy.ts中定义了布局策略的抽象接口,约定了所有布局策略应实现的方法。例如:

export interface LayoutStrategy {
  getAsideWidth(isCollapse: boolean): string;
  getMainContentClass(isCollapse: boolean): object;
  // ... 其他布局相关方法
}

2. 具体策略实现

以左侧菜单策略为例,LeftSidebarStrategy.ts的实现如下:

/**
 * 左侧菜单布局策略
 */
import type { LayoutStrategy } from './LayoutStrategy'
import type { RouteRecordRaw } from 'vue-router'
export class LeftSidebarStrategy implements LayoutStrategy {
  /**
   * 获取顶部菜单项 - 左侧菜单模式不显示顶部菜单,返回空数组
   */
  getTopMenuItems(_menuItems: RouteRecordRaw[]): RouteRecordRaw[] {
    return []
  }

  /**
   * 获取侧边菜单项 - 左侧菜单模式显示所有菜单项
   */
  getSideMenuItems(menuItems: RouteRecordRaw[], _activeTopMenu: string): RouteRecordRaw[] {
    return menuItems
  }

  /**
   * 是否显示顶部菜单 - 左侧菜单模式不显示顶部菜单
   */
  showTopMenu(): boolean {
    return false
  }

  /**
   * 是否显示侧边菜单 - 左侧菜单模式显示侧边菜单
   */
  showSideMenu(): boolean {
    return true
  }

  /**
   * 获取当前激活的菜单项 - 左侧菜单模式直接使用当前路径
   */
  getActiveMenu(
    currentPath: string,
    _activeTopMenu: string,
    _mode: 'horizontal' | 'vertical',
  ): string {
    return currentPath
  }
}

其他布局策略(如RightSidebarStrategyTopMenuStrategyMixedStrategy)均遵循类似的实现方式,各自处理特定布局的逻辑。

3.策略工厂与上下文

LayoutStrategyFactory.ts负责根据当前配置(如layoutMode)动态返回对应的策略实例。这样,业务层无需关心具体策略的创建和切换逻辑。

export class LayoutStrategyFactory {
  static getStrategy(mode: string): LayoutStrategy {
    switch (mode) {
      case 'left-sidebar': return new LeftSidebarStrategy();
      case 'right-sidebar': return new RightSidebarStrategy();
      case 'top-menu': return new TopMenuStrategy();
      case 'mixed': return new MixedStrategy();
      default: return new LeftSidebarStrategy();
    }
  }
}

4. 策略使用钩子

通过Vue3的组合式API,我们封装了useLayoutStrategy.ts钩子函数,为视图层提供统一的策略访问接口:

import { computed } from 'vue';
import { useConfigStore } from '@/stores/config';
import { LayoutStrategyFactory } from '@/layout/strategies/LayoutStrategyFactory';

export function useLayoutStrategy() {
  const configStore = useConfigStore();
  const layoutMode = computed(() => configStore.config.layout.layoutMode);
  const strategy = computed(() => LayoutStrategyFactory.getStrategy(layoutMode.value));
  return { strategy };
}

5. 视图层实现

在视图层,我们基于策略模式实现了灵活的菜单组件渲染。核心代码如下所示:

<script lang="ts" setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useLayoutStrategy } from '@/hooks/useLayoutStrategy'
import RecursiveMenuItem from './components/RecursiveMenuItem.vue'
import type { RouteRecordRaw } from 'vue-router'

interface Props {
  /** 菜单项数组 */
  menuItems: RouteRecordRaw[]
  /** 菜单模式,水平或垂直 */
  mode?: 'horizontal' | 'vertical'
  /** 是否折叠菜单 */
  collapse?: boolean
  /** 菜单背景色 */
  backgroundColor?: string
  /** 菜单文本颜色 */
  textColor?: string
  /** 菜单激活文本颜色 */
  activeTextColor?: string
  /** 是否只保持一个子菜单展开 */
  uniqueOpened?: boolean
  /** 是否开启折叠动画 */
  collapseTransition?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  mode: 'vertical',
  collapse: false,
  backgroundColor: '#304156',
  textColor: '#bfcbd9',
  uniqueOpened: true,
  collapseTransition: false,
})

const router = useRouter()
const { shouldShowSubMenu, getActiveMenu, getTopMenuItems, getSideMenuItems } = useLayoutStrategy()

// 判断是否显示子菜单
const showSubMenu = computed(() => {
  return shouldShowSubMenu(props.mode)
})

// 计算当前激活的菜单项
const activeMenu = computed(() => {
  return getActiveMenu(props.mode)
})

// 菜单点击处理
const handleMenuClick = (path: string) => {
  // 确保路径以 / 开头
  const fullPath = path.startsWith('/') ? path : `/${path}`
  router.push(fullPath)
}

// 获取当前模式下应该显示的菜单项
const displayMenuItems = computed(() => {
  if (props.mode === 'horizontal') {
    return getTopMenuItems(props.menuItems)
  } else {
    return getSideMenuItems(props.menuItems)
  }
})
</script>

<template>
  <el-menu
    :default-active="activeMenu"
    :mode="mode"
    :background-color="backgroundColor"
    :text-color="textColor"
    :active-text-color="activeTextColor"
    :unique-opened="uniqueOpened"
    :collapse="collapse"
    :collapse-transition="collapseTransition"
    class="app-menu"
  >
    <template v-for="item in displayMenuItems" :key="item.path">
      <!-- 使用递归组件渲染菜单项 -->
      <recursive-menu-item
        :menu-item="item"
        :base-path="item.path"
        :show-sub-menu="showSubMenu"
        @menu-click="handleMenuClick"
      />
    </template>
  </el-menu>
</template>

<style lang="scss" scoped>
.app-menu {
  border-right: none;
  width: 100%;

  &.el-menu--horizontal {
    border-bottom: none;
  }
}
</style>

<style lang="scss">
/* 全局样式,确保弹出菜单使用正确的背景色和hover背景色 */
.custom-popper-menu {
  .el-menu {
    background-color: var(--sidebar-bg, #304156) !important;

    .el-menu-item,
    .el-sub-menu__title {
      color: var(--sidebar-menu-text, #bfcbd9) !important;

      &:hover {
        background-color: var(--sidebar-menu-hover-bg, #263445) !important;
      }
    }
  }
}
</style>

6.优势分析

  • 高内聚低耦合:各布局策略独立实现,互不影响,便于维护。
  • 易于扩展:新增布局只需实现新策略类并注册到工厂,无需修改现有业务逻辑。
  • 代码清晰:业务层只关心“用什么策略”,不关心“怎么实现”,提升了代码可读性和可维护性