在 Next.js 框架中,封装统一的请求方法并区分服务端和客户端是一个常见需求,因为 Next.js 既支持服务端渲染 (SSR) 也支持客户端渲染 (CSR)。服务端和客户端的环境不同(例如,服务端没有 `window` 对象,客户端可能需要处理用户认证 token 等),因此需要分别处理两者的请求逻辑。 以下是一个封装统一请求方法的示例,基于 `axios` 库,区分服务端和客户端的请求逻辑: ### 1. 创建请求工具文件 我们可以创建一个 `utils/request.js` 文件,用于封装请求方法,并根据环境(服务端或客户端)进行不同的处理。 ```javascript import axios from 'axios'; import { getSession } from 'next-auth/react'; // 假设使用 next-auth 处理认证 // 创建 axios 实例 const createAxiosInstance = (baseURL, headers = {}) => { return axios.create({ baseURL: baseURL || process.env.NEXT_PUBLIC_API_BASE_URL, // API 基础 URL timeout: 10000, // 请求超时时间 headers: { 'Content-Type': 'application/json', ...headers, }, }); }; // 拦截器 - 请求前处理 const setupInterceptors = (instance, isServer) => { instance.interceptors.request.use( async (config) => { // 客户端环境:从 session 获取 token if (!isServer) { const session = await getSession(); if (session?.accessToken) { config.headers.Authorization = `Bearer ${session.accessToken}`; } } // 服务端环境:可能从其他地方获取 token 或不设置 return config; }, (error) => { return Promise.reject(error); } ); // 响应拦截器 instance.interceptors.response.use( (response) => { return response.data; // 返回响应数据 }, (error) => { // 统一处理错误 const errorMessage = error.response?.data?.message || 'Request failed'; return Promise.reject(new Error(errorMessage)); } ); return instance; }; // 服务端请求方法 export const serverRequest = (headers = {}) => { const instance = createAxiosInstance(process.env.NEXT_PUBLIC_API_BASE_URL, headers); return setupInterceptors(instance, true); }; // 客户端请求方法 export const clientRequest = (headers = {}) => { const instance = createAxiosInstance(process.env.NEXT_PUBLIC_API_BASE_URL, headers); return setupInterceptors(instance, false); }; // 统一请求方法(根据环境选择服务端或客户端) export const request = (headers = {}) => { const isServer = typeof window === 'undefined'; return isServer ? serverRequest(headers) : clientRequest(headers); }; // 封装常用请求方法 export const api = { get: (url, params = {}, headers = {}) => request(headers).get(url, { params }), post: (url, data = {}, headers = {}) => request(headers).post(url, data), put: (url, data = {}, headers = {}) => request(headers).put(url, data), delete: (url, headers = {}) => request(headers).delete(url), }; ``` ### 2. 使用说明 - **服务端请求**:在 `getServerSideProps` 或 `getStaticProps` 中,可以直接使用 `serverRequest` 或通过 `request` 自动判断环境。 - **客户端请求**:在 React 组件中(例如 `useEffect` 或事件处理中),可以使用 `clientRequest` 或通过 `request` 自动判断环境。 #### 服务端使用示例(在 `getServerSideProps` 中) ```javascript export async function getServerSideProps(context) { try { // 服务端请求,传递额外的 headers(如果需要) const response = await api.get('/api/data', {}, { 'Custom-Header': 'value' }); return { props: { data: response, }, }; } catch (error) { console.error('Server request error:', error); return { props: { data: null, }, }; } } ``` #### 客户端使用示例(在 React 组件中) ```javascript import { useEffect, useState } from 'react'; import { api } from '../utils/request'; export default function Home() { const [data, setData] = useState(null); useEffect(() => { const fetchData = async () => { try { const response = await api.get('/api/data'); setData(response); } catch (error) { console.error('Client request error:', error); } }; fetchData(); }, []); return ( <div> {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Loading...'} </div> ); } ``` ### 3. 关键点说明 - **环境判断**:通过 `typeof window === 'undefined'` 判断当前是否为服务端环境。 - **请求拦截器**:在请求前可以添加认证 token 或其他逻辑,客户端可以从 `next-auth` 或本地存储中获取 token,服务端可能需要从上下文或其他方式获取。 - **错误处理**:统一处理请求错误,返回友好的错误信息。 - **环境变量**:API 基础 URL 可以通过环境变量 `NEXT_PUBLIC_API_BASE_URL` 配置,确保在 `.env` 文件中正确设置。 ### 4. 扩展功能 - **认证处理**:如果项目使用其他认证方式(如 JWT 存储在 cookie 中),可以在服务端通过 `context.req.cookies` 获取 cookie,在客户端通过 `document.cookie` 或其他方式获取。 - **自定义配置**:可以根据需求扩展 `api` 对象,支持更多请求方法(如 `patch`)或添加自定义参数。 - **重试机制**:可以在拦截器中添加请求重试逻辑,处理网络不稳定等问题。 通过以上方式,你可以在 Next.js 中实现一个统一且灵活的请求封装,适用于服务端和客户端的不同场景。
要使用 Next.js 对接支付宝支付,遵循以下完整流程及代码实现: ### 核心流程 1. **创建支付宝应用** - 注册[支付宝开放平台](https://open.alipay.com/) - 创建网站应用,获取`APP_ID` - 配置`RSA2`密钥(应用公钥+支付宝公钥) 2. **Next.js服务端支付接口** ```jsx // /pages/api/alipay.js import AlipaySdk from 'alipay-sdk'; import { AlipayFormData } from 'alipay-sdk/lib/form'; export default async function handler(req, res) { if (req.method === 'POST') { const { orderId, amount, subject } = req.body; // 支付宝SDK配置 const alipaySdk = new AlipaySdk({ appId: process.env.ALIPAY_APP_ID, privateKey: process.env.ALIPAY_PRIVATE_KEY.replace(/\\n/g, '\n'), alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY.replace(/\\n/g, '\n'), gateway: 'https://openapi.alipaydev.com/gateway.do' // 沙箱环境 }); // 创建支付表单 const formData = new AlipayFormData(); formData.setMethod('get'); formData.addField('returnUrl', `${process.env.BASE_URL}/payment/success`); formData.addField('notifyUrl', `${process.env.BASE_URL}/api/alipay/notify`); // 业务参数 formData.addField('bizContent', { out_trade_no: orderId, // 商户订单号 product_code: 'FAST_INSTANT_TRADE_PAY', total_amount: amount, // 金额(元) subject: subject, // 订单标题 body: '商品描述' }); try { // 生成支付链接 const result = await alipaySdk.exec( 'alipay.trade.page.pay', {}, { formData } ); res.status(200).json({ url: result }); } catch (error) { res.status(500).json({ error: '支付创建失败' }); } } else { res.setHeader('Allow', 'POST'); res.status(405).end('Method Not Allowed'); } } ``` 3. **支付通知接口** ```jsx // /pages/api/alipay/notify.js export default async function handler(req, res) { if (req.method === 'POST') { const params = req.body; // 验证签名(需自行实现签名验证逻辑) const signVerified = verifyAlipaySignature(params); if (signVerified && params.trade_status === 'TRADE_SUCCESS') { // 更新数据库订单状态 await updateOrderStatus(params.out_trade_no, 'paid'); res.status(200).send('success'); } else { res.status(400).send('fail'); } } else { res.status(405).end(); } } ``` 4. **前端支付触发** ```jsx // /pages/payment.js import { useState } from 'react'; export default function Payment() { const [loading, setLoading] = useState(false); const handlePayment = async () => { setLoading(true); try { const response = await fetch('/api/alipay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: `ORDER_${Date.now()}`, amount: 99.99, subject: 'VIP会员' }) }); const { url } = await response.json(); window.location.href = url; // 跳转支付宝支付页 } catch (error) { alert('支付失败'); setLoading(false); } }; return ( <button onClick={handlePayment} disabled={loading} className="bg-blue-600 text-white px-6 py-3 rounded hover:bg-blue-700" > {loading ? '处理中...' : '立即支付'} </button> ); } ``` ### 环境变量配置(.env.local) ```ini ALIPAY_APP_ID=您的应用ID ALIPAY_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" ALIPAY_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" BASE_URL=https://yourdomain.com ``` ### 必备依赖 ```bash npm install alipay-sdk ``` ### 流程验证步骤 1. **本地测试** - 使用支付宝沙箱环境测试支付流程 - [沙箱账号生成工具](https://open.alipay.com/platform/manageHome.htm) 2. **生产部署** - 切换`gateway`为正式环境:`https://openapi.alipay.com/gateway.do` - 配置域名白名单和异步通知URL - 申请HTTPS证书(支付宝要求全站HTTPS) 3. **支付结果处理** - 同步跳转:通过`returnUrl`返回支付结果页 - 异步通知:处理订单状态更新(需幂等设计) > **注意事项** > 1. 私钥需保持单行换行符格式(使用`\n`) > 2. 金额单位为元(保留两位小数) > 3. 订单号需确保全局唯一性 > 4. 生产环境必须使用HTTPS 完整实现后,支付流程:用户点击支付 → 生成支付宝订单 → 跳转支付宝支付 → 支付成功返回商户网站 → 异步通知确认订单。
在 Next.js 中实现路由的动态渲染,并根据后端设置的当前用户权限来控制哪些路由可以渲染,类似于 Vue Router 的权限路由管理,是完全可行的。Next.js 虽然不像 Vue Router 那样有内置的路由守卫(Route Guard),但可以通过以下几种方式结合其特性(如中间件、动态路由、API 路由等)实现类似的功能。 下面我将详细介绍如何在 Next.js 中实现基于用户权限的路由动态渲染,并提供代码示例。 --- ### 实现思路 1. **用户权限获取**:从后端获取当前用户的角色或权限列表(例如通过 API 调用或 cookie/token 解析)。 2. **路由权限控制**: - 使用 Next.js 的 **中间件 (Middleware)** 在请求到达页面前进行权限校验,决定是否允许访问某些路由。 - 或者在页面级别(如 `getServerSideProps` 或 `getStaticProps`)根据权限动态决定是否渲染页面。 3. **动态路由渲染**:根据用户权限动态过滤或生成可访问的路由列表,并在前端导航(如菜单或链接)中只显示用户有权限访问的路由。 4. **重定向或错误处理**:对于无权限访问的路由,重定向到登录页或显示无权限提示页面。 --- ### 方法一:使用中间件 (Middleware) 进行全局路由权限控制 Next.js 的中间件(`middleware.js` 或 `middleware.ts`)运行在请求到达页面之前,非常适合用来实现全局路由守卫,检查用户权限并决定是否允许访问某些路由。 #### 实现步骤 1. **创建中间件文件**: 在项目根目录下创建 `middleware.ts`(或 `middleware.js`)。 ```typescript:middleware.ts import { NextResponse, NextRequest } from 'next/server'; // 定义需要权限控制的路由及其对应角色要求 const protectedRoutes = { '/dashboard': ['admin', 'editor'], // 只有 admin 和 editor 角色可以访问 '/profile': ['user', 'admin', 'editor'], // user、admin、editor 都可以访问 }; export async function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; const authToken = request.cookies.get('auth_token')?.value; // 如果没有 token,说明未登录,重定向到登录页面 if (!authToken) { if (pathname !== '/login') { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } // 模拟从 token 或后端 API 获取用户角色(这里假设从 cookie 或 API 获取) let userRole = 'user'; // 假设默认角色为 user try { // 这里可以调用 API 验证 token 并获取用户角色 // 例如:const response = await fetch('/api/auth/validate', { headers: { Authorization: `Bearer ${authToken}` } }); // const data = await response.json(); // userRole = data.role; } catch (error) { console.error('Failed to validate token:', error); return NextResponse.redirect(new URL('/login', request.url)); } // 检查当前路由是否需要权限控制 const routeRoles = protectedRoutes[pathname]; if (routeRoles && !routeRoles.includes(userRole)) { // 无权限访问,重定向到无权限页面或首页 return NextResponse.redirect(new URL('/unauthorized', request.url)); } // 有权限,继续请求 return NextResponse.next(); } // 定义中间件匹配的路径 export const config = { matcher: ['/dashboard', '/profile', '/login'], }; ``` 2. **创建无权限页面**: 在 `app/unauthorized/page.tsx` 中创建一个页面,用于显示无权限提示。 ```typescript:app/unauthorized/page.tsx "use client"; import React from 'react'; import Link from 'next/link'; const Unauthorized = () => { return ( <div className="flex flex-col items-center justify-center h-screen"> <h1 className="text-4xl font-bold text-red-600">Access Denied</h1> <p className="text-lg mt-4">You do not have permission to access this page.</p> <Link href="/" className="mt-6 text-blue-500 hover:underline"> Back to Home </Link> </div> ); }; export default Unauthorized; ``` 3. **动态导航菜单**: 在前端导航组件中,根据用户角色动态渲染可访问的路由链接。 ```typescript:components/Navbar.tsx "use client"; import React, { useEffect, useState } from 'react'; import Link from 'next/link'; const Navbar = () => { const [userRole, setUserRole] = useState<string | null>(null); useEffect(() => { // 假设从 cookie 或 API 获取用户角色 const fetchRole = async () => { // 这里可以调用 API 获取角色 setUserRole('admin'); // 模拟角色为 admin }; fetchRole(); }, []); // 定义路由及其对应的角色权限 const routes = [ { path: '/', label: 'Home', roles: ['user', 'admin', 'editor'] }, { path: '/dashboard', label: 'Dashboard', roles: ['admin', 'editor'] }, { path: '/profile', label: 'Profile', roles: ['user', 'admin', 'editor'] }, ]; return ( <nav className="bg-gray-800 text-white p-4"> <ul className="flex space-x-4"> {routes .filter(route => userRole && route.roles.includes(userRole)) .map(route => ( <li key={route.path}> <Link href={route.path} className="hover:underline"> {route.label} </Link> </li> ))} </ul> </nav> ); }; export default Navbar; ``` #### 优点与缺点 - **优点**:中间件在请求到达页面之前运行,性能高效,适用于全局路由权限控制;可以轻松扩展到复杂的权限逻辑。 - **缺点**:中间件无法直接访问前端状态(如 React Context),需要通过 API 或 cookie 获取用户权限信息;对静态页面(SSG)可能需要额外处理。 --- ### 方法二:使用 `getServerSideProps` 或 `getStaticProps` 进行页面级权限控制 如果您不希望使用中间件,或者只对部分页面进行权限控制,可以在页面级别使用 `getServerSideProps` 或 `getStaticProps` 结合 `getSession` 等方式检查权限,并决定是否渲染页面。 #### 实现步骤 1. **在页面中添加权限校验**: 在需要保护的页面中,使用 `getServerSideProps` 检查用户权限。 ```typescript:app/dashboard/page.tsx import { GetServerSideProps } from 'next'; import React from 'react'; const Dashboard = () => { return ( <div> <h1>Dashboard</h1> <p>Welcome to the dashboard. Only authorized users can see this.</p> </div> ); }; export const getServerSideProps: GetServerSideProps = async (context) => { const { req } = context; const cookieString = req.headers.cookie || ''; // 解析 cookie 获取 auth_token const getCookie = (name: string) => { if (!cookieString) return null; const cookies = cookieString.split(';'); for (let cookie of cookies) { cookie = cookie.trim(); if (cookie.startsWith(name + '=')) { return cookie.split('=')[1]; } } return null; }; const authToken = getCookie('auth_token'); if (!authToken) { return { redirect: { destination: '/login', permanent: false, }, }; } // 模拟验证 token 和角色(可以调用 API) const userRole = 'admin'; // 假设从 API 获取 const allowedRoles = ['admin', 'editor']; if (!allowedRoles.includes(userRole)) { return { redirect: { destination: '/unauthorized', permanent: false, }, }; } return { props: {}, // 可以将用户数据传递给页面 }; }; export default Dashboard; ``` 2. **动态导航菜单**: 与方法一相同,根据用户角色动态渲染导航菜单。 #### 优点与缺点 - **优点**:适合对特定页面进行权限控制,可以直接在页面级别获取用户数据并传递给组件;易于与 SSR 结合。 - **缺点**:每个页面都需要单独编写权限逻辑,代码重复性较高;不适合全局路由控制(需要重复代码)。 --- ### 方法三:结合 React Context 或状态管理库实现前端动态路由 如果权限数据已经在前端状态(如 Context、Redux、Zustand)中,可以在前端通过条件渲染动态显示路由或页面内容。 #### 实现步骤 1. **使用 Context 存储用户权限**: 参考之前提供的 Context 示例,将用户角色或权限存储在全局状态中。 ```typescript:context/AppContext.tsx "use client"; import React, { createContext, useContext, useState, useEffect } from 'react'; interface AppContextType { isLoggedIn: boolean; userRole: string | null; setLoggedIn: (isLoggedIn: boolean) => void; setUserRole: (role: string | null) => void; } const AppContext = createContext<AppContextType | undefined>(undefined); export const useAppContext = () => { const context = useContext(AppContext); if (context === undefined) { throw new Error('useAppContext must be used within an AppProvider'); } return context; }; export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [isLoggedIn, setLoggedIn] = useState(false); const [userRole, setUserRole] = useState<string | null>(null); useEffect(() => { // 从 cookie 或 API 初始化用户状态 const token = document.cookie.split('; ').find(row => row.startsWith('auth_token='))?.split('=')[1]; if (token) { setLoggedIn(true); setUserRole('admin'); // 假设从 API 获取角色 } }, []); return ( <AppContext.Provider value={{ isLoggedIn, userRole, setLoggedIn, setUserRole }}> {children} </AppContext.Provider> ); }; ``` 2. **条件渲染路由或组件**: 在页面或布局中根据用户角色条件渲染内容。 ```typescript:app/layout.tsx "use client"; import { useAppContext } from '@/context/AppContext'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; import React from 'react'; import { useRouter } from 'next/navigation'; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export default function RootLayout({ children }: { children: React.ReactNode }) { const { isLoggedIn, userRole } = useAppContext(); const router = useRouter(); // 示例:如果未登录,重定向到登录页面 React.useEffect(() => { if (!isLoggedIn && window.location.pathname !== '/login') { router.push('/login'); } }, [isLoggedIn, router]); return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased !bg-white`}> <Navbar /> <div className="mt-[63px]"> {children} </div> <Footer /> </body> </html> ); } ``` 3. **动态导航**: 参考方法一中 `Navbar.tsx` 的实现,根据用户角色过滤显示路由。 #### 优点与缺点 - **优点**:权限逻辑完全在前端,易于与状态管理结合;适合客户端渲染(CSR)场景。 - **缺点**:不适合 SSR 或 SSG,因为权限校验在客户端执行,可能导致闪烁或不安全的短暂内容显示;需要额外的重定向逻辑。 --- ### 总结与推荐 - **全局权限控制**:使用 **中间件 (Middleware)**,这是 Next.js 中最接近 Vue Router 路由守卫的方式,适用于大多数场景,性能和安全性较高。 - **页面级权限控制**:使用 `getServerSideProps` 或 `getStaticProps`,适合少数特定页面需要权限控制的场景。 - **前端动态渲染**:结合 Context 或状态管理库,适合客户端渲染项目,但不推荐用于安全性要求高的场景(应结合后端校验)。 #### 完整实现流程建议 1. 使用中间件校验用户权限并重定向无权限请求。 2. 在前端状态管理中存储用户角色,动态渲染导航菜单或页面内容。 3. 后端 API 提供权限校验接口,确保 token 和角色信息准确。 如果您有具体的路由结构或权限需求(例如特定的角色和路由映射),请提供更多细节,我可以进一步定制代码实现。
在 Next.js 框架中,全局状态管理是一个常见需求,特别是在构建复杂应用时需要共享状态(如用户登录信息、主题设置等)跨多个组件或页面。Next.js 本身不内置全局状态管理工具,但可以通过结合流行的状态管理库或 React 的内置功能来实现。以下我将详细介绍几种在 Next.js 中实现全局状态管理的方法,供您根据项目需求选择合适的方案。 ### 方法一:使用 React Context API React 内置的 Context API 是一种轻量级的全局状态管理方式,适用于中小型项目或简单状态管理需求。Next.js 作为 React 的框架,可以直接使用 Context API。 #### 实现步骤 1. **创建 Context 文件**: 在项目中创建一个 Context 文件,用于定义全局状态和提供状态的 Provider。 ```typescript:context/AppContext.tsx "use client"; import React, { createContext, useContext, useState, useEffect } from 'react'; // 定义全局状态的类型 interface AppContextType { isLoggedIn: boolean; setLoggedIn: (isLoggedIn: boolean) => void; user: { name: string; id: string } | null; setUser: (user: { name: string; id: string } | null) => void; } // 创建 Context const AppContext = createContext<AppContextType | undefined>(undefined); // 自定义 Hook 用于访问 Context export const useAppContext = () => { const context = useContext(AppContext); if (context === undefined) { throw new Error('useAppContext must be used within an AppProvider'); } return context; }; // Provider 组件 export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [isLoggedIn, setLoggedIn] = useState(false); const [user, setUser] = useState<{ name: string; id: string } | null>(null); // 示例:从 localStorage 或 cookie 初始化状态 useEffect(() => { const storedLoggedIn = localStorage.getItem('isLoggedIn') === 'true'; setLoggedIn(storedLoggedIn); // 可以添加更多初始化逻辑 }, []); return ( <AppContext.Provider value={{ isLoggedIn, setLoggedIn, user, setUser }}> {children} </AppContext.Provider> ); }; ``` 2. **在 RootLayout 中包裹 Provider**: 在 `app/layout.tsx` 或主应用入口文件中使用 `AppProvider` 包裹整个应用。 ```typescript:app/layout.tsx "use client"; import { AppProvider } from '@/context/AppContext'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; import React from 'react'; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased !bg-white`}> <AppProvider> <Navbar /> <div className="mt-[63px]">{children}</div> <Footer /> </AppProvider> </body> </html> ); } ``` 3. **在组件中使用 Context**: 在任何组件或页面中使用 `useAppContext` Hook 访问或更新全局状态。 ```typescript:components/Navbar.tsx "use client"; import React from 'react'; import { useAppContext } from '@/context/AppContext'; const Navbar = () => { const { isLoggedIn, setLoggedIn } = useAppContext(); const handleLogout = () => { setLoggedIn(false); localStorage.setItem('isLoggedIn', 'false'); }; return ( <nav> {isLoggedIn ? ( <button onClick={handleLogout}>Logout</button> ) : ( <button onClick={() => setLoggedIn(true)}>Login</button> )} </nav> ); }; export default Navbar; ``` #### 优点与缺点 - **优点**:内置于 React,无需额外依赖,轻量级,适合简单状态管理。 - **缺点**:状态更新可能导致整个 Provider 下的组件重新渲染,性能可能在复杂状态管理中受限;难以处理复杂的异步逻辑。 --- ### 方法二:使用 Redux 或 Redux Toolkit 对于中大型项目,Redux 或其简化版 Redux Toolkit 是非常流行的全局状态管理工具,提供了强大的状态管理和调试能力。Next.js 可以很好地集成 Redux。 #### 实现步骤 1. **安装依赖**: ```bash npm install @reduxjs/toolkit react-redux ``` 2. **创建 Store 和 Slice**: 创建 Redux store 和状态切片(slice)来管理全局状态。 ```typescript:store/store.ts import { configureStore } from '@reduxjs/toolkit'; import authReducer from './authSlice'; export const store = configureStore({ reducer: { auth: authReducer, }, }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; ``` ```typescript:store/authSlice.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit'; interface AuthState { isLoggedIn: boolean; user: { name: string; id: string } | null; } const initialState: AuthState = { isLoggedIn: false, user: null, }; const authSlice = createSlice({ name: 'auth', initialState, reducers: { setLoggedIn: (state, action: PayloadAction<boolean>) => { state.isLoggedIn = action.payload; }, setUser: (state, action: PayloadAction<{ name: string; id: string } | null>) => { state.user = action.payload; }, }, }); export const { setLoggedIn, setUser } = authSlice.actions; export default authSlice.reducer; ``` 3. **在应用入口提供 Store**: 在 `app/layout.tsx` 或自定义 `_app.tsx` 中使用 Redux 的 `Provider` 包裹应用。 ```typescript:app/layout.tsx "use client"; import { Provider } from 'react-redux'; import { store } from '@/store/store'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; import React from 'react'; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased !bg-white`}> <Provider store={store}> <Navbar /> <div className="mt-[63px]">{children}</div> <Footer /> </Provider> </body> </html> ); } ``` 4. **在组件中使用 Redux**: 使用 `useSelector` 和 `useDispatch` 访问或更新全局状态。 ```typescript:components/Navbar.tsx "use client"; import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { RootState } from '@/store/store'; import { setLoggedIn } from '@/store/authSlice'; const Navbar = () => { const isLoggedIn = useSelector((state: RootState) => state.auth.isLoggedIn); const dispatch = useDispatch(); const handleLogout = () => { dispatch(setLoggedIn(false)); localStorage.setItem('isLoggedIn', 'false'); }; return ( <nav> {isLoggedIn ? ( <button onClick={handleLogout}>Logout</button> ) : ( <button onClick={() => dispatch(setLoggedIn(true))}>Login</button> )} </nav> ); }; export default Navbar; ``` #### 优点与缺点 - **优点**:适合复杂应用,支持中间件(处理异步逻辑)、强大的调试工具(如 Redux DevTools)、状态管理更结构化。 - **缺点**:学习曲线较陡,代码量较多,中小型项目可能显得过于复杂。 --- ### 方法三:使用 Zustand Zustand 是一个轻量级的状态管理库,结合了 Redux 和 Context 的优点,API 简洁,适合 Next.js 项目。 #### 实现步骤 1. **安装依赖**: ```bash npm install zustand ``` 2. **创建 Store**: 定义一个全局 store,包含状态和更新函数。 ```typescript:store/authStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface AuthState { isLoggedIn: boolean; user: { name: string; id: string } | null; setLoggedIn: (isLoggedIn: boolean) => void; setUser: (user: { name: string; id: string } | null) => void; } export const useAuthStore = create<AuthState>()( persist( (set) => ({ isLoggedIn: false, user: null, setLoggedIn: (isLoggedIn) => set({ isLoggedIn }), setUser: (user) => set({ user }), }), { name: 'auth-storage', // 持久化到 localStorage 的键名 } ) ); ``` 3. **在组件中使用 Store**: 直接在任何组件中使用 `useAuthStore` 访问或更新状态,无需额外的 Provider。 ```typescript:components/Navbar.tsx "use client"; import React from 'react'; import { useAuthStore } from '@/store/authStore'; const Navbar = () => { const { isLoggedIn, setLoggedIn } = useAuthStore(); const handleLogout = () => { setLoggedIn(false); }; return ( <nav> {isLoggedIn ? ( <button onClick={handleLogout}>Logout</button> ) : ( <button onClick={() => setLoggedIn(true)}>Login</button> )} </nav> ); }; export default Navbar; ``` #### 优点与缺点 - **优点**:API 简洁,无需 Provider 包裹,支持持久化(如 localStorage),性能优化好。 - **缺点**:功能不如 Redux 强大,复杂异步逻辑可能需要额外处理。 --- ### 方法四:使用 Recoil Recoil 是 Facebook 开发的状态管理库,专为 React 设计,适用于需要精细状态管理的 Next.js 项目。 #### 实现步骤 1. **安装依赖**: ```bash npm install recoil ``` 2. **创建 Atom**: 定义状态的基本单位(atom)。 ```typescript:store/authAtom.ts import { atom } from 'recoil'; export const authState = atom({ key: 'authState', default: { isLoggedIn: false, user: null as { name: string; id: string } | null, }, }); ``` 3. **在 RootLayout 中提供 RecoilRoot**: 在应用入口包裹 `RecoilRoot`。 ```typescript:app/layout.tsx "use client"; import { RecoilRoot } from 'recoil'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; import React from 'react'; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] }); export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased !bg-white`}> <RecoilRoot> <Navbar /> <div className="mt-[63px]">{children}</div> <Footer /> </RecoilRoot> </body> </html> ); } ``` 4. **在组件中使用 Recoil**: 使用 `useRecoilState` 访问和更新状态。 ```typescript:components/Navbar.tsx "use client"; import React from 'react'; import { useRecoilState } from 'recoil'; import { authState } from '@/store/authAtom'; const Navbar = () => { const [auth, setAuth] = useRecoilState(authState); const handleLogout = () => { setAuth({ ...auth, isLoggedIn: false }); }; return ( <nav> {auth.isLoggedIn ? ( <button onClick={handleLogout}>Logout</button> ) : ( <button onClick={() => setAuth({ ...auth, isLoggedIn: true })}>Login</button> )} </nav> ); }; export default Navbar; ``` #### 优点与缺点 - **优点**:支持细粒度状态管理,性能优化好,适合复杂应用。 - **缺点**:相对较新,社区资源较少,学习曲线稍陡。 --- ### 结合 Next.js 特性的注意事项 1. **客户端与服务器端渲染(SSR)**: - Next.js 支持 SSR 和 SSG,状态管理库(如 Redux、Zustand)在 SSR 时需要特别处理初始状态,确保服务器端渲染时状态一致。 - 例如,使用 Zustand 的 `persist` 中间件或在 `getServerSideProps` 中初始化 Redux store。 2. **持久化状态**: - 登录状态等需要持久化的数据可以存储在 `localStorage` 或 `cookie` 中,并在应用启动时同步到全局状态。 3. **性能优化**: - 使用选择器(selector)或惰性加载减少不必要的重新渲染,特别是在使用 Context 或 Redux 时。 --- ### 推荐选择 - **小项目或简单状态**:使用 React Context API,代码简单,无额外依赖。 - **中大型项目或复杂状态**:使用 Redux Toolkit,结构化强,支持复杂逻辑。 - **轻量级且现代化**:使用 Zustand,API 简洁,支持持久化。 - **需要细粒度控制**:使用 Recoil,性能优化好。 根据您的代码中已经有登录状态管理需求(如 `isLoggedIn`),如果项目较小,可以直接使用 Context API(如方法一)将其扩展到全局状态管理。如果您计划扩展更多复杂功能(如异步 API 调用、多个模块状态),建议选择 Zustand 或 Redux Toolkit。 如果您有具体的项目需求或偏好,请告诉我,我可以进一步提供更详细的实现代码或优化建议。
我仔细审查了您提供的中间件代码,并结合您的需求分析了可能的逻辑漏洞。以下是对代码的详细分析、潜在问题以及改进建议,同时确保代码的完整性。 ### 代码分析 这段中间件代码的主要功能是: - 保护以 `/api/` 开头的路径,验证用户是否具有有效的 `auth-token`。 - 设置白名单,允许某些 API 路径(如 `/api/auth/login`)或特定模式(如 `/api/articles/数字`)无需认证即可访问。 - 通过调用 `/api/check-auth` API 验证 token 的有效性。 - 配置 `matcher` 规则,应用中间件逻辑到特定路径。 当前代码逻辑: 1. 定义白名单路径(`whiteList`)和文章详情路径的正则表达式(`articleDetailPattern`)。 2. 检查请求路径是否在白名单内或匹配文章详情路径。 3. 对于非白名单的 `/api/` 路径,验证 `auth-token` 是否存在,并通过 `/api/check-auth` 确认其有效性。 4. 如果认证失败,尝试重定向到 `/articles` 或返回错误响应。 5. 配置 `matcher` 规则,匹配除某些静态资源外的所有路径。 ### 潜在逻辑漏洞与问题 1. **白名单与路径匹配逻辑问题**: - 当前白名单匹配使用 `startsWith`,这可能导致意外匹配。例如,`/api/auth/login-test` 会被误认为是 `/api/auth/login` 的子路径而通过白名单检查。 - 建议改进:对白名单路径进行精确匹配,或者对部分需要前缀匹配的路径(如 `/api/system-info`)单独处理。 - 当前 `articleDetailPattern` 正则表达式 `^\/api\/articles\/\d+$` 限制了文章 ID 必须是纯数字,这与您的需求一致,但如果未来需求变化(例如支持非数字 ID),需要调整。 2. **重定向逻辑与 API 响应的冲突**: - 在认证失败时,代码中既有重定向逻辑(`errorRedirect`)又有 JSON 错误响应逻辑(`NextResponse.json`)。注释掉的部分重定向逻辑与当前返回 JSON 的逻辑冲突,可能导致维护困惑。 - 建议:统一处理方式。对于 API 请求,推荐始终返回 JSON 错误响应,而不是重定向(因为 API 调用方通常是前端代码或第三方工具,期望 JSON 响应)。 3. **Token 验证依赖外部 API 调用的风险**: - 代码通过调用 `/api/check-auth` 验证 token,这依赖于该 API 的可用性和性能。如果 `/api/check-auth` 不可用或响应缓慢,整个中间件都会受到影响。 - 潜在问题:如果 `/api/check-auth` 本身被攻击或过载,可能导致中间件逻辑阻塞或失效。 - 建议:考虑增加超时机制或本地缓存已验证的 token 状态(如果适用),减少对外部 API 的频繁调用。 4. **Cookie 处理的安全性问题**: - 代码将 `auth-token` 从请求的 cookie 直接转发到 `/api/check-auth`,但未检查 token 是否被篡改或伪造。如果攻击者伪造一个无效 token,可能会导致不必要的 API 调用。 - 建议:可以在中间件层增加 token 格式验证(如长度、字符集等),在明显无效时直接拒绝请求。 5. **路径匹配器的范围过广**: - 当前 `matcher` 配置为 `['/((?!api|_next/static|_next/image|favicon.ico).*)']`,排除了 `/api` 和静态资源,但实际上中间件逻辑只处理 `/api/` 开头的路径。这导致中间件不必要地处理了大量非 API 请求(如页面路由),浪费资源。 - 建议:将 `matcher` 限制为只匹配 `/api/:path*`,因为中间件逻辑只针对 API 路径有效。 6. **错误处理不够完善**: - 在 `fetch` 调用 `/api/check-auth` 时,如果网络错误或服务器返回非标准响应,可能导致未捕获的异常或不明确的错误消息。 - 建议:完善错误处理逻辑,确保所有可能的异常都被捕获并返回用户友好的错误信息。 7. **日志与调试信息不足**: - 当前代码中 `console.log(url, 'url')` 打印了整个 URL 对象,可能包含敏感信息(如查询参数中的 token)。此外,缺乏详细日志来追踪认证失败的具体原因。 - 建议:限制日志内容,避免泄露敏感信息,并增加认证失败时的详细日志(例如,token 是否存在、API 响应状态码等)。 ### 改进后的代码 以下是修改后的代码,修复了上述问题并保持了原有功能: ```javascript:middleware.js import { NextResponse } from 'next/server'; export async function middleware(request) { // 白名单列表,包含不需要验证的 API 路径或前缀 const whiteList = [ '/api/auth/login', '/api/articles/list', '/api/sinfo', '/api/system-info', '/api/check-auth' ]; // 正则表达式匹配文章详情路径(例如 /api/articles/123456) const articleDetailPattern = /^\/api\/articles\/\d+$/; const authToken = request.cookies.get('auth-token'); const url = request.nextUrl.clone(); const pathname = url.pathname; // 检查是否在白名单中或者匹配文章详情路径 // 精确匹配白名单路径,或者检查是否符合文章详情路径模式 const isWhitelisted = whiteList.some(path => pathname === path || (path.endsWith('/') && pathname.startsWith(path))) || articleDetailPattern.test(pathname); // 保护以 /api/ 开头的路径,但排除白名单和文章详情路径 const isProtected = pathname.startsWith('/api/') && !isWhitelisted; // 如果访问的是受保护接口,验证 cookie if (isProtected) { if (!authToken) { return NextResponse.json({ error: '认证失败:缺少 auth-token' }, { status: 401 }); } try { // 调用 check-auth API 路由进行认证验证,设置超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 const checkAuthResponse = await fetch(`${url.origin}/api/check-auth`, { method: 'GET', headers: { 'Cookie': `auth-token=${authToken.value}`, }, signal: controller.signal }); clearTimeout(timeoutId); if (checkAuthResponse.ok) { const result = await checkAuthResponse.json(); if (result.success) { // 认证成功,允许请求继续 return NextResponse.next(); } else { return NextResponse.json({ error: result.error || '认证失败,请重新登录' }, { status: 401 }); } } else { return NextResponse.json({ error: `认证失败:服务器返回 ${checkAuthResponse.status}` }, { status: checkAuthResponse.status }); } } catch (error) { // 捕获网络错误或超时错误 const errorMessage = error.name === 'AbortError' ? '认证请求超时' : '认证检查失败,请稍后重试'; return NextResponse.json({ error: errorMessage }, { status: 503 }); } } return NextResponse.next(); } // 配置中间件匹配路径,仅针对 /api/ 路径 export const config = { matcher: ['/api/:path*'], }; ``` ### 改进点说明 1. **白名单匹配改进**: - 修改 `isWhitelisted` 逻辑,对白名单路径进行精确匹配(`pathname === path`),或允许特定前缀匹配(暂未启用),避免意外匹配子路径。 2. **统一错误响应**: - 删除了重定向逻辑(`errorRedirect`),统一为 JSON 响应,适合 API 请求场景。 3. **超时机制**: - 为 `fetch` 请求增加了 5 秒超时机制,避免 `/api/check-auth` 响应过慢导致中间件挂起。 4. **限制匹配范围**: - 将 `config.matcher` 改为 `['/api/:path*']`,确保中间件仅处理 API 路径,提高性能。 5. **增强错误处理**: - 捕获 `fetch` 可能的所有错误(包括网络错误和超时),并返回明确的错误消息。 6. **日志安全性**: - 移除了可能泄露敏感信息的 `console.log(url, 'url')`,可根据需求添加安全的日志(如仅记录路径)。 ### 额外建议 - **Token 安全性**:如果 `auth-token` 是 JWT 或其他可自验证的 token,可以在中间件层直接验证签名,减少对 `/api/check-auth` 的依赖。 - **缓存机制**:对于频繁访问的用户,可以使用 Redis 或本地内存缓存 token 验证结果,减少 API 调用。 - **白名单扩展**:如果未来有更多路径需要白名单支持,可以将白名单配置为外部文件,便于维护。 - **监控与告警**:在生产环境中,建议对认证失败或超时的情况添加监控和告警,便于快速发现问题。 ### 测试建议 - **测试用例**: - 访问 `/api/auth/login`:应通过白名单,不检查 token。 - 访问 `/api/articles/123456`:应通过正则匹配,不检查 token。 - 访问 `/api/protected/resource` 无 token:应返回 401 错误。 - 访问 `/api/protected/resource` 无效 token:应返回 401 错误。 - 访问 `/api/check-auth` 不可用:应返回 503 错误(超时或服务不可用)。 - **调试工具**:在测试时,可以临时添加日志(如 `console.log(pathname, isProtected, isWhitelisted)`)确认路径匹配逻辑是否正确。 通过以上分析和修改,您的中间件代码逻辑漏洞已得到修复,安全性和健壮性得到提升。如果有其他问题或进一步需求,请随时告知。
在 UniApp 中,如果 `web-view` 的上边距在手机上不生效,这通常是因为 `web-view` 是一个原生组件(在 App 端),其层级较高且默认会占据整个页面。以下是几种解决方案: ### 修改后的代码(使用绝对定位方法): ```html <template> <keep-alive> <!-- 移除容器的 padding-top,改用其他方法 --> <view class="webview-container"> <!-- 添加一个占位视图 --> <view class="spacer"></view> <web-view src="http://14.103.129.210/articles" progress="true" :webview-styles="webviewStyles"></web-view> </view> </keep-alive> </template> <script> // ...原有的 script 代码保持不变... </script> <style> .webview-container { display: flex; flex-direction: column; height: 100vh; position: relative; } /* 添加一个占位元素,用于撑开顶部空间 */ .spacer { height: 80rpx; /* 调整这个值来控制下移距离 */ width: 100%; } /* 确保 web-view 有高度和位置 */ web-view { position: absolute; top: 80rpx; /* 与 spacer 高度一致 */ left: 0; right: 0; bottom: 0; height: calc(100% - 80rpx); /* 重要:计算高度 */ } /* 备选方案:调整 webview 内容而非容器 */ /* 可能会影响页面布局 */ web-view { transform: translateY(80rpx); } </style> ``` ### 其他可能的解决方案: #### 1. **使用 `native` 平台的特定设置**(App端) ```js onReady() { // #ifdef APP-PLUS var currentWebview = this.$scope.$getAppWebview().children()[0]; // 添加延时确保 webview 加载完成 setTimeout(() => { // 调整 webview 位置 currentWebview.setStyle({ top: '80rpx', height: 'calc(100% - 80rpx)' }); }, 500); // #endif } ``` #### 2. **添加导航栏(navigationBar)** 在 pages.json 中添加导航栏,利用导航栏的高度作为间隔: ```json { "pages": [ { "path": "pages/your-page", "style": { "navigationBarTitleText": "标题", "navigationBarBackgroundColor": "#F8F8F8", "app-plus": { "titleNView": { "backgroundColor": "#F8F8F8", "height": "80rpx" } } } } ] } ``` #### 3. **调整页面整体布局** 使用更精确的 flex 布局: ```html <template> <view class="container"> <!-- 顶部预留空间 --> <view class="header-space"></view> <!-- web-view --> <web-view ...></web-view> </view> </template> <style> .container { display: flex; flex-direction: column; height: 100vh; } .header-space { height: 80rpx; width: 100%; } web-view { flex: 1; } </style> ``` #### 4. **尝试使用 `cover-view` 覆盖** 在 web-view 顶部添加一个覆盖层(需要处理事件传递): ```html <view class="container"> <web-view ...></web-view> <cover-view class="top-space"></cover-view> </view> <style> .top-space { position: absolute; top: 0; left: 0; right: 0; height: 80rpx; background: transparent; /* 透明背景 */ /* 如果需要处理点击事件,添加 pointer-events: none; */ } </style> ``` ### 关键注意事项: 1. **组件特性**: - Web-view 是原生组件,部分 CSS 属性(如 z-index, position)可能表现不同于普通组件 - 在 App 端,Web-view 默认覆盖整个页面 2. **响应式设计**: - 使用 rpx 确保在不同设备上效果一致 - 在 CSS 中使用百分比高度而不是固定像素 3. **调试技巧**: - 使用 `console.log(plus.screen)` 打印屏幕信息 - 使用 `console.log(currentWebview.getStyle())` 查看 Web-view 样式 ```js // 在 onReady 中添加调试代码 console.log('屏幕尺寸:', plus.screen.resolutionHeight); console.log('Web-view 样式:', currentWebview.getStyle()); ``` 使用绝对定位方法结合计算高度 (`height: calc(100% - 80rpx)`) 通常是最高效的解决方案,建议首先尝试。
在 Next.js 中,服务端渲染(Server-Side Rendering, SSR)是一种默认支持的渲染方式,它允许在服务器端生成 HTML 内容,然后发送给客户端,从而提高首屏加载速度和 SEO 效果。Next.js 提供了简单而强大的工具来实现 SSR,开发者无需手动配置复杂的服务器端逻辑。以下是关于如何在 Next.js 中进行服务端渲染开发的详细指南,涵盖核心概念、实现步骤以及注意事项。 --- ### **1. 什么是服务端渲染(SSR)?** - **定义**:SSR 是在服务器端预渲染页面内容,将生成的 HTML 发送到客户端,而不是在客户端通过 JavaScript 动态生成内容。 - **优点**: - 提高首屏加载速度(First Contentful Paint, FCP)。 - 改善 SEO(搜索引擎优化),因为爬虫可以直接读取 HTML 内容。 - 更好的用户体验,尤其是在低性能设备或慢速网络上。 - **缺点**: - 服务器负载增加,因为每次请求都需要服务器计算和渲染。 - 相比静态生成(SSG),动态内容可能导致缓存难度增加。 --- ### **2. Next.js 中的 SSR 实现方式** Next.js 默认支持 SSR,开发者只需在页面中使用 `getServerSideProps` 函数来获取数据并在服务器端渲染页面内容。以下是具体步骤和方法: #### **步骤 1:创建页面并使用 `getServerSideProps`** 在 `pages` 目录下创建页面文件,并通过 `getServerSideProps` 函数在服务器端获取数据。 ```javascript // pages/index.js import React from 'react'; export default function Home({ data }) { return ( <div> <h1>服务端渲染示例</h1> <p>从服务器获取的数据: {data.message}</p> </div> ); } // 在服务器端执行的数据获取逻辑 export async function getServerSideProps(context) { // context 包含请求相关信息,如 req, res, query 等 const { req, res, query } = context; // 模拟从 API 获取数据(可以替换为真实的 API 调用) const response = await fetch('https://api.example.com/data'); const data = await response.json(); // 可以根据请求头、cookie 等进行权限验证 const isAuthenticated = req.cookies.token === 'valid-token'; if (!isAuthenticated) { // 服务端重定向到登录页 res.writeHead(302, { Location: '/login' }); res.end(); return { props: {} }; } // 返回数据给页面组件 return { props: { data, // 将数据传递给页面组件 }, }; } ``` #### **步骤 2:运行 Next.js 应用** 确保你的 Next.js 项目已经配置好,运行以下命令启动开发服务器: ```bash npm run dev # 或 yarn dev ``` 访问页面时,Next.js 会在服务器端执行 `getServerSideProps`,获取数据并渲染 HTML 内容。 #### **步骤 3:验证 SSR 效果** - 打开浏览器开发者工具,查看页面源代码(右键 -> 查看页面源代码),你会看到服务器端渲染的完整 HTML 内容,而不是空的 `<div id="__next"></div>`。 - 禁用 JavaScript(在浏览器设置中),页面仍然可以显示内容,证明内容是在服务器端生成的。 --- ### **3. 使用场景和注意事项** #### **适用场景** - 需要动态数据的页面,例如用户仪表盘、实时内容展示。 - 对 SEO 有较高要求的页面,例如博客文章、产品详情页。 - 需要根据用户请求(如 cookie、IP 地址)个性化渲染内容的场景。 #### **注意事项** - **`getServerSideProps` 只在服务器端运行**:不能在其中使用浏览器相关的 API(如 `window` 或 `document`),因为它运行在 Node.js 环境中。 - **性能考虑**:SSR 每次请求都会触发服务器端计算,避免在 `getServerSideProps` 中执行耗时操作,尽量使用缓存(如 Redis)或 CDN。 - **缓存控制**:可以通过设置 HTTP 头(如 `Cache-Control`)来缓存 SSR 页面内容,减少服务器压力。 ```javascript export async function getServerSideProps(context) { const { res } = context; res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59'); return { props: { data: { message: 'Hello from SSR' } } }; } ``` --- ### **4. SSR 与其他渲染方式的对比** Next.js 支持多种渲染方式,开发者可以根据需求选择合适的模式: - **SSR(服务端渲染)**:使用 `getServerSideProps`,适合动态数据和个性化内容。 - **SSG(静态生成)**:使用 `getStaticProps`,适合内容不频繁变化的页面(如博客、文档),可以在构建时生成 HTML。 - **ISR(增量静态再生)**:结合 `getStaticProps` 和 `revalidate` 参数,适合内容定期更新的页面,允许在后台重新生成静态内容。 - **CSR(客户端渲染)**:不使用数据获取函数,完全依赖客户端 JavaScript 渲染,适合高度交互的页面,但不利于 SEO。 --- ### **5. 在 App Router 中实现 SSR(Next.js 13+)** 在 Next.js 13 及以上版本中,引入了 `app` 目录和 App Router,默认组件是 Server Components,支持服务端渲染。实现 SSR 的方式如下: #### **步骤 1:创建页面文件** 在 `app` 目录下创建页面文件,默认情况下页面就是服务端渲染的。 ```javascript // app/page.js import React from 'react'; export default async function Home() { // 直接在组件中获取数据(Server Component) const response = await fetch('https://api.example.com/data'); const data = await response.json(); return ( <div> <h1>服务端渲染示例 (App Router)</h1> <p>从服务器获取的数据: {data.message}</p> </div> ); } ``` #### **步骤 2:动态路由和参数** 如果需要处理动态路由或请求参数,可以通过 `params` 和 `searchParams` 获取。 ```javascript // app/post/[id]/page.js export default async function Post({ params }) { const { id } = params; // 获取动态参数 const response = await fetch(`https://api.example.com/post/${id}`); const data = await response.json(); return ( <div> <h1>帖子详情</h1> <p>ID: {id}</p> <p>内容: {data.content}</p> </div> ); } ``` #### **步骤 3:权限控制和重定向** 在 App Router 中,可以使用 `redirect` 函数进行服务器端重定向。 ```javascript // app/dashboard/page.js import { redirect } from 'next/navigation'; export default async function Dashboard() { const token = getTokenFromCookie(); // 自定义函数获取 token if (!token) { redirect('/login'); // 重定向到登录页 } const response = await fetch('https://api.example.com/dashboard'); const data = await response.json(); return ( <div> <h1>仪表盘</h1> <p>数据: {data.message}</p> </div> ); } function getTokenFromCookie() { // 从 cookie 获取 token(在服务器端需要解析 cookie) return null; // 示例,替换为真实逻辑 } ``` #### **注意事项** - App Router 中的 Server Components 默认在服务器端运行,支持直接调用 `fetch` 等异步操作。 - 如果需要客户端交互(如 `useState`, `useEffect`),需要在文件顶部添加 `"use client"` 指令,将组件标记为 Client Component。 - App Router 提供了更细粒度的缓存控制,可以通过 `export const dynamic = 'force-dynamic'` 强制动态渲染。 ```javascript export const dynamic = 'force-dynamic'; // 强制动态渲染,类似于 SSR export const revalidate = 0; // 禁用缓存 export default async function DynamicPage() { const data = await fetch('https://api.example.com/data', { cache: 'no-store' }); const result = await data.json(); return <div>{result.message}</div>; } ``` --- ### **6. 优化 SSR 性能** - **使用缓存**:对于频繁访问的数据,使用内存缓存(如 Redis)或文件缓存,避免重复请求 API。 - **CDN 加速**:通过 Vercel 或其他 CDN 服务缓存 SSR 页面内容,减少服务器负载。 - **减少阻塞操作**:在 `getServerSideProps` 或 Server Components 中避免同步阻塞操作,尽量使用异步请求。 - **条件渲染**:结合 `getServerSideProps` 和客户端状态管理,仅在必要时触发服务器端渲染。 --- ### **7. 部署 SSR 应用** - **开发环境**:使用 `npm run dev` 启动开发服务器,Next.js 会自动处理 SSR。 - **生产环境**:使用 `npm run build` 和 `npm run start`,确保你的服务器环境支持 Node.js。 - **托管平台**:推荐使用 Vercel,它对 Next.js 提供了开箱即用的支持,包括 SSR 和缓存优化。 - **自定义服务器**:如果需要自定义服务器,可以使用 `next/server` 模块搭建 Express 或其他框架的服务器。 示例自定义服务器: ```javascript // server.js const { createServer } = require('http'); const { parse } = require('url'); const next = require('next'); const dev = process.env.NODE_ENV !== 'production'; const app = next({ dev }); const handle = app.getRequestHandler(); app.prepare().then(() => { createServer((req, res) => { const parsedUrl = parse(req.url, true); handle(req, res, parsedUrl); }).listen(3000, (err) => { if (err) throw err; console.log('> Ready on http://localhost:3000'); }); }); ``` --- ### **总结** - **传统路由(`pages` 目录)**:通过 `getServerSideProps` 实现 SSR,适合需要动态数据和服务器端逻辑的页面。 - **App Router(`app` 目录)**:默认支持 Server Components,直接在组件中获取数据,简化了 SSR 开发。 - **性能优化**:合理使用缓存、CDN 和条件渲染,减少服务器负载。 - **部署**:推荐使用 Vercel 托管,或者自定义 Node.js 服务器。 如果你有具体的 SSR 开发问题(如数据获取、权限控制或性能优化),请告诉我,我会进一步帮助你!
在 Next.js 中实现路由权限控制并实时监听用户状态(例如用户登录失效时跳转到登录页)是一个常见需求。以下是详细的实现方案,涵盖了 `pages` 目录(传统路由)和 `app` 目录(App Router)两种模式。我们将使用上下文(Context)或状态管理工具(如 Redux)存储用户状态,并结合中间件(Middleware)或自定义 HOC(高阶组件)来实现权限控制和状态监听。 --- ### **实现目标** 1. **权限控制**:限制未登录用户访问某些页面,自动重定向到登录页。 2. **实时监听**:检测用户状态变化(如 token 失效或用户登出),一旦失效立即跳转到登录页。 3. **适用场景**:支持客户端和服务器端渲染(SSR/SSG)。 --- ### **方案 1:使用 Context + HOC(适用于 `pages` 目录)** #### **步骤 1:创建 Auth Context** 创建一个上下文用于存储用户登录状态,并在整个应用中共享。 ```javascript // context/AuthContext.js import { createContext, useContext, useState, useEffect } from 'react'; import { useRouter } from 'next/router'; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); // 用户状态,null 表示未登录 const router = useRouter(); // 模拟检查用户状态(可以替换为真实的 API 调用或 token 验证) useEffect(() => { const checkUser = async () => { const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage if (!token) { setUser(null); router.push('/login'); // 未登录跳转到登录页 } else { // 验证 token 是否有效(这里是伪代码) try { const response = await fetch('/api/verify-token', { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { setUser({ token }); // 假设用户有效 } else { localStorage.removeItem('token'); setUser(null); router.push('/login'); } } catch (error) { localStorage.removeItem('token'); setUser(null); router.push('/login'); } } }; checkUser(); // 定时检查 token 是否失效(例如每 5 秒) const interval = setInterval(checkUser, 5000); return () => clearInterval(interval); }, [router]); return ( <AuthContext.Provider value={{ user, setUser }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => useContext(AuthContext); ``` #### **步骤 2:创建 HOC 进行权限控制** 使用高阶组件(HOC)包装受保护的页面,确保只有登录用户才能访问。 ```javascript // hoc/withAuth.js import { useRouter } from 'next/router'; import { useAuth } from '../context/AuthContext'; import { useEffect } from 'react'; const withAuth = (WrappedComponent) => { return (props) => { const { user } = useAuth(); const router = useRouter(); useEffect(() => { if (!user) { router.push('/login'); // 未登录跳转到登录页 } }, [user, router]); if (!user) { return null; // 或者显示加载中... } return <WrappedComponent {...props} />; }; }; export default withAuth; ``` #### **步骤 3:在 _app.js 中包裹 AuthProvider** 在自定义 App 组件中应用 `AuthProvider`,确保所有页面都能访问用户状态。 ```javascript // pages/_app.js import { AuthProvider } from '../context/AuthContext'; export default function MyApp({ Component, pageProps }) { return ( <AuthProvider> <Component {...pageProps} /> </AuthProvider> ); } ``` #### **步骤 4:保护特定页面** 对需要权限控制的页面应用 `withAuth` HOC。 ```javascript // pages/dashboard.js import withAuth from '../hoc/withAuth'; function Dashboard() { return <div>欢迎来到仪表盘!只有登录用户可见。</div>; } export default withAuth(Dashboard); ``` #### **步骤 5:登录页面和登出逻辑** 在登录页面中更新用户状态,并在登出时清除状态。 ```javascript // pages/login.js import { useAuth } from '../context/AuthContext'; import { useRouter } from 'next/router'; export default function Login() { const { setUser } = useAuth(); const router = useRouter(); const handleLogin = () => { // 模拟登录成功(实际应调用 API 获取 token) localStorage.setItem('token', 'mock-token'); setUser({ token: 'mock-token' }); router.push('/dashboard'); }; return <button onClick={handleLogin}>登录</button>; } ``` ```javascript // components/LogoutButton.js import { useAuth } from '../context/AuthContext'; import { useRouter } from 'next/router'; export default function LogoutButton() { const { setUser } = useAuth(); const router = useRouter(); const handleLogout = () => { localStorage.removeItem('token'); setUser(null); router.push('/login'); }; return <button onClick={handleLogout}>登出</button>; } ``` --- ### **方案 2:使用中间件(适用于 `pages` 和 `app` 目录)** Next.js 12 及以上版本引入了中间件(Middleware),可以在请求到达页面之前进行权限检查,非常适合服务器端权限控制。 #### **步骤 1:创建中间件** 在项目根目录创建 `middleware.js` 或 `middleware.ts`。 ```javascript // middleware.js import { NextResponse } from 'next/server'; export function middleware(request) { const token = request.cookies.get('token')?.value; // 假设 token 存储在 cookie 中 const isProtected = request.nextUrl.pathname.startsWith('/dashboard'); // 保护的路由 if (isProtected && !token) { const loginUrl = new URL('/login', request.url); return NextResponse.redirect(loginUrl); } // 如果有 token,可以进一步验证(例如调用 API) // 如果 token 无效,清除 cookie 并重定向到登录页 if (isProtected && token) { // 伪代码:验证 token const isValidToken = validateToken(token); // 替换为实际验证逻辑 if (!isValidToken) { const response = NextResponse.redirect(new URL('/login', request.url)); response.cookies.delete('token'); return response; } } return NextResponse.next(); } // 定义需要中间件处理的路由 export const config = { matcher: ['/dashboard/:path*'], // 保护的路由范围 }; // 模拟 token 验证逻辑 function validateToken(token) { // 这里可以调用 API 或检查 token 是否过期 return token === 'valid-token'; // 仅作示例 } ``` #### **步骤 2:客户端监听状态变化** 中间件主要处理服务器端请求,但客户端状态变化(如用户手动登出或 token 过期)仍需监听。可以在 Context 或全局状态管理中添加监听逻辑(如方案 1)。 #### **步骤 3:设置 Cookie 或 localStorage** 在登录时将 token 存储到 Cookie 或 localStorage 中,供中间件和客户端代码使用。 ```javascript // pages/login.js import { useRouter } from 'next/router'; export default function Login() { const router = useRouter(); const handleLogin = async () => { // 模拟登录成功 document.cookie = 'token=valid-token; path=/'; // 存储到 cookie router.push('/dashboard'); }; return <button onClick={handleLogin}>登录</button>; } ``` --- ### **方案 3:App Router 中的权限控制(Next.js 13+)** 在 `app` 目录中,权限控制可以使用布局(`layout.js`)或中间件实现。 #### **步骤 1:使用布局进行权限控制** 在受保护路由的 `layout.js` 中检查用户状态。 ```javascript // app/dashboard/layout.js import { redirect } from 'next/navigation'; export default function DashboardLayout({ children }) { const token = getTokenFromCookieOrLocalStorage(); // 自定义函数获取 token if (!token) { redirect('/login'); // 重定向到登录页 } return <div>{children}</div>; } function getTokenFromCookieOrLocalStorage() { // 从 cookie 或 localStorage 获取 token return document.cookie.includes('token=valid-token') ? 'valid-token' : null; } ``` #### **步骤 2:实时监听状态** 由于 App Router 支持 Server Components,默认组件无法使用 `useEffect` 进行客户端监听。如果需要实时监听,可以将组件标记为 Client Component(使用 `"use client"`),然后结合 Context 实现类似方案 1 的逻辑。 ```javascript // components/AuthChecker.js 'use client'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; export default function AuthChecker() { const router = useRouter(); useEffect(() => { const checkAuth = () => { const token = document.cookie.includes('token=valid-token') ? 'valid-token' : null; if (!token) { router.push('/login'); } }; checkAuth(); const interval = setInterval(checkAuth, 5000); // 每 5 秒检查一次 return () => clearInterval(interval); }, [router]); return null; } ``` 在页面或布局中引入 `AuthChecker` 组件: ```javascript // app/dashboard/page.js import AuthChecker from '../../components/AuthChecker'; export default function Dashboard() { return ( <> <AuthChecker /> <div>欢迎来到仪表盘!</div> </> ); } ``` #### **步骤 3:结合中间件** App Router 同样支持中间件,代码与方案 2 类似,可以在服务器端提前拦截未授权请求。 --- ### **总结和注意事项** 1. **方案选择**: - 如果使用 `pages` 目录,推荐使用 Context + HOC 实现客户端权限控制,结合中间件增强服务器端检查。 - 如果使用 `app` 目录,推荐使用中间件 + 布局实现权限控制,必要时使用 Client Components 进行状态监听。 2. **安全性**: - 不要仅依赖客户端检查,始终在服务器端(如中间件或 API)验证用户身份。 - 使用 HTTPS 传输 token,避免存储敏感信息在 localStorage 中,优先使用 HttpOnly Cookie。 3. **性能优化**: - 避免过于频繁的状态检查(如定时器),可以结合 WebSocket 或长轮询监听用户状态变化。 4. **用户体验**: - 在跳转到登录页时,可以保存用户当前路径(如通过查询参数),登录后自动跳转回原页面。 如果你有具体的代码问题或需要更详细的实现(例如结合 Redux 或 JWT 验证),请告诉我,我会进一步帮助你!
Next.js 是一个基于 React 的全栈框架,其路由规则主要基于文件系统(File System Based Routing),即通过项目中 `pages` 或 `app` 目录下的文件和文件夹结构来定义路由。Next.js 的路由规则非常直观,开发者无需手动配置复杂的路由表,只需按照约定组织文件即可。以下是 Next.js 路由规则的详细说明,涵盖了 `pages` 目录(传统路由)和 `app` 目录(App Router,Next.js 13 及以上版本引入)两种模式。 --- ### **1. 传统路由:基于 `pages` 目录** 在 Next.js 12 及之前的版本(以及未启用 App Router 的项目)中,路由基于 `pages` 目录下的文件结构。规则如下: #### **基本路由规则** - **文件映射到路由**:`pages` 目录下的每个 `.js`, `.jsx`, `.ts`, 或 `.tsx` 文件都会映射到一个路由。 - 例如:`pages/index.js` 映射到根路径 `/`。 - 例如:`pages/about.js` 映射到路径 `/about`。 - **嵌套路由**:通过文件夹创建嵌套路由。 - 例如:`pages/blog/post.js` 映射到路径 `/blog/post`。 - **动态路由**:通过在文件名或文件夹名中使用方括号 `[param]` 定义动态参数。 - 例如:`pages/post/[id].js` 映射到 `/post/1`, `/post/abc` 等路径,`id` 可以通过 `useRouter` 获取。 - 例如:`pages/shop/[category]/[product].js` 映射到 `/shop/electronics/phone`,其中 `category` 和 `product` 是动态参数。 - **可选的 Catch-All 路由**:使用 `[...param]` 定义可选的动态路由,捕获多个路径段。 - 例如:`pages/docs/[...slug].js` 可以匹配 `/docs/a`, `/docs/a/b`, `/docs/a/b/c` 等,`slug` 是一个数组。 - **强制 Catch-All 路由**:使用 `[[...param]]` 定义可选的动态路由,包含根路径。 - 例如:`pages/docs/[[...slug]].js` 可以匹配 `/docs`, `/docs/a`, `/docs/a/b` 等。 #### **特殊文件** - **_app.js**:自定义 App 组件,用于全局布局或状态管理,不映射到具体路由。 - **_document.js**:自定义 HTML 文档结构,仅在服务器端渲染时生效,不映射到路由。 - **_error.js**:自定义错误页面,用于处理 404 或 500 错误。 - **404.js**:自定义 404 页面。 - **500.js**:自定义 500 页面。 #### **API 路由** - 在 `pages/api` 目录下定义后端接口,文件结构映射到 `/api/*` 路径。 - 例如:`pages/api/hello.js` 映射到 `/api/hello`。 - 支持动态参数,例如:`pages/api/post/[id].js` 映射到 `/api/post/1`。 #### **中间件和重写规则** - 使用 `next.config.js` 或中间件(Next.js 12 引入)可以重写路由规则、实现重定向或国际化支持。 --- ### **2. 新路由:基于 `app` 目录(App Router,Next.js 13 及以上)** 从 Next.js 13 开始,引入了基于 `app` 目录的新路由模式(App Router),旨在支持 React Server Components、更好的性能优化和更灵活的布局。规则如下: #### **基本路由规则** - **文件夹映射到路由**:`app` 目录下的每个文件夹对应一个路由段,文件 `page.js` 或 `page.tsx` 定义该路由的内容。 - 例如:`app/page.js` 映射到根路径 `/`。 - 例如:`app/about/page.js` 映射到路径 `/about`。 - **嵌套路由**:通过嵌套文件夹定义嵌套路由。 - 例如:`app/blog/post/page.js` 映射到 `/blog/post`。 - **动态路由**:使用 `[param]` 文件夹定义动态参数。 - 例如:`app/post/[id]/page.js` 映射到 `/post/1`, `/post/abc`,`id` 可通过 `params` 获取。 - **可选的 Catch-All 路由**:使用 `[...param]` 文件夹捕获多个路径段。 - 例如:`app/docs/[...slug]/page.js` 匹配 `/docs/a`, `/docs/a/b` 等。 - **强制 Catch-All 路由**:使用 `[[...param]]` 文件夹,包含根路径。 - 例如:`app/docs/[[...slug]]/page.js` 匹配 `/docs`, `/docs/a` 等。 #### **布局系统** - **layout.js**:定义布局文件,可以为特定路由或路由组提供共享布局。 - 例如:`app/layout.js` 是根布局,包裹所有页面。 - 例如:`app/dashboard/layout.js` 为 `/dashboard` 及其子路由定义布局。 - 布局可以嵌套,子路由会继承父路由的布局。 #### **其他特殊文件** - **error.js**:定义错误边界,处理特定路由的错误。 - **loading.js**:定义加载状态 UI,与 Suspense 结合使用。 - **not-found.js**:自定义 404 页面。 - **route.js**:定义 API 路由,类似 `pages/api`,但支持更多 HTTP 方法。 #### **路由组(Route Groups)** - 使用 `(group-name)` 形式的文件夹定义路由组,用于组织代码或应用不同布局,不影响 URL 路径。 - 例如:`app/(marketing)/about/page.js` 和 `app/(marketing)/contact/page.js` 共享一个布局,但 URL 仍然是 `/about` 和 `/contact`。 #### **并行路由(Parallel Routes)** - 使用 `@folder` 语法定义并行路由,用于在同一页面渲染多个独立的内容。 - 例如:`app/dashboard/@analytics/page.js` 和 `app/dashboard/@overview/page.js` 可以在 `/dashboard` 页面同时渲染。 #### **中间件和配置** - App Router 支持更强大的中间件,可以用于重定向、URL 重写、身份验证等。 - 通过 `next.config.js` 或 `middleware.js` 配置。 #### **Server Components 和 Client Components** - 默认情况下,App Router 中的组件是 Server Components(在服务器端渲染)。 - 使用 `"use client"` 指令将组件标记为 Client Components(在客户端渲染),支持交互性。 --- ### **3. 路由优先级** 在 `pages` 和 `app` 目录中,Next.js 按照以下优先级解析路由: 1. 静态路由优先于动态路由。 - 例如:`/about` 会优先匹配 `pages/about.js`,而不是 `pages/[slug].js`。 2. 具体路径优先于 Catch-All 路由。 - 例如:`/docs/a` 会优先匹配 `pages/docs/a.js`,而不是 `pages/docs/[...slug].js`。 --- ### **4. 获取路由参数和查询参数** - **在 `pages` 目录中**: - 使用 `useRouter` Hook(来自 `next/router`)获取动态参数和查询参数。 ```javascript import { useRouter } from 'next/router'; export default function Post() { const router = useRouter(); const { id } = router.query; // 获取动态参数 return <div>Post ID: {id}</div>; } ``` - **在 `app` 目录中**: - 动态参数通过 `params` 传递给页面组件,查询参数通过 `searchParams` 获取。 ```javascript export default function Post({ params, searchParams }) { const { id } = params; // 获取动态参数 const query = searchParams.q; // 获取查询参数 ?q=xxx return <div>Post ID: {id}, Query: {query}</div>; } ``` --- ### **5. 路由跳转** - **客户端导航**:使用 `next/link` 组件或 `useRouter` 的 `push` 方法。 ```javascript import Link from 'next/link'; <Link href="/about">About Us</Link>; ``` ```javascript import { useRouter } from 'next/router'; // 或 next/navigation(App Router) const router = useRouter(); router.push('/about'); ``` - **服务器端重定向**:使用 `getServerSideProps` 或 `getStaticProps` 返回 `redirect`。 ```javascript export async function getServerSideProps(context) { return { redirect: { destination: '/about', permanent: false, }, }; } ``` --- ### **6. 国际化路由** Next.js 支持国际化路由,通过 `next.config.js` 配置 `i18n` 实现多语言支持。 ```javascript module.exports = { i18n: { locales: ['en', 'zh'], defaultLocale: 'en', }, }; ``` - 路由会自动添加语言前缀,例如 `/en/about` 和 `/zh/about`。 --- ### **总结** - **Pages Router**(`pages` 目录):传统路由,基于文件系统,简单易用,适合小型项目或需要快速开发的场景。 - **App Router**(`app` 目录):Next.js 13+ 引入,支持 Server Components、布局系统和更复杂的功能,适合现代大型应用。 - 路由规则的核心是文件系统约定,开发者通过文件和文件夹的命名来定义静态、动态和嵌套路由。 如果你有具体的路由问题或需要某个功能的代码示例,请告诉我,我会进一步帮助你!
### `useRef` 在 React 中的主要使用场景 #### 📌 1. **访问 DOM 元素** 最常见的用法,用于直接操作 DOM 节点(如聚焦输入框、测量元素尺寸): ```jsx function TextInput() { const inputRef = useRef(null); const focusInput = () => { inputRef.current.focus(); // 手动聚焦输入框 }; return ( <> <input ref={inputRef} type="text" /> <button onClick={focusInput}>聚焦输入框</button> </> ); } ``` #### ⏱️ 2. **存储计时器 ID** 保存 `setInterval`/`setTimeout` 的返回值,用于后续清除: ```jsx function Timer() { const timerRef = useRef(null); useEffect(() => { timerRef.current = setInterval(() => { console.log('定时器运行中...'); }, 1000); return () => clearInterval(timerRef.current); // 组件卸载时清除 }, []); } ``` #### 📊 3. **保存前次状态** 实现类似类组件中 `prevProps`/`prevState` 的功能: ```jsx function Counter() { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { prevCountRef.current = count; // 更新为当前值 }); return ( <div> 当前值: {count}, 前次值: {prevCountRef.current} </div> ); } ``` #### 🧮 4. **缓存计算开销大的值** 存储无需触发重新渲染的派生数据: ```jsx function ExpensiveComponent({ data }) { const cachedDataRef = useRef(); if (!cachedDataRef.current) { // 仅当第一次渲染时计算 cachedDataRef.current = heavyComputation(data); } return <div>{cachedDataRef.current}</div>; } ``` #### 🎬 5. **存储动画帧 ID** 用于 `requestAnimationFrame` 动画控制: ```jsx function Animation() { const frameRef = useRef(); const animate = () => { // 动画逻辑... frameRef.current = requestAnimationFrame(animate); }; useEffect(() => { frameRef.current = requestAnimationFrame(animate); return () => cancelAnimationFrame(frameRef.current); }, []); } ``` #### 📸 6. **保存第三方库实例** 存储非 React 管理的对象实例(如 D3 图表、地图库): ```jsx function Chart({ data }) { const chartRef = useRef(null); const chartInstance = useRef(); useEffect(() => { if (chartRef.current) { // 初始化图表 chartInstance.current = new ThirdPartyChart(chartRef.current, data); } return () => chartInstance.current.destroy(); // 清理 }, [data]); return <div ref={chartRef} />; } ``` #### 🪢 7. **记录组件挂载状态** 检测组件是否仍在挂载状态(避免更新卸载组件的状态): ```jsx function DataFetcher() { const isMountedRef = useRef(true); useEffect(() => { fetchData().then(data => { if (isMountedRef.current) { // 仅当组件挂载时更新状态 setData(data); } }); return () => { isMountedRef.current = false; // 卸载时标记 }; }, []); } ``` #### 🧩 8. **控制自定义 Hook 内部状态** 在自定义 Hook 中维护私有状态: ```jsx function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; // 更新前值但不触发渲染 }, [value]); return ref.current; // 返回前次值 } ``` ### 💡 核心特性总结 | 特性 | 说明 | |--------------------------|----------------------------------------------------------------------| | **跨渲染周期持久化** | 存储的值在组件重新渲染时保持不变 | | **修改不触发重渲染** | 更改 `.current` 值不会导致组件更新 | | **类似实例变量** | 类似类组件的实例字段(非状态数据) | | **同步更新** | 修改后立即可读取新值(不同于 useState 的异步批量更新) | ### ⚠️ 注意事项 1. **不要用于渲染数据**:修改 `ref` 不会触发重渲染 2. **初始化避免函数调用**:`useRef(computeExpensiveValue())` 会在每次渲染执行,应改用: ```jsx const ref = useRef(null); if (ref.current === null) { ref.current = computeExpensiveValue(); // 惰性初始化 } ``` 3. **严格模式问题**:开发环境下双重渲染可能导致 `ref` 被重置(使用 `useEffect` 可避免) > 当需要操作 DOM 或存储与渲染无关的可变值时优先考虑 `useRef`,需要触发视图更新则使用 `useState`。