243 lines
6.5 KiB
Markdown
243 lines
6.5 KiB
Markdown
# Ether 项目前端路由最佳实践
|
||
|
||
## 概述
|
||
|
||
本文档总结了 Ether 项目前端路由开发中的最佳实践,以及常见问题和解决方案。
|
||
|
||
## 路由架构设计
|
||
|
||
### 1. 路由分层结构
|
||
|
||
```
|
||
路由层级:
|
||
├── 公开路由 (Public Routes)
|
||
│ ├── /login - 登录页
|
||
│ └── /no-project - 无项目提示页
|
||
│
|
||
├── 布局路由 (Layout Route)
|
||
│ └── / (Layout)
|
||
│ ├── /space-node - 空间节点管理
|
||
│ ├── /mdm/projects - 项目管理
|
||
│ ├── /mdm/visitors - 访客管理
|
||
│ └── ... 其他业务页面
|
||
│
|
||
└── 404 路由
|
||
└── /* - 匹配所有未定义路由
|
||
```
|
||
|
||
### 2. 动态路由生成机制
|
||
|
||
```typescript
|
||
// 1. 定义组件映射表
|
||
const dynamicRouteComponents: Record<string, () => Promise<any>> = {
|
||
"Mdm/projects": () => import("../views/mdm/project/index.vue"),
|
||
"Mdm/visitors": () => import("../views/mdm/visitor/index.vue"),
|
||
// ...
|
||
};
|
||
|
||
// 2. 根据后端菜单数据生成路由
|
||
function generateRoutesFromMenus(menus: MenuInfo[]): RouteRecordRaw[] {
|
||
return menus.map(menu => ({
|
||
path: menu.menuPath, // 如: "/mdm/visitors"
|
||
name: menu.componentName, // 如: "Mdm/visitors"
|
||
component: dynamicRouteComponents[menu.componentName],
|
||
meta: {
|
||
title: menu.permissionName,
|
||
icon: menu.menuIcon,
|
||
},
|
||
}));
|
||
}
|
||
|
||
// 3. 动态添加到路由表
|
||
menuRoutes.forEach(route => {
|
||
router.addRoute("Layout", {
|
||
path: route.path.substring(1), // 移除开头的 "/"
|
||
name: route.name,
|
||
component: route.component,
|
||
meta: route.meta,
|
||
});
|
||
});
|
||
```
|
||
|
||
## 常见问题与解决方案
|
||
|
||
### 问题 1: 菜单点击后页面不跳转
|
||
|
||
**症状**: 点击侧边栏菜单,页面没有切换,URL 不变。
|
||
|
||
**根本原因**:
|
||
- 菜单点击使用 `router.push({ name: key })`,但动态路由的 `name` 可能未正确注册
|
||
- 路由守卫中动态添加路由后,使用 `name` 导航可能找不到对应路由
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// ❌ 错误方式:使用 name 导航
|
||
const handleMenuClick = ({ key }: { key: string }) => {
|
||
router.push({ name: key }); // 可能找不到路由
|
||
};
|
||
|
||
// ✅ 正确方式:使用 path 导航
|
||
const handleMenuClick = ({ key }: { key: string }) => {
|
||
const menu = findMenuByComponentName(menus, key);
|
||
if (menu?.menuPath) {
|
||
router.push(menu.menuPath); // 使用 path 更可靠
|
||
}
|
||
};
|
||
```
|
||
|
||
### 问题 2: 动态路由添加后刷新 404
|
||
|
||
**症状**: 动态添加路由后可以访问,但刷新页面后显示 404。
|
||
|
||
**根本原因**:
|
||
- 动态路由只在路由守卫中添加一次,刷新后需要重新添加
|
||
- 404 路由优先级问题
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 1. 确保每次路由守卫都检查并添加路由
|
||
router.beforeEach(async (to, from, next) => {
|
||
if (!permissionStore.isInitialized) {
|
||
await permissionStore.fetchUserPermissions();
|
||
|
||
// 重新生成并添加路由
|
||
const menuRoutes = generateRoutesFromMenus(permissionStore.menus);
|
||
menuRoutes.forEach(route => {
|
||
const existing = router.getRoutes().find(r => r.name === route.name);
|
||
if (!existing) {
|
||
router.addRoute("Layout", route);
|
||
}
|
||
});
|
||
|
||
// 重新导航
|
||
next({ ...to, replace: true });
|
||
return;
|
||
}
|
||
next();
|
||
});
|
||
|
||
// 2. 404 路由最后添加,确保优先级最低
|
||
router.addRoute({
|
||
path: "/:pathMatch(.*)*",
|
||
name: "NotFound",
|
||
component: () => import("../views/404/index.vue"),
|
||
});
|
||
```
|
||
|
||
### 问题 3: 路由名称冲突
|
||
|
||
**症状**: 控制台警告路由名称已存在。
|
||
|
||
**根本原因**:
|
||
- 多次添加相同 name 的路由
|
||
- 热更新时重复添加
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 添加前检查是否已存在
|
||
menuRoutes.forEach(route => {
|
||
const existingRoute = router.getRoutes().find(r => r.name === route.name);
|
||
if (!existingRoute) {
|
||
router.addRoute("Layout", route);
|
||
}
|
||
});
|
||
```
|
||
|
||
### 问题 4: 路由路径格式不一致
|
||
|
||
**症状**: 菜单数据和路由配置的路径不匹配。
|
||
|
||
**根本原因**:
|
||
- 后端返回的 `menuPath` 包含前导 `/`,但动态添加时需要移除
|
||
- 路由配置中 path 格式不统一
|
||
|
||
**解决方案**:
|
||
```typescript
|
||
// 统一路径格式
|
||
const normalizePath = (path: string): string => {
|
||
// 确保以 / 开头
|
||
if (!path.startsWith('/')) {
|
||
path = '/' + path;
|
||
}
|
||
// 移除尾部 /
|
||
return path.replace(/\/$/, '') || '/';
|
||
};
|
||
|
||
// 动态添加时移除前导 /
|
||
router.addRoute("Layout", {
|
||
path: route.path.substring(1), // "/mdm/visitors" -> "mdm/visitors"
|
||
...
|
||
});
|
||
```
|
||
|
||
## 数据流设计
|
||
|
||
### 菜单数据结构
|
||
|
||
```typescript
|
||
interface MenuInfo {
|
||
id: string;
|
||
permissionCode: string; // 权限编码: "mdm:visitor:menu"
|
||
permissionName: string; // 显示名称: "访客管理"
|
||
menuPath: string; // 路由路径: "/mdm/visitors"
|
||
menuIcon?: string; // 图标: "UserAddOutlined"
|
||
componentName: string; // 组件名: "Mdm/visitors"
|
||
parentId?: string; // 父菜单ID
|
||
sortOrder: number; // 排序
|
||
children?: MenuInfo[]; // 子菜单
|
||
}
|
||
```
|
||
|
||
### 关键映射关系
|
||
|
||
| 后端字段 | 前端用途 | 示例 |
|
||
|---------|---------|------|
|
||
| `componentName` | 路由 name / 菜单 key | `Mdm/visitors` |
|
||
| `menuPath` | 路由 path | `/mdm/visitors` |
|
||
| `permissionName` | 页面标题 / 菜单标签 | `访客管理` |
|
||
| `menuIcon` | 菜单图标 | `UserAddOutlined` |
|
||
|
||
## 开发规范
|
||
|
||
### 1. 新增页面步骤
|
||
|
||
1. **创建页面组件**: `src/views/{module}/{page}/index.vue`
|
||
2. **注册路由映射**: 在 `src/router/index.ts` 的 `dynamicRouteComponents` 中添加
|
||
3. **配置后端权限**: 在数据库中添加菜单和权限数据
|
||
4. **验证路由**: 确保 `componentName` 和 `menuPath` 匹配
|
||
|
||
### 2. 路由配置检查清单
|
||
|
||
- [ ] `componentName` 在 `dynamicRouteComponents` 中有对应映射
|
||
- [ ] `menuPath` 格式统一(以 `/` 开头)
|
||
- [ ] 路由 `name` 唯一,不与其他路由冲突
|
||
- [ ] 动态路由添加前检查是否已存在
|
||
|
||
### 3. 调试技巧
|
||
|
||
```typescript
|
||
// 查看所有已注册路由
|
||
console.log(router.getRoutes());
|
||
|
||
// 检查特定路由是否存在
|
||
const route = router.getRoutes().find(r => r.name === 'Mdm/visitors');
|
||
console.log(route);
|
||
|
||
// 监听路由变化
|
||
router.beforeEach((to, from) => {
|
||
console.log('Navigating from', from.path, 'to', to.path);
|
||
});
|
||
```
|
||
|
||
## 相关文件
|
||
|
||
- `src/router/index.ts` - 路由配置和动态路由生成
|
||
- `src/views/layout/index.vue` - 布局组件和菜单处理
|
||
- `src/stores/permission.ts` - 权限状态和菜单数据
|
||
|
||
## 更新记录
|
||
|
||
| 日期 | 更新内容 | 作者 |
|
||
|------|---------|------|
|
||
| 2026-02-10 | 初始版本,总结路由问题和解决方案 | AI Assistant |
|