Vue3实战中Menu菜单组件的设计与性能优化技巧
先看效果,再看代码
最近项目里要做一个侧边栏菜单,一开始我直接用原生 HTML + CSS 搞了个静态的,结果产品经理说“能不能加个展开收起”“能不能支持多级嵌套”“能不能记住上次打开的项”。行吧,那干脆重写一个灵活点的。折腾了两天,最后搞出一个我觉得还不错的方案,亲测有效,今天就分享出来。
核心思路其实很简单:用状态管理当前激活的菜单项,配合 CSS 控制子菜单的显示隐藏。但细节上坑不少,后面会细说。先看最基础的结构:
<nav class="menu">
<ul>
<li>
<button class="menu-item" data-key="dashboard">仪表盘</button>
</li>
<li>
<button class="menu-item has-children" data-key="users">
用户管理
</button>
<ul class="submenu hidden">
<li><a href="/users/list">用户列表</a></li>
<li><a href="/users/roles">角色权限</a></li>
</ul>
</li>
</ul>
</nav>
这里我用 data-key 做唯一标识,方便后续状态追踪。子菜单默认加 hidden 类隐藏,点击父项时切换这个类。
核心代码就这几行
用原生 JS 写状态管理,不依赖框架(虽然 React/Vue 里逻辑类似,但原理相通)。关键是要维护一个“展开项”的集合,比如用 Set 存储当前展开的 key:
const menu = document.querySelector('.menu');
const expandedKeys = new Set();
menu.addEventListener('click', (e) => {
const btn = e.target.closest('.menu-item');
if (!btn) return;
const key = btn.dataset.key;
const isParent = btn.classList.contains('has-children');
if (isParent) {
if (expandedKeys.has(key)) {
expandedKeys.delete(key);
} else {
expandedKeys.add(key);
}
// 更新 DOM
const submenu = btn.nextElementSibling;
if (submenu && submenu.classList.contains('submenu')) {
submenu.classList.toggle('hidden');
}
}
});
这段代码够简单吧?但注意:别用 toggleClass 直接操作,一定要和状态同步。我之前图省事直接 toggle,结果在“记住上次展开”功能里翻车了——页面刷新后状态丢了,但 DOM 还是展开的,导致不一致。
这个场景最好用:动态菜单 + 异步加载
有些后台系统菜单是从接口拉的,结构可能很深。这时候建议把菜单数据结构化,比如:
[
{
"key": "users",
"title": "用户管理",
"children": [
{ "key": "list", "title": "用户列表", "path": "/users/list" },
{ "key": "roles", "title": "角色权限", "path": "/users/roles" }
]
}
]
然后递归渲染。我写了个简易的 render 函数:
function renderMenu(items, level = 0) {
const ul = document.createElement('ul');
ul.className = level === 0 ? 'menu' : 'submenu';
items.forEach(item => {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.className = 'menu-item';
btn.dataset.key = item.key;
btn.textContent = item.title;
if (item.children) {
btn.classList.add('has-children');
li.appendChild(btn);
li.appendChild(renderMenu(item.children, level + 1));
} else {
const link = document.createElement('a');
link.href = item.path;
link.textContent = item.title;
li.appendChild(link);
}
ul.appendChild(li);
});
return ul;
}
注意:叶子节点我用的是 <a> 而不是 <button>,因为要跳转。但如果你是 SPA,可能全用 button + history.push 更合适。按需调整。
踩坑提醒:这三点一定注意
第一,键盘可访问性。很多开发者只做鼠标点击,忘了 tab 键和方向键。至少得保证:
- 所有可交互元素能被 tab 聚焦
- 展开/收起能用 Enter 或 Space 触发
- ARIA 属性要加,比如
aria-expanded
比如在按钮上动态更新:
// 展开时
btn.setAttribute('aria-expanded', 'true');
// 收起时
btn.setAttribute('aria-expanded', 'false');
第二,滚动区域问题。如果菜单很长,放在固定高度容器里,记得给容器加 overflow-y: auto,但别让子菜单超出容器被裁剪。我的做法是:子菜单用 absolute 定位,但限制最大高度,超出滚动。不过要注意定位上下文,别被父级 overflow hidden 干掉。
第三,状态持久化。用户刷新页面后希望保留上次展开的菜单。我一般用 localStorage 存 expandedKeys:
// 初始化时
const saved = localStorage.getItem('menu-expanded');
if (saved) {
expandedKeys = new Set(JSON.parse(saved));
// 然后遍历 DOM,把对应 submenu 显示出来
}
// 每次变更时
localStorage.setItem('menu-expanded', JSON.stringify([...expandedKeys]));
但注意:别存太多,万一菜单结构变了(比如 key 改了),旧数据可能无效。加个版本号或清理逻辑更稳妥。
高级技巧:带图标的菜单怎么对齐
UI 设计师总喜欢加图标,结果文字和图标对不齐,看着难受。我的方案是:用 flex 布局,固定图标宽度,文字左对齐。
.menu-item {
display: flex;
align-items: center;
padding: 8px 16px;
gap: 8px;
}
.menu-icon {
width: 20px;
text-align: center; /* 如果是 icon font */
}
如果是 SVG 图标,直接 inline 写在 HTML 里最省事,避免额外请求。但注意:别用 background-image,不好控制大小和对齐。
另外,hover 和 active 状态要区分清楚。我见过太多菜单 hover 时变色,但点击后没高亮,用户根本不知道当前在哪。建议当前激活项用不同背景色,比如:
.menu-item.active {
background-color: #e6f7ff;
font-weight: bold;
}
激活状态怎么判断?根据当前路由匹配。比如当前路径是 /users/list,就高亮 key 为 list 的项,同时它的所有祖先也要展开并高亮(如果设计需要的话)。
结语
以上就是我折腾菜单组件的一些经验。说实话,看似简单的菜单,要做好细节真不少。我这个方案不算完美——比如没处理超长文本换行、没做动画过渡(加个 transition 其实很简单,但有些项目禁用动画),但够用、稳定、易维护,这就够了。
这个技术的拓展用法还有很多,比如结合拖拽排序、支持右键菜单、或者做成可收起的 mini 模式。后续会继续分享这类博客。以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流!

暂无评论