引言
前面已经实现了基本布局,然后又添加了多级菜单的支持(忽略文章介绍),现在给项目加点料,实现多布局设置,文章只提供思路,具体代码见仓库
在现代前端开发中,灵活的页面布局切换 (主要还是菜单的布局的切换) 已成为提升用户体验和适应多场景需求的重要手段。参考了其他开源项目后,大都是没有使用同一的菜单组件,代码的可维护性和扩展性会显著下降。为此,策略模式(Strategy Pattern)作为一种经典的设计模式,为布局切换提供了优雅且高内聚的解决方案。本文以实际项目为例,系统阐述如何在 Vue3 + TypeScript 项目中应用策略模式实现多布局切换。
策略模式简介
策略模式是一种行为型设计模式,旨在定义一系列算法(或策略),将每个算法封装起来,并使它们可以互换。客户端可在运行时选择不同的策略,从而实现算法的灵活切换。其核心优势在于将行为的定义与使用解耦,便于扩展和维护。
策略模式的典型结构包含以下组件:
- 策略接口(Strategy Interface):定义所有支持的算法的公共接口
- 具体策略(Concrete Strategy):实现策略接口的具体算法类
- 上下文(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
}
}
其他布局策略(如RightSidebarStrategy、TopMenuStrategy和MixedStrategy)均遵循类似的实现方式,各自处理特定布局的逻辑。
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.优势分析
- 高内聚低耦合:各布局策略独立实现,互不影响,便于维护。
- 易于扩展:新增布局只需实现新策略类并注册到工厂,无需修改现有业务逻辑。
- 代码清晰:业务层只关心“用什么策略”,不关心“怎么实现”,提升了代码可读性和可维护性