第五篇:登录态与权限控制
本篇是整个企业官网体系中最核心的一篇 。
如果第四篇解决的是「用户如何进来」,这一篇解决的是:
用户进来之后,你如何知道他是谁、能做什么、能不能访问某个页面。
一、本篇目标
我们将完整解决以下问题:
-
登录成功后,登录态如何保存
-
JWT / Cookie / Token 如何取舍
-
Next.js App Router 下如何做 middleware 鉴权
-
[locale]场景下的国际化路由保护 -
Header 如何根据登录态自动切换
-
如何设计「受保护路由」而不是到处写判断
二、JWT / Cookie / Token 的正确选择
1.为什么不用 LocalStorage(重要)
XSS 风险极高
middleware 访问不到
SSR 无法读取
企业级项目一律不用 LocalStorage 存登录态
2.推荐方案(本教程采用)
JWT + HttpOnly Cookie
-
JWT:后端签发
-
Cookie:HttpOnly + Secure
-
前端:
-
看不到 token 内容
-
但可以知道"是否已登录"
-
优势:
-
防 XSS
-
proxy(middleware) 可读取
-
SSR / RSC 安全
三、登录接口约定(前后端共识)
bash
POST /auth/login
成功响应
bash
Set-Cookie: auth_token=xxx; HttpOnly; Path=/; Secure
javascript
{
"user": {
"id": "u_123",
"email": "user@example.com",
"name": "User"
}
}
前端 不保存 token,只关心 user 信息
四、登录态读取策略(三层)
| 层级 | 作用 | 能否访问 Cookie |
|---|---|---|
| proxy(middleware) | 路由拦截 | ✅ |
| server component | SSR | ✅ |
| client component | UI | ❌(只能通过接口) |
五、middleware 鉴权(含 [locale])
1.受保护路由定义
javascript
// src/lib/auth/route.ts
export const protectedRoutes = ['/user', '/settings']
export function isProtectedPath(pathname: string) {
return protectedRoutes.some((route) => pathname.startsWith(route))
}
2.proxy.ts(middleware)(核心代码)
javascript
import { defaultLocale, locales } from '@/i18n/config';
import { isProtectedPath } from '@/lib/auth/route';
import createMiddleware from 'next-intl/middleware';
import { NextRequest, NextResponse } from 'next/server';
const intlMiddleware = createMiddleware({
locales,
defaultLocale,
localePrefix: 'always'
})
const PUBLIC_FILE = /\.(.*)$/
// Next.js 15+ proxy 约定:导出默认函数处理请求
export default function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
if (
PUBLIC_FILE.test(pathname) ||
pathname.startsWith('/api')
) {
// For public files and API routes, only apply i18n
return intlMiddleware(request);
}
const locale = pathname.split('/')[1]
const pathnameWithoutLocale = pathname.replace(`/${locale}`, '') || '/'
const token = request.cookies.get('auth_token')?.value
if (isProtectedPath(pathnameWithoutLocale) && !token) {
const loginUrl = new URL(`/${locale}/login`, request.url)
return NextResponse.redirect(loginUrl)
}
// For other requests, apply i18n
return intlMiddleware(request);
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}
✔ 同时支持国际化
✔ 不依赖客户端
✔ 企业级标准写法
六、获取当前用户信息(Server Action / API)
1. /auth/me 接口
GET /auth/me
javascript
{
"id": "u_123",
"email": "user@example.com",
"name": "User"
}
2.前端封装
javascript
// src/types/user.ts
export interface User {
id: string
email: string
name: string
role: 'admin' | 'user'
}
javascript
// src/services/auth.service.ts
import { LoginFormValues } from '@/forms/schemas/login.schema'
import { RegisterFormValues } from '@/forms/schemas/register.schema'
import { http } from '@/lib/http/client'
import { ApiSingleResponse } from '@/types'
import { User } from '@/types/user'
interface LoginResponse {
token: string
user: User
}
export const authService = {
login(data: LoginFormValues) {
return http.post<ApiSingleResponse<LoginResponse>>('/auth/login', data)
},
register(data: RegisterFormValues) {
return http.post<ApiSingleResponse<LoginResponse>>('/auth/register', data)
},
fetchCurrentUser() {
return http.get<ApiSingleResponse<LoginResponse>>('/auth/me')
}
}
七、全局登录态管理(不使用 Redux)
登录态不是全局状态,是请求状态
我们用:
-
React Context + 请求
-
页面级拉取
1.AuthContext
添加cookies-next和@types/cookie依赖
javascript
pnpm add cookies-next
pnpm add -D @types/cookie

