news 2026/11/3 12:14:03

五种技术栈实现同一Admin后台:动态路由与权限控制实战对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
五种技术栈实现同一Admin后台:动态路由与权限控制实战对比

1. 为什么同一个 Admin 后台要折腾五种技术栈

后台管理系统这东西,做前端的谁没写过几套。CRUD 表格、权限菜单、登录鉴权、图表看板,翻来覆去就那些东西。但真正让我下决心用五种技术栈各写一遍同一个 Admin 的,是去年接的一个外包咨询项目——客户团队前端用 Vue2 写了一半,后端是 NestJS,老板又听说 React 生态好想换,运维那边还压着一套老 Pear Admin Boot 的 Java 版本要迁移。五拨人五个说法,谁也说服不了谁。

与其开会扯皮,不如直接动手。我花了大概三周时间,把同一套 Admin 系统的核心功能——登录、动态路由、权限控制、数据表格、图表看板、文件上传——分别用 React、Vue3、NestJS 全栈、Pear Admin Boot、以及一个极简 Vue3 Admin 模板各实现了一遍。这篇文章不是要评出谁好谁坏,那种结论毫无意义。我想聊的是:同一件事在不同技术栈里的实现路径差异到底在哪,哪些坑是共通的,哪些是某个栈独有的。

先说清楚这套 Admin 系统到底要什么。核心需求就六条:用户登录拿到 token、根据角色动态生成侧边栏菜单、路由级别的权限拦截、一个带分页搜索的订单列表、一个 ECharts 折线图看板、以及文件上传。听起来简单,但每一条在不同栈里的实现逻辑差别很大。比如动态路由,React 里靠react-router的配置式路由配合状态管理动态注入,Vue3 里用router.addRoute()在导航守卫里动态添加,NestJS 全栈方案里前端部分其实还是 Vue 或 React,但后端要负责把菜单树算好返回。

关键词里提到的 VibeCoding 这个词,我理解成一种"跟着感觉走、快速试错"的开发状态。这套多栈实现本身就是 VibeCoding 的产物——不追求一步到位,先跑起来再说,跑不通就换条路。下面我按技术栈拆开讲,每个栈都会给出核心代码、踩过的坑、以及我个人的取舍理由。

提示:本文所有代码都是可运行的最小实现,省略了样式和部分样板代码。完整跑通需要 Node 18+ 和对应的包管理器。

2. React 栈:动态路由与状态管理的硬仗

2.1 路由配置式 vs 组件式:我为什么选配置式

React 生态里做 Admin,路由方案基本就两条路:react-router的配置式(v6 的createBrowserRouter)和组件式(<Routes>里写<Route>)。我一开始用的是组件式,写起来直观,但做到动态权限路由的时候直接卡住了——组件式路由是静态声明的,你没法在运行时根据后端返回的菜单树动态增删路由。

后来换成配置式,核心思路是:先定义一份完整的路由表(包含所有可能的路由),然后根据用户权限过滤出可访问的部分,再用createBrowserRouter生成实际路由。代码大概长这样:

// routes.js export const allRoutes = [ { path: '/dashboard', element: <Dashboard />, meta: { roles: ['admin', 'user'] } }, { path: '/orders', element: <Orders />, meta: { roles: ['admin'] } }, { path: '/settings', element: <Settings />, meta: { roles: ['admin'] } }, ]; // 根据权限过滤 export function filterRoutes(routes, userRoles) { return routes.filter(r => r.meta.roles.some(role => userRoles.includes(role))); }

然后在 App 组件里用useMemo根据当前用户角色生成路由。这里有个坑:createBrowserRouter必须在组件外调用,但用户角色是异步获取的。我的解法是用一个顶层RouterProvider配合useState存路由实例,角色变化时重新创建 router。实测下来这样会有一次闪烁,但可以接受。

2.2 状态管理选型:Zustand 还是 Redux Toolkit

React 这边状态管理我试了三个:Redux Toolkit、Zustand、Jotai。最后选了 Zustand,理由很简单——Admin 系统的状态不复杂,无非是用户信息、权限列表、菜单折叠状态、主题色。Redux Toolkit 的样板代码太多,一个简单的用户信息要写 slice、reducer、action、selector 四层。Zustand 一个create就搞定:

