在Vue.js 3项目中操作DOM元素:应选用原生JS、jQuery还是纯Vue.js方案?
绝对应该坚持纯Vue.js方案,别碰框架外的DOM操作
先给你拍板:不要用querySelector或jQuery来处理这些状态逻辑,从务实的角度看,框架外的DOM操作会给你带来一堆后续难以解决的问题,而Vue本身完全有能力优雅搞定递归侧边栏的状态管理。我给你拆解下原因和具体解决方案:
为什么不能用框架外的DOM操作?
- 违背Vue的核心原理:数据驱动DOM
Vue的DOM是完全由数据状态驱动的,如果你手动用querySelector去加/删active/showing类,相当于绕开了Vue的响应式系统。举个例子:你手动给某个链接加了active类,但Vue的状态里没记录这个变化,当组件因为路由跳转、父组件更新等原因重新渲染时,Vue会根据当前数据状态重新生成DOM,你手动加的类会被覆盖,出现“状态跳变”的诡异bug。 - 维护性灾难
递归组件的结构本身就有嵌套层级,用jQuery/querySelector的话,你得写一堆复杂的选择器(比如$('.sidebar-item').siblings().removeClass('active'))来定位元素。一旦后续你调整了组件的DOM结构(比如给外层加了个容器、改了类名),所有这些DOM操作的代码都要跟着改,而Vue的方案只需要维护数据逻辑,DOM会自动同步。 - 状态同步成本极高
比如你要实现“激活一个链接时其他链接自动取消激活”,用DOM操作的话,每次点击都要遍历所有链接元素,移除active类再给当前元素加上。但如果是路由跳转触发的激活(比如用户直接输入URL),你还要额外监听路由变化再重复一遍这个操作。而Vue的方案只需要让所有组件共享“当前激活路由”这个状态,自动判断是否加类,完全不需要手动同步。
务实的Vue.js解决方案
针对递归侧边栏的状态管理,核心思路是把所有状态(激活、展开/折叠)放到Vue的响应式系统里,让组件根据状态自动渲染,而不是手动操作DOM。这里给你一套具体的实现方案:
1. 用状态管理工具统一管理菜单状态
如果你的项目已经用了Pinia(Vue3推荐)或Vuex,直接用它们来存全局的菜单状态:
- 当前激活的路由路径(或者直接用Vue Router的
$route对象) - 每个菜单的展开/折叠状态(用一个对象存储,键是菜单的路由path,值是布尔值)
举个Pinia的例子:
// stores/menu.js import { defineStore } from 'pinia' export const useMenuStore = defineStore('menu', { state: () => ({ // 存储展开的菜单,key是路由path expandedMenus: {} }), actions: { // 切换单个菜单的展开状态 toggleExpand(path) { this.expandedMenus[path] = !this.expandedMenus[path] }, // 路由跳转时自动展开所有父级菜单 expandParentMenus(currentPath) { const pathParts = currentPath.split('/') for (let i = 1; i < pathParts.length; i++) { const parentPath = pathParts.slice(0, i + 1).join('/') this.expandedMenus[parentPath] = true } } } })
2. 写递归的侧边栏子组件
每个子组件只需要接收菜单数据,然后根据全局状态判断自己的样式:
<!-- SidebarItem.vue --> <template> <div class="sidebar-item"> <!-- 菜单链接 --> <a :class="{ active: isActive, collapsed: !isExpanded && menuItem.children }" @click.prevent="handleMenuItemClick" > {{ menuItem.label }} <!-- 展开/折叠图标 --> <span v-if="menuItem.children" class="toggle-icon"> {{ isExpanded ? '▼' : '▶' }} </span> </a> <!-- 递归渲染子菜单 --> <div v-if="menuItem.children && isExpanded" class="sidebar-submenu" > <SidebarItem v-for="child in menuItem.children" :key="child.path" :menu-item="child" /> </div> </div> </template> <script setup> import { computed } from 'vue' import { useRoute } from 'vue-router' import { useMenuStore } from '@/stores/menu' const props = defineProps({ menuItem: { type: Object, required: true } }) const route = useRoute() const menuStore = useMenuStore() // 判断当前菜单是否处于激活状态(支持嵌套路由) const isActive = computed(() => { return route.matched.some(route => route.path.startsWith(props.menuItem.path)) }) // 判断当前菜单是否展开 const isExpanded = computed(() => { return menuStore.expandedMenus[props.menuItem.path] || false }) const handleMenuItemClick = () => { // 如果有子菜单,切换展开状态 if (props.menuItem.children) { menuStore.toggleExpand(props.menuItem.path) } // 没有子菜单的话,直接跳转路由 else { route.push(props.menuItem.path) } } </script>
3. 路由跳转时自动展开父菜单
在Vue Router的后置守卫里调用状态管理的方法,确保用户跳转路由时,对应的父菜单自动展开:
// router/index.js import { createRouter, createWebHistory } from 'vue-router' import { useMenuStore } from '@/stores/menu' const router = createRouter({ history: createWebHistory(), routes: [/* 你的路由配置数组 */] }) router.afterEach((to) => { const menuStore = useMenuStore() menuStore.expandParentMenus(to.path) }) export default router
最后总结
用Vue的响应式系统+状态管理来处理递归侧边栏的状态,不仅能完美解决你提到的类切换、状态关联问题,而且代码更简洁、维护性更强,后续扩展功能(比如记住用户的展开状态、权限控制菜单显示)也会非常方便。而框架外的DOM操作只会给你埋下一堆难以排查的bug,完全是捡芝麻丢西瓜的选择。
内容的提问来源于stack exchange,提问作者Giovanni Di Toro
相关产品推荐
相关产品推荐