/src/context/AuthContext.tsx
javascript
'use client'
import { fetchCurrentUser } from '@/services/auth.service'
import { User } from '@/types/user'
import { deleteCookie, getCookie, setCookie } from 'cookies-next'
import { useRouter } from 'next/navigation'
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
interface AuthContextType {
user: User | null
loading: boolean
login: (user: User, token: string) => void
logout: () => void
setUser: (user: User | null) => void
isAuthenticated: boolean
}
const AuthContext = createContext<AuthContextType | null>(null)
interface AuthProviderProps {
children: ReactNode
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const router = useRouter()
// 初始化:从 Cookie 取 Token,调用 /api/me 获取用户信息
useEffect(() => {
const initUser = async () => {
try {
// 1. 从 Cookie 获取 Token
const token = getCookie('auth_token')
if (!token) {
setLoading(false)
return
}
// 2. 调用 /api/me 获取用户信息
const res = await fetchCurrentUser()
if (!res.data) throw new Error('用户信息获取失败')
const userData = res.data
// 3. 将用户信息存入 Context
setUser(userData)
} catch (error) {
console.error('初始化用户状态失败:', error)
// Token 无效时清除 Cookie
deleteCookie('auth_token')
} finally {
setLoading(false)
}
}
initUser()
}, [])
// 登录方法:存储用户到 Context,Token 存入 Cookie
const login = (userInfo: User, token: string) => {
setUser(userInfo)
// 设置 Cookie(配置有效期、域名等,根据业务调整)
setCookie('auth_token', token, {
maxAge: 60 * 60 * 24 * 7, // 7天有效期
path: '/', // 全站可用
secure: process.env.NODE_ENV === 'production', // 生产环境仅 HTTPS
httpOnly: false, // 客户端需要读取的话设为 false(若仅服务端用可设为 true)
sameSite: 'lax',
})
router.push('/') // 登录后跳转首页
}
// 登出方法:清空 Context 和 Cookie
const logout = () => {
setUser(null)
deleteCookie('auth_token')
router.push('/login') // 登出后跳登录页
}
// 简化鉴权判断
const isAuthenticated = !!user
return (
<AuthContext.Provider
value={{
user,
loading,
login,
logout,
setUser,
isAuthenticated,
}}
>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth 必须在 AuthProvider 内部使用')
}
return context
}
八、Header 登录态联动(关键)
1.在 layout 中注入 user
javascript
import '@/app/globals.css'
import { Footer } from '@/components/layout/Footer'
import { Header } from '@/components/layout/Header'
import { AuthProvider } from '@/context/AuthContext'
import { Locale, locales } from '@/i18n/config'
import { NextIntlClientProvider } from 'next-intl'
import { notFound } from 'next/navigation'
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: Locale }>
}) {
const { locale } = await params
if (!locales.includes(locale as Locale)) {
notFound()
}
const messages = (await import(`@/i18n/messages/${locale}.json`)).default
return (
<html lang={locale}>
<body className="min-h-screen bg-background text-foreground">
<NextIntlClientProvider locale={locale} messages={messages}>
<AuthProvider>
<Header />
{children}
<Footer />
</AuthProvider>
</NextIntlClientProvider>
</body>
</html>
)
}
2.Header 中使用
javascript
'use client'
import { useAuth } from '@/context/AuthContext'
import { useTranslations } from 'next-intl'
import Link from 'next/link'
import { MainNav } from './MainNav'
import { UserMenu } from './UserMenu'
// 临时模拟登录态(后续会替换为真实 auth)
const mockUser = {
// email: 'user@example.com',
email: '',
}
export function Header() {
const { user } = useAuth()
const isLoggedIn = Boolean(user)
const t = useTranslations()
return (
<header className="border-b">
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-6">
<div className="flex items-center space-x-8">
<Link href="/" className="text-lg font-bold">
Enterprise
</Link>
<MainNav />
</div>
<div>
{!isLoggedIn ? (
<Link href="/login" className="text-sm font-medium">
{t('auth.login')}
</Link>
) : (
<div className="flex items-center space-x-3">
<Link href="/user" className="text-sm">
{t('user_menu.user_center')}
</Link>
<span className="text-muted-foreground">|</span>
<UserMenu email={user?.email || ''} />
</div>
)}
</div>
</div>
</header>
)
}
九、修改相关页面和代码
login/page.tsx
javascript
// src/app/[locale]/(auth)/login/page.tsx
'use client'
import { CaptchaCanvas } from '@/components/form/CaptchaCanvas'
import { PasswordInput } from '@/components/form/PasswordInput'
import { TextInput } from '@/components/form/TextInput'
import { Button } from '@/components/ui/button'
import { useAuth } from '@/context/AuthContext'
import { useLoginForm } from '@/forms/hooks/useLoginForm'
import { authService } from '@/services/auth.service'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { Controller } from 'react-hook-form'
export default function LoginPage() {
const form = useLoginForm()
const t = useTranslations()
const [loading, setLoading] = useState(false)
const { login } = useAuth()
const onSubmit = form.handleSubmit(async (data) => {
setLoading(true)
try {
const res = await authService.login(data)
console.log(res)
const { token, user } = res.data.result.data
login(user, token)
} catch (error) {
console.error('登录出错:', error)
} finally {
setLoading(false)
}
})
return (
<div className="mx-auto max-w-md py-16">
<h1 className="mb-6 text-2xl font-bold">{t('login.title')}</h1>
<form onSubmit={onSubmit} className="mx-auto mt-10 max-w-md space-y-4">
<Controller
control={form.control}
name="email"
render={({ field, fieldState }) => (
<div>
<TextInput
type="email"
value={field.value}
onChange={field.onChange}
placeholder={t('register.email')}
/>
{fieldState.error && (
<p className="text-xs text-destructive">{t(fieldState.error?.message || '')}</p>
)}
</div>
)}
/>
<Controller
control={form.control}
name="password"
render={({ field, fieldState }) => (
<div>
<PasswordInput
value={field.value}
onChange={field.onChange}
placeholder={t('register.password')}
/>
{fieldState.error && (
<p className="text-xs text-destructive">{t(fieldState.error?.message || '')}</p>
)}
</div>
)}
/>
<Controller
control={form.control}
name="captcha"
render={({ field, fieldState }) => (
<div>
<CaptchaCanvas
value={field.value}
onChange={field.onChange}
placeholder={t('register.captcha')}
/>
{fieldState.error && (
<p className="text-xs text-destructive">{t(fieldState.error?.message || '')}</p>
)}
</div>
)}
/>
<Button type="submit" className="w-full" disabled={loading}>
{t('login.submit')}
</Button>
</form>
</div>
)
}
register/pag.tsx
javascript
'use client'
import { Button } from '@/components/ui/button'
import { useAuth } from '@/context/AuthContext'
import { FormAgree } from '@/forms/components/FormAgree'
import { FormInput } from '@/forms/components/FormInput'
import { useRegisterForm } from '@/forms/hooks/useRegisterForm'
import { authService } from '@/services/auth.service'
import { useTranslations } from 'next-intl'
import { useState } from 'react'
import { FormProvider } from 'react-hook-form'
export default function RegisterPage() {
const methods = useRegisterForm()
const t = useTranslations('register')
const [countdown, setCountdown] = useState(0)
const [isSending, setIsSending] = useState(false)
const [loading, setLoading] = useState(false)
const { login } = useAuth()
const startCountdown = () => {
setCountdown(60)
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer)
return 0
}
return prev - 1
})
}, 1000)
}
const handleSendCode = async () => {
if (countdown > 0) return
const phone = methods.getValues('phone')
if (!phone) {
methods.setError('phone', { message: 'form.phone.required' })
return
}
setIsSending(true)
try {
await new Promise((resolve) => setTimeout(resolve, 1000))
startCountdown()
} catch (error) {
console.error('Failed to send verification code:', error)
} finally {
setIsSending(false)
}
}
const onSubmit = methods.handleSubmit(async (values) => {
try {
setLoading(true)
const res = await authService.register(values)
console.log(res)
const { token, user } = res.data.result.data
login(user, token)
} catch (error) {
console.error('注册出错:', error)
} finally {
setLoading(false)
}
})
return (
<div className="mx-auto max-w-md py-16">
<h1 className="mb-6 text-2xl font-bold">{t('title')}</h1>
<FormProvider {...methods}>
<form onSubmit={onSubmit} className="space-y-4">
<FormInput
label={t('email')}
{...methods.register('email')}
error={methods.formState.errors.email}
/>
<FormInput
label={t('phone')}
{...methods.register('phone')}
error={methods.formState.errors.phone}
/>
<FormInput
type="password"
label={t('password')}
{...methods.register('password')}
error={methods.formState.errors.password}
/>
<FormInput
label={t('captcha')}
{...methods.register('captcha')}
error={methods.formState.errors.captcha}
/>
<div className="space-y-1">
<div
className={`flex gap-2 ${methods.formState.errors.code ? 'items-center' : 'items-end'}`}
>
<FormInput
label={t('code')}
className="flex-1"
{...methods.register('code')}
error={methods.formState.errors.code}
/>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={countdown > 0 || isSending}
className="px-3 shrink-0"
>
{countdown > 0 ? `${countdown}s` : isSending ? t('sending') : t('send_code')}
</Button>
</div>
</div>
<FormAgree
name="agree"
control={methods.control}
label={t('agree_terms')}
error={methods.formState.errors.agree}
/>
<Button type="submit" className="w-full" disabled={loading}>
{t('submit')}
</Button>
</form>
</FormProvider>
</div>
)
}
修改axios,添加interceptors.request处理
javascript
// src/lib/http/client.ts
import axios from 'axios'
import { getCookie } from 'cookies-next'
if (!process.env.NEXT_PUBLIC_API_BASE) {
throw new Error('NEXT_PUBLIC_API_BASE is not defined')
}
export const http = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE,
timeout: 10000,
withCredentials: true, // 为将来登录态做准备
})
http.interceptors.request.use(
config => {
const token = getCookie('auth_token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
http.interceptors.response.use(
response => {
if (response.data && response.data.header && typeof response.data.header.isSuccess === 'boolean') {
const apiResponse = response.data;
if (!apiResponse.header.isSuccess) {
alert(apiResponse.header.message || 'API Error');
throw new Error(apiResponse.header.message || 'API Error');
}
}
return response;
},
error => {
console.error(error)
const message =
error.response?.data?.message ||
error.message ||
'Network Error'
return Promise.reject(new Error(message))
}
)
九、受保护页面示例
javascript
// app/[locale]/(user)/user/page.tsx
export default function UserPage() {
return <div>用户中心(必须登录)</div>
}
是否登录?proxy 已经替你挡在门外
十、常见误区总结(非常重要)
❌ 用 LocalStorage 存 token
❌ 在组件里到处写 if (!user) redirect
❌ 用 Redux 管登录态
✅ Cookie + proxy(middleware)
✅ 请求即状态
✅ Header 自然联动
十一、到这里你已经具备的能力
-
企业级登录态设计能力
-
App Router + proxy 鉴权
-
国际化路由保护
-
可扩展的权限体系
附录:Mock.js 模拟 API(本地开发)
本附录用于补全第五篇与第四篇的开发体验:
后端未完成
但前端需要完整跑通:登录 / 注册 / 鉴权 / Header 联动
我们将引入 Mock.js + Axios 拦截 来模拟真实 API。
一、为什么选择 Mock.js(而不是 MSW)
在你的当前阶段:
-
需要快速返回数据
-
不需要模拟复杂网络层
-
希望逻辑尽量简单、可控
Mock.js 更适合本教程阶段
等进入第七 / 八篇(真实后端联调)时,可以再切换到 MSW。
二、安装依赖
pnpm add mockjs
pnpm add -D @types/mockjs

三、Mock 目录结构(推荐)
javascript
src/
├─ mock/
│ ├─ modules
│ │ ├─ user.mock.ts # 用户 增删改查
│ │ └─ auth.mock.ts # 登录 / 注册 / me
│ │
│ ├─ index.ts # Mock 入口
│ └─ setup.ts # 初始化
│
└─ providers.tsx
四、Mock 初始化入口
src/mock/setup.ts
javascript
import './modules/auth.mock';
import './modules/user.mock';
src/mock/index.ts
javascript
export async function setupMock() {
if (
process.env.NODE_ENV !== 'development' ||
process.env.NEXT_PUBLIC_USE_MOCK !== 'true'
) {
return
}
if (typeof window === 'undefined') return
const { setupAuthMock } = await import('./modules/auth.mock')
const { setupUserMock } = await import('./modules/user.mock')
setupAuthMock()
setupUserMock()
console.info('[Mock] API mock enabled')
}
五、Mock 实现
src/mock/modules/auth.mock.ts
javascript
import { ApiResponseHeader, ApiSingleResult } from '@/types/api-response';
import { User } from '@/types/user';
import Mock from 'mockjs';
const users = [
{
id: '1',
email: 'admin@example.com',
password: '123456',
name: 'Admin',
role: 'admin',
},
]
function createApiResponse<T>(
data: T,
code: number = 200,
message: string = 'Success'
): {
header: ApiResponseHeader;
result: ApiSingleResult<T>;
} {
return {
header: {
code,
message,
isSuccess: code === 200,
},
result: {
data,
},
}
}
export function setupAuthMock() {
Mock.mock('/api/auth/login', 'post', (req) => {
const body = JSON.parse(req.body)
const user = users.find(
(u) => u.email === body.email && u.password === body.password
)
if (!user) {
return createApiResponse(null, 401, 'Invalid credentials')
}
const loginResult = {
token: 'mock-jwt-token',
user: {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
},
}
return createApiResponse(loginResult)
})
Mock.mock('/api/auth/me', 'get', () => {
const userData: User = users[0] as User
return createApiResponse(userData)
})
Mock.mock('/api/auth/logout', 'post', () => {
return createApiResponse(null)
})
Mock.mock('/api/auth/register', 'post', (req) => {
const body = JSON.parse(req.body)
const user: User = {
id: Mock.mock('@guid'),
email: body.email,
name: body.name,
role: 'user',
}
const registerResult = {
token: 'mock-jwt-token',
user: user,
}
return createApiResponse(registerResult)
})
}
src/mock/modules/user.mock.ts
javascript
// src/mock/modules/user.mock.ts
import { ApiPagedListResult, ApiResponseHeader, ApiSingleResult } from '@/types/api-response';
import { User } from '@/types/user';
import Mock from 'mockjs';
const mockUsers: User[] = Mock.mock({
'list|10-20': [
{
'id|+1': 1,
email: '@email',
name: '@cname',
'role|1': ['admin', 'user', 'moderator'],
},
],
}).list.map((item: { id: number, email: string, name: string, role: string }) => ({
id: item.id.toString(),
email: item.email,
name: item.name,
role: item.role,
}));
function createSingleApiResponse<T>(
data: T,
code: number = 200,
message: string = 'Success'
): {
header: ApiResponseHeader;
result: ApiSingleResult<T>;
} {
return {
header: {
code,
message,
isSuccess: code === 200,
},
result: {
data,
},
};
}
function createPagedListApiResponse<T>(
data: T[],
page: number,
size: number,
total: number,
code: number = 200,
message: string = 'Success'
): {
header: ApiResponseHeader;
result: ApiPagedListResult<T>;
} {
const totalPage = Math.ceil(total / size);
return {
header: {
code,
message,
isSuccess: code === 200,
},
result: {
data,
pagination: {
page,
size,
total,
totalPage,
},
},
};
}
export function setupUserMock() {
Mock.mock(RegExp('/api/users/\\d+'), 'get', (options) => {
const url = options.url;
const userId = url.split('/').pop();
const user = mockUsers.find(u => u.id === userId);
if (!user) {
return createSingleApiResponse(null, 404, 'User not found');
}
return createSingleApiResponse(user);
});
Mock.mock('/api/users', 'get', (options) => {
const url = new URL(options.url, 'http://localhost');
const page = parseInt(url.searchParams.get('page') || '1');
const size = parseInt(url.searchParams.get('size') || '10');
const start = (page - 1) * size;
const end = start + size;
const pagedUsers = mockUsers.slice(start, end);
return createPagedListApiResponse(
pagedUsers,
page,
size,
mockUsers.length
);
});
Mock.mock('/api/users', 'post', (req) => {
const body = JSON.parse(req.body);
const newUser: User = {
id: (mockUsers.length + 1).toString(),
...body,
};
mockUsers.push(newUser);
return createSingleApiResponse(newUser);
});
Mock.mock(RegExp('/api/users/\\d+'), 'put', (req) => {
const url = req.url;
const userId = url.split('/').pop();
const body = JSON.parse(req.body);
const userIndex = mockUsers.findIndex(u => u.id === userId);
if (userIndex === -1) {
return createSingleApiResponse(null, 404, 'User not found');
}
mockUsers[userIndex] = { ...mockUsers[userIndex], ...body };
return createSingleApiResponse(mockUsers[userIndex]);
});
}
⚠️ 注意:Mock.js 无法真正设置 HttpOnly Cookie
我们在本地开发阶段:
假设 cookie 已存在
专注前端流程与 UI
六、在 Next.js 中启用 Mock(关键)
1.创建src/app/[locale]/providers.tsx
javascript
'use client'
import { setupMock } from '@/mock'
import { useEffect } from 'react'
export function Providers({ children }: { children: React.ReactNode }) {
useEffect(() => {
setupMock()
}, [])
return <>{children}</>
}
1.在 src/app/[locale]/layout.tsx 中添加providers
javascript
import '@/app/globals.css'
import { Footer } from '@/components/layout/Footer'
import { Header } from '@/components/layout/Header'
import { AuthProvider } from '@/context/AuthContext'
import { Locale, locales } from '@/i18n/config'
import { NextIntlClientProvider } from 'next-intl'
import { notFound } from 'next/navigation'
import { Providers } from './providers'
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: Locale }>
}) {
const { locale } = await params
if (!locales.includes(locale as Locale)) {
notFound()
}
const messages = (await import(`@/i18n/messages/${locale}.json`)).default
return (
<html lang={locale}>
<body className="min-h-screen bg-background text-foreground">
<Providers>
<NextIntlClientProvider locale={locale} messages={messages}>
<AuthProvider>
<Header />
{children}
<Footer />
</AuthProvider>
</NextIntlClientProvider>
</Providers>
</body>
</html>
)
}
2.Axios 不需要任何改动
javascript
// src/lib/http/client.ts
import axios from 'axios'
import { getCookie } from 'cookies-next'
if (!process.env.NEXT_PUBLIC_API_BASE) {
throw new Error('NEXT_PUBLIC_API_BASE is not defined')
}
export const http = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE,
timeout: 10000,
withCredentials: true, // 为将来登录态做准备
})
http.interceptors.request.use(
config => {
const token = getCookie('auth_token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
http.interceptors.response.use(
response => {
if (response.data && response.data.header && typeof response.data.header.isSuccess === 'boolean') {
const apiResponse = response.data;
if (!apiResponse.header.isSuccess) {
alert(apiResponse.header.message || 'API Error');
throw new Error(apiResponse.header.message || 'API Error');
}
}
return response;
},
error => {
console.error(error)
const message =
error.response?.data?.message ||
error.message ||
'Network Error'
return Promise.reject(new Error(message))
}
)
Mock.js 会直接拦截请求。
七、登录流程现在如何跑通
Login Page
↓
react-hook-form submit
↓
authService.login()
↓
Mock.js 返回 user
↓
/auth/me 可获取用户
↓
Header 显示登录态
✔ 不依赖后端
✔ 页面与权限逻辑全部可验证
八、常见问题(你一定会遇到)
Q1:proxy(middleware) 能用 Mock 吗?
❌ 不能。
原因:
-
proxy(middleware) 运行在 Edge
-
Mock.js 运行在浏览器
👉 本地阶段:
-
proxy(middleware) 只验证「是否有 cookie」
-
不验证 token 真伪
Q2:那 cookie 怎么办?
开发阶段可选方案:
-
A:proxy(middleware) 中放行 mock 用户
-
B:临时写死
auth_token
示例(开发用):
javascript
const token = request.cookies.get('auth_token')?.value
九、Mock 的生命周期(非常重要)
| 阶段 | 是否使用 Mock |
|---|---|
| UI / 表单 / 权限 | ✅ 必须 |
| 后端联调 | ❌ 移除 |
| 生产环境 | ❌ 绝对不能 |
十、Mock 与真实 API切换
修改.env.local为
javascript
NEXT_PUBLIC_API_BASE=http://localhost:4000/api
NEXT_PUBLIC_USE_MOCK=false
NEXT_PUBLIC_APP_NAME=Enterprise Web local
NEXT_PUBLIC_APP_ENV=local
修改 .env.development为
javascript
NEXT_PUBLIC_API_BASE=/api
NEXT_PUBLIC_USE_MOCK=true
NEXT_PUBLIC_APP_NAME=Enterprise Web Development
NEXT_PUBLIC_APP_ENV=development
修改.env.production为
javascript
NEXT_PUBLIC_API_BASE=https://api.enterprise.com
NEXT_PUBLIC_USE_MOCK=false
NEXT_PUBLIC_APP_NAME=Enterprise Web
NEXT_PUBLIC_APP_ENV=production
安装dotenv依赖
javascript
pnpm add dotenv --save-dev
在package.json中添加脚本
javascript
"scripts": {
"dev": "next dev",
"dev:local": "dotenv -e .env.local -- next dev",
"dev:development": "dotenv -e .env.development -- next dev",
"dev:staging": "dotenv -e .env.staging -- next dev",
"dev:production": "dotenv -e .env.production -- next dev",
"build": "next build",
"build:local": "dotenv -e .env.local -- next build",
"build:development": "dotenv -e .env.development -- next build",
"build:staging": "dotenv -e .env.staging -- next build",
"build:production": "dotenv -e .env.production -- next build",
"start": "next start",
"start:local": "dotenv -e .env.local -- next start",
"start:development": "dotenv -e .env.development -- next start",
"start:staging": "dotenv -e .env.staging -- next start",
"start:production": "dotenv -e .env.production -- next start",
"lint": "eslint",
"lint:fix": "eslint --fix",
"lint:check": "eslint --check",
"lint:format": "prettier --write",
"lint:format:check": "prettier --check"
},
运行
javascript
pnpm dev:development
时,将使用mock.js
运行
javascript
pnpm dev:local
pnpm dev:production
时,将使用真实的api
十、你现在的工程状态(总结)
你现在已经拥有:
-
可运行的登录 / 注册 / Header 联动
-
proxy(middleware) + locale 鉴权逻辑
-
完整前端闭环(即使没有后端)
这已经是真实企业项目的前端开发节奏。