import { create } from 'zustand'; export const useUserStore = create((set) => ({ userInfo: null, roles: [], setUser: (info) => set({ userInfo: info, roles: info.roles }), logout: () => set({ userInfo: null, roles: [] }), }));

Jotai 的原子化思路在 Admin 里反而显得零碎,不如 Zustand 的集中式 store 好维护。这里有个经验:Admin 系统的状态天然是"全局单例"性质的,用户信息、权限、主题这些在整个应用生命周期里只有一份,用集中式 store 比原子化更贴合心智模型。

2.3 权限拦截的三种粒度

React 里做权限拦截,我分了三个粒度。第一层是路由级,在路由配置里加loader函数,没权限直接redirect。第二层是组件级,写一个<AuthWrapper roles={['admin']}>组件,没权限渲染 403 页面。第三层是按钮级,用usePermissionhook 返回一个hasPermission(code)函数,按钮上判断。

function usePermission() { const roles = useUserStore(s => s.roles); return { hasPermission: (required) => roles.some(r => required.includes(r)), }; }

实测下来,路由级拦截最稳,因为它在数据加载前就生效。组件级和按钮级容易漏,尤其是新来的同事不知道要包一层。我的建议是:路由级必须做,组件级和按钮级按需做,但要在团队规范里写清楚哪些页面必须包。

2.4 React 图表与表格的性能陷阱

图表用的 ECharts,表格用的 Ant Design Table。这两个在数据量大的时候都有坑。ECharts 的问题是每次setOption都会触发重绘,如果数据是轮询更新的,页面会卡。解法是用useRef存 chart 实例,更新时用chart.setOption(option, { notMerge: false })做增量更新。

Ant Design Table 的坑在rowKey。如果不指定rowKey,React 会用索引做 key,分页切换时会出现行状态错乱(比如选中状态跑到别的行上)。一定要用数据的唯一 ID 做rowKey。另外如果表格列超过 20 列,建议开virtual虚拟滚动,否则滚动会掉帧。

3. Vue3 栈:组合式 API 下的动态路由与权限

3.1 addRoute 的时机与坑

Vue3 的动态路由核心就一个 API:router.addRoute()。但什么时候调用它,是个大学问。我试过三个时机:登录成功后、路由守卫里、以及 App 挂载前。

登录成功后调用最直观,但有个问题——如果用户刷新页面,路由就丢了,因为addRoute添加的路由不会持久化。所以必须在路由守卫里做。我的做法是在router.beforeEach里判断:如果已登录但路由表为空,就拉取菜单树并addRoute,然后next({ ...to, replace: true })重新导航一次。

router.beforeEach(async (to, from, next) => { const userStore = useUserStore(); if (!userStore.token) { return to.path === '/login' ? next() : next('/login'); } if (userStore.routes.length === 0) { const menus = await fetchMenus(); menus.forEach(menu => { router.addRoute('Layout', { path: menu.path, component: () => import(`../views/${menu.component}.vue`), meta: { roles: menu.roles }, }); }); userStore.setRoutes(menus); return next({ ...to, replace: true }); } next(); });

这里有个大坑:next({ ...to, replace: true })如果不加replace: true,会在历史记录里多一条,用户点返回会回到空白页。另外addRoute的父路由名必须和实际注册的 Layout 路由名一致,否则路由会挂到根上,侧边栏高亮会失效。

3.2 组合式 API 的状态复用:Pinia 的取舍

Vue3 的状态管理我用了 Pinia。相比 Vuex,Pinia 的setup写法更贴近组合式 API 的心智。用户 store 大概这样:

export const useUserStore = defineStore('user', () => { const token = ref(localStorage.getItem('token') || ''); const roles = ref([]); const routes = ref([]); function setToken(t) { token.value = t; localStorage.setItem('token', t); } function setRoutes(r) { routes.value = r; } return { token, roles, routes, setToken, setRoutes }; });

Pinia 的好处是ref和computed直接能用,不用像 Vuex 那样区分 state、getters、mutations。但有个细节要注意:Pinia 的 store 在路由守卫里使用时,必须在守卫函数内部调用useUserStore(),不能在模块顶层调用,否则会报 "getActivePinia was called with no active Pinia"。

3.3 选项式与组合式混合开发的现实

