跳转到内容

路由规范

路由命名、权限路由、页面传参


  • Requirement:路由 path 使用 kebab-case(小写连字符),多单词用 - 分隔。
  • Requirement:路径以 / 开头,子路由不以 / 开头(相对路径)。
  • Standard:路径中使用名词,避免动词,如 /users 而非 /getUsers。
// 正确
{ path: '/user-list', name: 'UserList', ... }
{ path: '/order-detail', name: 'OrderDetail', ... }
 
// 错误
{ path: '/userList', name: 'UserList', ... }        // 驼峰
{ path: '/user_list', name: 'UserList', ... }       // 下划线
{ path: 'getUserList', name: 'UserList', ... }      // 动词
  • Requirement:路由 name 使用 PascalCase,与页面组件名对应,如 UserList、OrderDetail。
  • Standard:name 全局唯一,禁止重复。
  • Requirement:动态路由的 name 需包含父级语义,如 UserEdit 而非 Edit。
  • Standard:meta 字段统一包含以下约定字段:
字段类型说明
titlestring页面标题,用于浏览器标签和面包屑
requiresAuthboolean是否需要登录,默认 true
permissionstring权限标识,如 'user:read'
iconstring菜单图标
keepAliveboolean是否缓存页面,默认 false
hiddenboolean是否在菜单中隐藏,默认 false
{
  path: '/user',
  name: 'User',
  component: () => import('@/layouts/BasicLayout'),
  meta: { title: '用户管理', icon: 'UserOutlined' },
  children: [
    {
      path: 'list',
      name: 'UserList',
      component: () => import('@/pages/User/List'),
      meta: { title: '用户列表', permission: 'user:read', keepAlive: true },
    },
    {
      path: 'edit/:id',
      name: 'UserEdit',
      component: () => import('@/pages/User/Edit'),
      meta: { title: '编辑用户', permission: 'user:write', hidden: true },
    },
  ],
}
反模式问题建议
path 使用驼峰URL 不规范使用 kebab-case
name 重复路由跳转冲突name 全局唯一
meta 字段随意命名难以统一处理使用约定字段

  • Standard:采用基于角色的访问控制(RBAC),路由 meta.permission 声明所需权限标识。
  • Standard:权限标识格式为 资源:操作,如 user:read、order:write、user:delete。
  • Requirement:权限校验在路由守卫中统一处理,禁止在每个页面组件内重复写权限判断。
  • Standard:无权限时跳转 403 页面,而非隐藏菜单(防止直接输入 URL 访问)。
// src/router/guards.ts
import type { Router } from 'vue-router';
import { useUserStore } from '@/store/user';
 
export function setupGuards(router: Router) {
  router.beforeEach((to, from, next) => {
    const userStore = useUserStore();
 
    // 1. 登录校验
    if (to.meta.requiresAuth !== false && !userStore.isLoggedIn) {
      next({ name: 'Login', query: { redirect: to.fullPath } });
      return;
    }
 
    // 2. 权限校验
    if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) {
      next({ name: 'Forbidden' });
      return;
    }
 
    next();
  });
}
  • Standard:根据用户权限动态生成菜单,过滤掉无权限的路由。
  • Standard:前端权限只做”显示控制”,后端接口必须做二次鉴权,禁止仅靠前端控制权限。
// 根据权限过滤路由
function filterRoutes(routes, permissions) {
  return routes
    .filter(route => {
      if (route.meta?.permission) {
        return permissions.includes(route.meta.permission);
      }
      return true;
    })
    .map(route => ({
      ...route,
      children: route.children ? filterRoutes(route.children, permissions) : [],
    }));
}
  • Standard:页面内按钮级权限通过自定义指令或 Hook 控制,不通过路由。
// React Hook
import { usePermission } from '@/hooks/usePermission';
 
function UserActions() {
  const canEdit = usePermission('user:write');
  const canDelete = usePermission('user:delete');
 
  return (
    <>
      {canEdit && <Button>编辑</Button>}
      {canDelete && <Button danger>删除</Button>}
    </>
  );
}
反模式问题建议
仅前端隐藏无权限菜单用户可直接输入 URL 访问路由守卫 + 后端双重校验
每个组件内写权限判断重复代码统一在守卫处理
权限标识随意命名难以维护统一 资源:操作 格式

  • Standard:参数传递有三种方式,按场景选择:
方式适用场景示例
动态路径参数必填、简单标识(ID)/user/:id
Query 参数可选、筛选条件/user/list?page=1&status=active
State / Params 状态非 URL 可见的临时数据表单草稿、跳转来源
  • Requirement:路径参数用于必填标识,如 ID,格式为 :paramName。
  • Standard:参数在组件内通过路由 API 获取,并做类型校验。
