这是一个基于 Next.js 15+ App Router 的现代化前端架构,遵循以下原则:
- 文件系统路由 - 使用 Next.js 的文件系统路由,不需要额外配置
- 全局中间件认证 - 使用
middleware.ts做统一的路由拦截和权限控制 - 配置化菜单 - 菜单配置与路由解耦,支持动态权限过滤
- 分层架构 - 清晰的代码组织和职责划分
Next.js 使用文件系统作为路由,这是默认且推荐的方式:
app/
├── (main)/ # 路由组:需要认证的页面
│ ├── page.tsx # 对应路由: /
│ ├── pickup/
│ │ └── page.tsx # 对应路由: /pickup
│ ├── anniversary/
│ │ └── page.tsx # 对应路由: /anniversary
│ └── profile/
│ └── page.tsx # 对应路由: /profile
│
└── auth/ # 路由组:不需要认证的页面
├── login/
│ └── page.tsx # 对应路由: /auth/login
└── register/
└── page.tsx # 对应路由: /auth/register
关键点:
(main)和auth是路由组(Route Groups),用括号包裹的文件夹不会成为 URL 的一部分- 只有包含
page.tsx的文件夹才会生成路由 - 路由组用于组织代码,不影响 URL 结构
// ❌ 不使用路由组
app/
├── home/page.tsx → /home
├── pickup/page.tsx → /pickup
└── login/page.tsx → /login
// ✅ 使用路由组
app/
├── (main)/
│ ├── page.tsx → /
│ └── pickup/page.tsx → /pickup
└── auth/
└── login/page.tsx → /auth/login路由组的好处:
- 逻辑分组 - 将需要认证和不需要认证的页面分开
- 共享布局 - 每个路由组可以有自己的
layout.tsx - 不影响 URL - 括号内的名称不会出现在 URL 中
核心设计:使用 Next.js 中间件在服务端做全局路由拦截
// middleware.ts
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const token = request.cookies.get('access_token')?.value;
// 公开路径:无需认证
if (publicPaths.includes(pathname)) {
return NextResponse.next();
}
// 受保护路径:检查 token
if (protectedPaths.includes(pathname)) {
if (!token) {
// 重定向到登录页
return NextResponse.redirect(new URL('/auth/login', request.url));
}
}
return NextResponse.next();
}优势:
- ✅ 统一拦截 - 所有路由在服务端统一验证,无需每个页面重复代码
- ✅ 性能更好 - 在服务端拦截,避免页面加载后再跳转
- ✅ 更安全 - 服务端验证,无法绕过
- ✅ 代码简洁 - 页面组件不需要关心认证逻辑
虽然有全局中间件,但客户端仍需要:
// contexts/auth-context.tsx
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
// 初始化时获取用户信息
useEffect(() => {
const initAuth = async () => {
const token = request.getAccessToken();
if (token) {
const userData = await authApi.getCurrentUser();
setUser(userData);
}
};
initAuth();
}, []);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}用途:
- 存储用户信息(用户名、角色等)
- 提供登录/登出方法
- 菜单权限过滤
用户访问页面
↓
middleware.ts 拦截
↓
检查 cookie 中的 token
↓
┌─────────────┬─────────────┐
│ 有 token │ 无 token │
│ ↓ │ ↓ │
│ 放行 │ 重定向登录 │
└─────────────┴─────────────┘
↓
页面加载
↓
AuthContext 初始化
↓
获取用户信息
↓
渲染页面内容
重要概念:菜单配置和路由是解耦的
// config/menu.ts - 菜单配置
export const menuConfig: MenuConfig = [
{
id: 'home',
label: 'Home',
path: '/', // 指向实际路由
icon: Home,
},
{
id: 'pickup',
label: 'Pickup',
path: '/pickup', // 指向实际路由
icon: Package,
},
];路由文件系统 菜单配置
app/(main)/page.tsx ← { path: '/' }
app/(main)/pickup/ ← { path: '/pickup' }
1. 灵活性
// 可以控制菜单的显示顺序,而不依赖文件系统顺序
export const menuConfig = [
{ id: 'home', path: '/', ... },
{ id: 'pickup', path: '/pickup', ... },
// 实际路由可能有更多页面,但菜单只显示这些
];2. 权限控制
// 根据角色动态显示/隐藏菜单项
{
id: 'admin',
label: '管理面板',
path: '/admin',
roles: ['admin'], // 只有 admin 角色才能看到
}3. 菜单元数据
// 可以添加图标、徽章、分组等菜单特有的属性
{
id: 'pickup',
label: 'Pickup',
path: '/pickup',
icon: Package, // 菜单图标
badge: '5', // 徽章(未读数量)
group: 'main', // 菜单分组
}// components/layout/sidebar.tsx
export function Sidebar({ menuConfig }: SidebarProps) {
const filteredMenu = useMenu(menuConfig); // 根据用户角色过滤
return (
<nav>
{filteredMenu.map((item) => (
<Link href={item.path}> {/* 使用配置的 path */}
{item.icon && <item.icon />}
<span>{item.label}</span>
</Link>
))}
</nav>
);
}流程:
菜单配置 → 角色过滤 → 渲染菜单项 → Link 跳转到实际路由
1. 用户访问 /pickup
↓
2. middleware.ts 拦截
- 检查 cookie 中的 token
- 有 token: 继续
- 无 token: 重定向到 /auth/login
↓
3. Next.js 路由系统
- 根据文件系统找到 app/(main)/pickup/page.tsx
- 渲染页面
↓
4. 页面组件加载
- 使用 useAuth() 获取用户信息
- 渲染 AppLayout(包含侧边栏)
↓
5. 侧边栏渲染
- 读取菜单配置
- 根据用户角色过滤菜单
- 高亮当前路由对应的菜单项
↓
6. 页面完全渲染
1. 用户在 /auth/login 输入凭证
↓
2. 调用 login() 方法
↓
3. 请求后端 API
POST /api/v1/auth/login
↓
4. 后端返回
- access_token
- refresh_token (通过 cookie)
- user 信息
↓
5. 前端处理
- 存储 access_token 到 localStorage
- 存储 access_token 到 cookie (供 middleware 使用)
- 更新 AuthContext 中的 user 状态
↓
6. 跳转到首页 /
↓
7. middleware.ts 检查通过
↓
8. 渲染首页
pickup-web/
├── app/ # Next.js App Router
│ ├── (main)/ # 需要认证的页面路由组
│ │ ├── layout.tsx # 共享布局(可选)
│ │ ├── page.tsx # 首页 /
│ │ ├── pickup/page.tsx # /pickup
│ │ ├── anniversary/page.tsx # /anniversary
│ │ └── profile/page.tsx # /profile
│ │
│ ├── auth/ # 认证相关路由组
│ │ ├── layout.tsx # 认证页面布局
│ │ ├── login/page.tsx # /auth/login
│ │ └── register/page.tsx # /auth/register
│ │
│ ├── layout.tsx # 根布局(全局)
│ └── globals.css # 全局样式
│
├── components/ # React 组件
│ ├── layout/ # 布局组件
│ │ ├── app-layout.tsx # 应用主布局
│ │ ├── sidebar.tsx # 侧边栏(渲染菜单)
│ │ └── header.tsx # 顶部栏
│ └── ui/ # UI 基础组件
│
├── config/ # 配置文件
│ └── menu.ts # 菜单配置(与路由解耦)
│
├── contexts/ # React Context
│ └── auth-context.tsx # 认证状态管理
│
├── hooks/ # 自定义 Hooks
│ └── use-menu.ts # 菜单权限过滤
│
├── lib/ # 工具库
│ ├── api-client.ts # API 客户端
│ └── api/ # API 接口定义
│
├── types/ # TypeScript 类型
│
└── middleware.ts # 全局认证中间件 ⭐
旧方案(不推荐):
export default function PickupPage() {
useAuthGuard(true); // 每个页面都要写
return <div>...</div>;
}新方案(推荐):
// middleware.ts 统一处理
export function middleware(request: NextRequest) {
// 全局拦截,无需每个页面重复
}
// 页面组件简洁
export default function PickupPage() {
return <div>...</div>; // 不需要关心认证
}- 路由是技术实现 - 由文件系统决定
- 菜单是业务需求 - 需要灵活配置
// 可能有很多路由,但菜单只显示部分
app/
├── (main)/
│ ├── page.tsx # 显示在菜单
│ ├── pickup/page.tsx # 显示在菜单
│ ├── pickup/[id]/page.tsx # 不显示在菜单(详情页)
│ └── settings/page.tsx # 条件显示(需要权限)// 双重存储策略
setAccessToken(token) {
// 1. localStorage - 供 API 请求使用
localStorage.setItem('access_token', token);
// 2. cookie - 供 middleware 使用
document.cookie = `access_token=${token}; path=/`;
}原因:
middleware.ts运行在服务端,只能读取 cookieapi-client.ts运行在客户端,可以读取 localStorage
A: 因为菜单需要:
- 自定义显示顺序
- 权限控制
- 图标和样式
- 隐藏某些路由(如详情页)
A:
- middleware: 服务端拦截,控制能否访问页面
- useAuth: 客户端状态,提供用户信息和方法
# 1. 创建路由文件
app/(main)/new-page/page.tsx
# 2. (可选)添加到菜单配置
config/menu.ts- ✅ 使用 middleware.ts 做全局认证
- ✅ 使用路由组组织代码
- ✅ 菜单配置与路由分离
- ✅ 统一的错误处理
- ✅ TypeScript 类型安全
总结:这是一个遵循 Next.js 最佳实践的现代化架构,通过中间件实现全局认证,通过配置化菜单实现灵活的权限控制。