关键词里有个"vue 组合式和选项式混合开发",这个我实际遇到过。老项目是选项式写的,新页面想用组合式,两者能共存,但有几个注意点。第一,this在组合式里不可用,所以选项式里的this.$router要换成useRouter()。第二,生命周期钩子要对应,created对应setup里的直接执行,mounted对应onMounted。第三,mixins和组合式函数不要混用,容易出命名冲突。

我的建议是:新页面一律用组合式,老页面不动,但要在setup里通过getCurrentInstance()拿到this来兼容老代码。虽然不优雅,但迁移期这是最省事的方案。

3.4 Vue 的权限指令与按钮级控制

Vue 里做按钮级权限,最优雅的是自定义指令:

app.directive('permission', { mounted(el, binding) { const roles = useUserStore().roles; if (!binding.value.some(r => roles.includes(r))) { el.parentNode?.removeChild(el); } }, });

用的时候直接<button v-permission="['admin']">删除</button>。但这里有个坑:如果权限是异步获取的,指令mounted时 roles 还是空的,按钮会被误删。解法是在指令里加updated钩子,或者确保权限在路由守卫里已经加载完再渲染页面。我选后者,因为更简单。

4. NestJS 全栈:后端算菜单树,前端只管渲染

4.1 为什么把菜单树放到后端算

NestJS 这套方案里,我做了一个和前面两个栈不一样的决定:菜单树在后端算好,前端只负责渲染。理由是权限逻辑本来就该在后端,前端算菜单树等于把权限规则暴露在客户端,而且前后端两套权限规则容易不一致。

后端用 NestJS 的 Guard 和 Interceptor 做权限。Guard 负责路由级拦截,Interceptor 负责给响应统一加包装。菜单树的生成逻辑放在一个MenuService里,根据用户的角色从数据库查菜单,递归组装成树。

@Injectable() export class MenuService { async getUserMenus(userId: number) { const roles = await this.getUserRoles(userId); const menus = await this.menuRepo.find({ where: { roles: In(roles) } }); return this.buildTree(menus); } private buildTree(menus: Menu[]) { const map = new Map(menus.map(m => [m.id, { ...m, children: [] }])); const roots = []; map.forEach(m => { if (m.parentId) map.get(m.parentId)?.children.push(m); else roots.push(m); }); return roots; } }

4.2 JWT 鉴权与刷新 token 的完整链路

NestJS 里 JWT 用@nestjs/jwt和passport-jwt。登录成功后签发 access token(15 分钟)和 refresh token(7 天)。access token 过期后,前端用 refresh token 换新的。这里的关键是 refresh token 要存数据库,并且支持撤销。

@Post('refresh') async refresh(@Body('refreshToken') token: string) { const payload = this.jwtService.verify(token, { secret: REFRESH_SECRET }); const stored = await this.tokenRepo.findOne({ where: { token, revoked: false } }); if (!stored) throw new UnauthorizedException('Token 已失效'); return this.authService.issueTokens(payload.sub); }

踩过的坑:refresh token 的过期时间要比 access token 长,但也不能太长,7 天是个平衡点。另外前端要在 axios 拦截器里做无感刷新——检测到 401 就调 refresh 接口,成功后重放原请求,失败才跳登录页。这个逻辑写起来容易出死循环,一定要加一个_retry标记。

4.3 前后端分离下的跨域与代理

NestJS 默认跑在 3000 端口,前端跑在 5173,跨域是必然的。开发环境用 Vite 的server.proxy代理,生产环境用 Nginx 反代。NestJS 这边要开 CORS:

app.enableCors({ origin: ['http://localhost:5173'], credentials: true, });

注意credentials: true时,origin不能是*,必须是具体域名。这个坑我踩过,前端带 cookie 请求一直失败,查了半天才发现是 CORS 配置问题。

4.4 全栈方案里前端部分的选型

NestJS 只负责后端,前端我还是用了 Vue3。但这里有个取舍:既然后端已经算好了菜单树,前端就不需要再维护一份路由权限表了,直接用后端返回的菜单树生成路由。这样前后端的权限规则只有一份,改权限只需要改后端。

代价是前端启动时要等菜单接口返回才能渲染路由,首屏会慢一点。优化方案是把菜单树缓存在 localStorage,下次启动先用缓存渲染,同时后台请求最新菜单,有变化再更新。实测下来首屏从 800ms 降到 200ms 左右。

5. Pear Admin Boot 与极简 Vue3 Admin:拿来主义的边界

5.1 Pear Admin Boot 的适用场景