// 路由定义
{ path: '/user/edit/:id', name: 'UserEdit', component: () => import('@/pages/User/Edit') }
 
// 组件内获取
import { useParams } from 'react-router-dom';
 
export default function UserEdit() {
  const { id } = useParams<{ id: string }>();
  // 校验 id
  if (!id) return <Navigate to="/user/list" replace />;
}
  • Standard:筛选、分页等可分享的状态使用 query 参数,方便 URL 分享和刷新保留。
  • Standard:query 参数在组件内解析为数字、布尔等正确类型,禁止直接当字符串使用。
import { useSearchParams } from 'react-router-dom';
 
function UserList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const page = Number(searchParams.get('page')) || 1;
  const status = searchParams.get('status') || 'all';
 
  const updateQuery = (params) => {
    setSearchParams({ ...Object.fromEntries(searchParams), ...params });
  };
}
  • Caution:避免通过路由 state 传递大量数据,仅用于传递轻量的临时状态(如来源页面)。
  • Caution:刷新页面会丢失路由 state,不可作为持久化数据传递方式。
// 传递 state
navigate('/user/edit', { state: { from: 'list' } });
 
// 接收 state
const { state } = useLocation();
const from = state?.from;
反模式问题建议
用 query 传大段 JSONURL 过长、难以维护用 state 或全局状态
用 state 传 ID刷新丢失ID 用路径参数
query 参数不做类型转换数字比较出错显式转换类型

  • Requirement:除首页外,所有路由组件必须使用动态导入(懒加载),减小首屏包体积。
  • Standard:按业务模块分包,同一模块的路由打包到同一 chunk。
// 正确:动态导入
{
  path: '/user',
  component: () => import('@/layouts/BasicLayout'),
  children: [
    { path: 'list', component: () => import('@/pages/User/List') },
    { path: 'edit/:id', component: () => import('@/pages/User/Edit') },
  ],
}
 
// 错误:静态导入
import UserList from '@/pages/User/List';
{ path: '/user/list', component: UserList }
  • Standard:使用 webpack/Vite 的 /* webpackChunkName */ 或 /* @vite-ignore */ 注释对 chunk 命名,方便调试。
component: () => import(/* webpackChunkName: "user" */ '@/pages/User/List')
  • Standard:懒加载组件必须配合 Suspense(React)或异步组件 loading 组件,避免白屏。
// React
import { lazy, Suspense } from 'react';
import { Spin } from 'antd';
 
const UserList = lazy(() => import('@/pages/User/List'));
 
function App() {
  return (
    <Suspense fallback={<Spin />}>
      <UserList />
    </Suspense>
  );
}
反模式问题建议
路由组件静态导入首屏包体积过大使用动态导入
懒加载无 loading 兜底切换路由白屏配合 Suspense/loading
所有路由打包到一个 chunk无法利用缓存按模块分包

  • Standard:路由守卫分为三类:
    • 全局前置守卫(beforeEach):登录校验、权限校验
    • 全局后置钩子(afterEach):页面标题设置、埋点上报
    • 组件内守卫:离开页面时的未保存提示
  • Requirement:登录校验和权限校验统一在 beforeEach 中实现。
  • Standard:守卫逻辑保持简洁,复杂逻辑抽取为独立函数。
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
 
  // 白名单路由直接放行
  const whiteList = ['Login', 'Register', 'NotFound'];
  if (whiteList.includes(to.name as string)) {
    next();
    return;
  }
 
  // 未登录跳转登录页
  if (!userStore.isLoggedIn) {
    next({ name: 'Login', query: { redirect: to.fullPath } });
    return;
  }
 
  // 权限不足跳转 403
  if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) {
    next({ name: 'Forbidden' });
    return;
  }
 
  next();
});
  • Standard:在 afterEach 中设置页面标题和上报页面浏览埋点。
router.afterEach((to) => {
  // 设置页面标题
  const title = to.meta.title as string | undefined;
  document.title = title ? `${title} - 管理系统` : '管理系统';
 
  // 上报页面浏览埋点
  trackPageView({ path: to.fullPath, title });
});
  • Standard:表单页面离开时,如果有未保存数据,必须提示用户确认。
import { useBlocker } from 'react-router-dom';
 
function UserEdit() {
  const [isDirty, setIsDirty] = useState(false);
 
  useBlocker(
    () => isDirty && !window.confirm('有未保存的更改,确定离开吗?')
  );
}
反模式问题建议
登录校验写在每个组件重复代码、易遗漏统一在 beforeEach
后置钩子中做权限判断已进入页面,体验差权限判断放前置守卫
表单页面无离开提示数据丢失使用 useBlocker