Pear Admin Boot 是一套基于 Spring Boot 的后台管理框架,自带用户、角色、菜单、日志等模块。它的优势是开箱即用,适合那种"不想从零写权限系统"的项目。但它的前端是基于 Layui 的,如果你团队是 Vue 或 React 技术栈,改起来会很痛苦。

我用它实现了一遍订单列表和图表看板。订单列表直接用它自带的表格组件,配置一下 URL 和列定义就能跑。图表看板需要自己引入 ECharts,然后在页面里初始化。整体感受是:如果你的项目是 Java 技术栈且对前端定制要求不高,Pear Admin Boot 能省掉至少两周的权限系统开发时间。但如果你要深度定制 UI,它的 Layui 体系会成为负担。

5.2 极简 Vue3 Admin 模板的裁剪思路

关键词里有个"vue3 admin 最精简的框架",我找了一个只有 500 行代码的极简模板,然后按需加功能。极简模板通常只包含登录、Layout、一个示例页面。我的裁剪思路是:保留 Layout 和路由框架,删掉所有示例页面和多余的依赖,然后按项目需求逐个加。

这样做的代价是每个功能都要自己写,但好处是你对每一行代码都清楚,不会出现"这个功能是模板自带的但我不知道为什么能用"的情况。对于中小型项目,我倾向于从极简模板起步,而不是从大而全的框架裁剪。

5.3 五种实现的横向对比

维度ReactVue3NestJS 全栈Pear Admin Boot极简 Vue3
动态路由配置式 + 过滤addRoute后端算树后端算树addRoute
状态管理ZustandPiniaPinia自带Pinia
权限粒度三层指令 + 守卫Guard后端守卫
首屏速度中快慢(等菜单)快快
定制成本低低中高低
适合场景中大型中大型全栈团队Java 团队中小型

这张表不是让你选一个,而是让你看清楚每个方案的取舍点。比如 NestJS 全栈方案首屏慢,但权限最安全;Pear Admin Boot 定制成本高,但省开发时间。

6. 跨栈通用的踩坑清单与实操心得

6.1 动态路由的刷新丢失问题

不管哪个栈,动态路由都有一个共同问题:刷新页面后路由丢失。React 里靠重新过滤路由表解决,Vue 里靠路由守卫重新 addRoute,NestJS 全栈方案里靠重新请求菜单。核心思路都是把权限数据持久化(localStorage 或后端),刷新后重新生成路由。

这里有个细节:React 的createBrowserRouter重新创建时,如果当前 URL 对应的路由还没生成,会直接 404。解法是加一个errorElement或者用loader做重定向。Vue 的addRoute后必须next({ ...to, replace: true })重新导航,否则会白屏。

6.2 token 存储位置的选择

token 存 localStorage 还是 cookie,这个争论一直有。我的实践是:如果后端支持 httpOnly cookie,优先用 cookie,因为 XSS 偷不到。如果不支持,用 localStorage,但要做好 XSS 防护(CSP、输入转义)。sessionStorage 的问题是关掉标签页就没了,用户体验差。

refresh token 我建议存 httpOnly cookie,access token 存内存(不持久化)。这样即使 XSS 也偷不到 refresh token,access token 过期后自动刷新。代价是刷新页面后 access token 丢失,需要用 refresh token 重新换一次。

6.3 图表组件的内存泄漏

ECharts 和 AntV 这类图表库,如果不在组件卸载时dispose,会造成内存泄漏。React 里在useEffect的清理函数里调chart.dispose(),Vue 里在onUnmounted里调。这个坑在单页应用里不明显,但页面切换几十次后浏览器会卡死。

另外图表的resize监听也要清理。我见过一个项目,每次窗口 resize 都加一个监听,从不移除,跑一天后 resize 一次触发几百个回调,页面直接卡住。

6.4 权限变更后的实时生效

用户权限变了,但已经登录的会话里还是旧权限,这个怎么处理。我的方案是:后端在权限变更时,把该用户的所有 refresh token 标记为失效,前端下次刷新 token 时失败,强制重新登录。这样虽然用户体验有中断,但保证了权限的实时性。如果业务允许延迟,可以加一个定时轮询权限版本的接口,版本变了就重新拉菜单。

6.5 多标签页的状态同步

Admin 系统经常开多个标签页,一个标签页退出登录,其他标签页还显示着旧数据。解法是监听storage事件:

window.addEventListener('storage', (e) => { if (e.key === 'token' && !e.newValue) { window.location.href = '/login'; } });

这个事件只在其他标签页修改 localStorage 时触发,当前标签页不触发。所以退出登录时要先清 localStorage,再跳转,这样其他标签页能收到通知。

7. 五种实现跑下来,我真正想说的

三周时间五套实现,最大的收获不是学会了某个 API,而是看清了一件事:Admin 系统的复杂度不在技术栈,而在权限模型和状态同步。不管用 React 还是 Vue,动态路由、权限拦截、token 刷新这些核心逻辑的思路是一样的,差别只在 API 的写法。

如果让我给建议,我会说:团队用什么栈就用什么栈,别为了追新而换。React 和 Vue 在 Admin 场景下的开发效率差距,远小于团队熟悉度带来的差距。NestJS 全栈方案适合前后端一个人管的团队,Pear Admin Boot 适合 Java 背景且不想折腾前端的团队,极简模板适合想完全掌控代码的团队。

最后分享一个我踩过的坑:别在 Admin 系统里过度设计。我第一版 React 实现里搞了一套插件化的路由系统,支持运行时动态加载远程组件,结果调试了两天发现根本用不上。Admin 系统的路由和权限在项目生命周期里基本是稳定的,用最直接的方式实现就好。那些花哨的架构,等真的需要了再加也不迟。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 12:13:57

机器学习加速高频电磁场仿真:替代模型与工程实践

1. 高频电磁场仿真与机器学习融合的背景与价值高频电磁场仿真在现代工程设计中扮演着关键角色&#xff0c;从5G天线阵列到雷达系统&#xff0c;再到高速PCB设计&#xff0c;都需要精确的电磁场分析。传统仿真方法如有限元法(FEM)、时域有限差分法(FDTD)虽然精度高&#xff0c;但…

作者头像 李华
网站建设 2026/11/3 12:13:57

ANSYS预应力混凝土建模:等效荷载法与实体力筋法选型及避坑

简介&#xff1a;围绕 ANSYS 中的预应力混凝土分析&#xff0c;这份文档面向结构工程学习者与需要进行有限元建模的工程师&#xff0c;系统梳理等效荷载法与实体力筋法的适用条件、优缺点和建模取舍。文中对等效荷载法建模简便、整体效应易求但细部应力与张拉过程难以模拟的局限…

作者头像 李华
网站建设 2026/11/3 12:12:07

Z-score标准化:数据预处理的原理与实践

1. 数据归一化算法之Z-score标准化解析 在数据分析和机器学习领域&#xff0c;数据预处理是构建高质量模型的关键第一步。Z-score标准化作为最常用的数据归一化方法之一&#xff0c;能够有效解决不同特征量纲和分布差异带来的问题。我曾在多个金融风控项目中亲身体验过&#xf…

作者头像 李华
网站建设 2026/11/3 12:10:06

DBN与LSSVM混合模型在模式识别中的应用

1. 深度信念网络与最小二乘支持向量机的融合架构解析 这张结构示意图展示了一种将深度信念网络(DBN)与最小二乘支持向量机(LSSVM)相结合的混合模型架构。作为机器学习领域的前沿组合方案&#xff0c;这种结构在复杂模式识别任务中展现出独特优势。 从工程实践角度看&#xff0…

作者头像 李华
网站建设 2026/11/3 12:09:48

AI编码安全围栏:沙盒优先的工程化Harness框架

1. 项目概述&#xff1a;这不是一个“沙盒”&#xff0c;而是一套AI编码的工程化安全围栏“Sandbox-first AI coding harness”——光看这个标题&#xff0c;很多人第一反应是“又一个AI代码生成工具”&#xff0c;甚至联想到那些打着“无禁词”“免登录”旗号的网页版聊天框。…

作者头像 李华
网站建设 2026/11/3 12:09:34

人工智能产品经理商业落地:技术场景适配与能力边界指南

简介&#xff1a;《产品经理的AI实战》读书笔记以PDF形式呈现&#xff0c;面向产品经理、AI产品从业者及关注人工智能商业化的读者&#xff0c;聚焦AI技术如何与场景、产品和商业模式衔接&#xff0c;帮助读者建立从技术判断到商业落地的系统认知。笔记将原书三部分13章、216页…

作者头像 李华